From 211305c5727e56949d834f393da1f930d1962f6e Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 8 Jan 2026 10:45:44 +0200 Subject: [PATCH 01/47] 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) From a6d90c319e0b099aa380496ad99f42f3d11880fd Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 8 Jan 2026 10:55:38 +0200 Subject: [PATCH 02/47] feat: Add lifecycle-viewmodel KMP dependencies to commons Adds androidx.lifecycle.viewmodel.compose and lifecycle.runtime.compose to commons/commonMain as preparation for extracting ViewModels to shared code. These dependencies have KMP support since 2.8.0 (currently on 2.10.0). Preparation for Phase A2: Extract Thread ViewModels to commons. Co-Authored-By: Claude Sonnet 4.5 --- commons/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 90ccd7d18..af8cceeac 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -67,6 +67,10 @@ kotlin { implementation(compose.materialIconsExtended) implementation(compose.components.uiToolingPreview) + // Lifecycle ViewModel (KMP since 2.8.0) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + // Image loading (Coil 3 - KMP) implementation(libs.coil.compose) implementation(libs.coil.okhttp) From 9836a47f656057f5769e1e0fda453dfa3fbe0e0b Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 9 Jan 2026 06:48:40 +0200 Subject: [PATCH 03/47] update Android imports --- .../amethyst/model/LocalCache.kt | 13 +++ .../amethyst/service/BundledUpdates.kt | 27 ++++++ .../amethyst/ui/dal/FeedFilters.kt | 27 ++++++ .../amethyst/ui/feeds/FeedContentStateView.kt | 1 + .../amethyst/ui/feeds/FeedLoaded.kt | 1 + .../amethyst/ui/feeds/FeedStates.kt | 32 ++++++ .../amethyst/ui/screen/FeedView.kt | 4 +- .../amethyst/ui/screen/FeedViewModel.kt | 53 ++-------- .../amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../loggedIn/AccountFeedContentStates.kt | 29 +++--- .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../dal/BookmarkPrivateFeedViewModel.kt | 4 +- .../dal/BookmarkPublicFeedViewModel.kt | 4 +- .../loggedIn/chats/feed/ChatFeedView.kt | 4 +- .../privateDM/dal/ChatroomFeedViewModel.kt | 7 +- .../chats/rooms/feed/ChatroomListFeedView.kt | 4 +- .../chats/rooms/feed/ChatroomListTabs.kt | 2 +- .../rooms/singlepane/MessagesSinglePane.kt | 2 +- .../chats/rooms/twopane/ChatroomListPane.kt | 2 +- .../chats/rooms/twopane/MessagesTwoPane.kt | 2 +- .../communities/dal/CommunityFeedViewModel.kt | 4 +- .../dal/CommunityModerationFeedViewModel.kt | 4 +- .../loggedIn/discover/DiscoverScreen.kt | 4 +- .../screen/loggedIn/drafts/DraftListScreen.kt | 4 +- .../dal/NIP90ContentDiscoveryFeedViewModel.kt | 4 +- ...ollowPackFeedConversationsFeedViewModel.kt | 4 +- .../FollowPackFeedNewThreadFeedViewModel.kt | 4 +- .../geohash/dal/GeoHashFeedViewModel.kt | 4 +- .../hashtag/dal/HashtagFeedViewModel.kt | 4 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 4 +- .../notifications/CardFeedContentState.kt | 18 +--- .../loggedIn/notifications/CardFeedState.kt | 2 +- .../notifications/EqualImmutableLists.kt | 31 ++++++ .../dal/UserProfileBookmarksFeedViewModel.kt | 4 +- .../UserProfileConversationsFeedViewModel.kt | 4 +- .../profile/gallery/ProfileGalleryFeed.kt | 2 +- .../dal/UserProfileGalleryFeedViewModel.kt | 4 +- .../header/apps/DisplayAppRecommendations.kt | 2 +- .../UserAppRecommendationsFeedViewModel.kt | 4 +- .../dal/UserProfileMutualFeedViewModel.kt | 4 +- .../dal/UserProfileNewThreadsFeedViewModel.kt | 4 +- .../profile/relays/RelayFeedViewModel.kt | 2 +- .../dal/UserProfileReportFeedViewModel.kt | 4 +- .../loggedIn/search/SearchBarViewModel.kt | 2 +- .../loggedIn/settings/StringFeedViewModel.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../threadview/dal/LevelFeedViewModel.kt | 79 ++------------- .../threadview/dal/ThreadFeedViewModel.kt | 3 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 4 +- .../commons/utils/DebugUtils.android.kt | 25 +++++ .../commons/model/cache/ICacheEventStream.kt | 48 +++++++++ .../commons/model/cache/ICacheProvider.kt | 17 ++++ .../commons}/service/BundledUpdate.kt | 83 +++++++++------- .../commons/ui/feeds}/AdditiveFeedFilter.kt | 6 +- .../commons}/ui/feeds/FeedContentState.kt | 19 ++-- .../amethyst/commons/ui/feeds}/FeedFilter.kt | 6 +- .../amethyst/commons}/ui/feeds/FeedState.kt | 4 +- .../commons/ui/feeds}/IAdditiveFeedFilter.kt | 2 +- .../amethyst/commons/ui/feeds}/IFeedFilter.kt | 2 +- .../commons}/ui/feeds/InvalidatableContent.kt | 2 +- .../amethyst/commons/utils/DebugUtils.kt | 62 ++++++++++++ .../amethyst/commons/utils/ListUtils.kt | 37 +++++++ .../commons/viewmodels/FeedViewModel.kt | 72 ++++++++++++++ .../viewmodels/thread/LevelFeedViewModel.kt | 97 +++++++++++++++++++ .../amethyst/commons/utils/DebugUtils.jvm.kt | 23 +++++ 65 files changed, 682 insertions(+), 262 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdates.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilters.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedStates.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt create mode 100644 commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.android.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheEventStream.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/service/BundledUpdate.kt (74%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds}/AdditiveFeedFilter.kt (86%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/feeds/FeedContentState.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds}/FeedFilter.kt (86%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/feeds/FeedState.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds}/IAdditiveFeedFilter.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds}/IFeedFilter.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/feeds/InvalidatableContent.kt (96%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/ListUtils.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/FeedViewModel.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/LevelFeedViewModel.kt create mode 100644 commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.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 d8aeb392c..80441bbb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -402,6 +402,19 @@ object LocalCache : ILocalCache, ICacheProvider { return null } + override fun getEventStream(): com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream = + object : com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream { + override val newEventBundles = live.newEventBundles + override val deletedEventBundles = live.deletedEventBundles + } + + override fun hasBeenDeleted(event: Any): Boolean = + if (event is Event) { + deletionIndex.hasBeenDeleted(event) + } else { + false + } + fun getOrAddAliasNote( idHex: String, note: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdates.kt new file mode 100644 index 000000000..f79f238d2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdates.kt @@ -0,0 +1,27 @@ +/** + * 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.service + +// Re-export from commons for backwards compatibility +typealias BundledUpdate = com.vitorpamplona.amethyst.commons.service.BundledUpdate +typealias BasicBundledUpdate = com.vitorpamplona.amethyst.commons.service.BasicBundledUpdate +typealias BundledInsert = com.vitorpamplona.amethyst.commons.service.BundledInsert +typealias BasicBundledInsert = com.vitorpamplona.amethyst.commons.service.BasicBundledInsert diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilters.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilters.kt new file mode 100644 index 000000000..21d18a1dd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilters.kt @@ -0,0 +1,27 @@ +/** + * 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.ui.dal + +// Re-export from commons for backwards compatibility +typealias IFeedFilter = com.vitorpamplona.amethyst.commons.ui.feeds.IFeedFilter +typealias IAdditiveFeedFilter = com.vitorpamplona.amethyst.commons.ui.feeds.IAdditiveFeedFilter +typealias FeedFilter = com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter +typealias AdditiveFeedFilter = com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt index 31aa9f1b4..b4bcb797a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt index 1792ef4d0..fc943a425 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedStates.kt new file mode 100644 index 000000000..20654cb44 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedStates.kt @@ -0,0 +1,32 @@ +/** + * 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.ui.feeds + +// Re-export from commons for backwards compatibility - import everything +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState as CommonsFeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState as CommonsFeedState +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent as CommonsInvalidatableContent +import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState as CommonsLoadedFeedState + +typealias FeedState = CommonsFeedState +typealias LoadedFeedState = CommonsLoadedFeedState +typealias InvalidatableContent = CommonsInvalidatableContent +typealias FeedContentState = CommonsFeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 6b3381180..c3d8cbd99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -28,12 +28,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 0ba80943e..948596a86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -20,52 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen -import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent -import com.vitorpamplona.quartz.utils.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -@Stable -abstract class FeedViewModel( +// Re-export from commons for backwards compatibility +typealias FeedViewModel = com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel + +/** + * Android-specific FeedViewModel base class that provides LocalCache as the cache provider. + * Subclasses can extend this to automatically use LocalCache without passing cacheProvider. + */ +abstract class AndroidFeedViewModel( localFilter: FeedFilter, -) : ViewModel(), - InvalidatableContent { - val feedState = FeedContentState(localFilter, viewModelScope) - - override val isRefreshing = feedState.isRefreshing - - fun sendToTop() = feedState.sendToTop() - - suspend fun sentToTop() = feedState.sentToTop() - - override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing) - - init { - Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}") - viewModelScope.launch(Dispatchers.IO) { - LocalCache.live.newEventBundles.collect { newNotes -> - Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}") - feedState.updateFeedWith(newNotes) - } - } - - viewModelScope.launch(Dispatchers.IO) { - LocalCache.live.deletedEventBundles.collect { newNotes -> - Log.d("Rendering Metrics", "Delete from feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}") - feedState.deleteFromFeed(newNotes) - } - } - } - - override fun onCleared() { - Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") - super.onCleared() - } -} +) : FeedViewModel(localFilter, LocalCache) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index b31b33843..82bc48a4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 10982b6c8..c9a75058c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -20,11 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListKnownFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListNewFeedFilter @@ -50,28 +51,28 @@ class AccountFeedContentStates( val scope: CoroutineScope, ) { val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope) - val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope) - val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope) + val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope, LocalCache) + val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope, LocalCache) - val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope) - val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope) + val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope, LocalCache) + val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope, LocalCache) - val videoFeed = FeedContentState(VideoFeedFilter(account), scope) + val videoFeed = FeedContentState(VideoFeedFilter(account), scope, LocalCache) - val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope) - val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope) - val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope) - val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope) - val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope) - val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope) - val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope) + val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope, LocalCache) + val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope, LocalCache) + val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope, LocalCache) + val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope, LocalCache) + val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope, LocalCache) + val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope, LocalCache) + val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache) val notifications = CardFeedContentState(NotificationFeedFilter(account), scope) val notificationSummary = NotificationSummaryState(account) val feedListOptions = TopNavFilterState(account, scope) - val drafts = FeedContentState(DraftEventsFeedFilter(account), scope) + val drafts = FeedContentState(DraftEventsFeedFilter(account), scope, LocalCache) suspend fun init() { notificationSummary.initializeSuspend() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 9c983e475..3072e0d18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings @@ -75,7 +76,6 @@ import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt index a0df51e0d..c95eb69a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel @Stable class BookmarkPrivateFeedViewModel( val account: Account, -) : FeedViewModel(BookmarkPrivateFeedFilter(account)) { +) : AndroidFeedViewModel(BookmarkPrivateFeedFilter(account)) { class Factory( val account: Account, ) : ViewModelProvider.Factory { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt index 96b71053e..ece24a216 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel @Stable class BookmarkPublicFeedViewModel( val account: Account, -) : FeedViewModel(BookmarkPublicFeedFilter(account)) { +) : AndroidFeedViewModel(BookmarkPublicFeedFilter(account)) { class Factory( val account: Account, ) : ViewModelProvider.Factory { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index d411332a1..f6688afcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -30,12 +30,12 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 9b434ffce..95173849d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -24,12 +24,13 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.ListChange +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers @@ -53,7 +54,7 @@ abstract class ListChangeFeedViewModel( localFilter: ChangesFlowFilter, ) : ViewModel(), InvalidatableContent { - val feedState = FeedContentState(localFilter, viewModelScope) + val feedState = FeedContentState(localFilter, viewModelScope, LocalCache) override val isRefreshing = feedState.isRefreshing diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index a43e09e9c..495dece01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -31,11 +31,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 15a8a6371..229f49977 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt index 6503d9f85..ca318d35f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt index 0525c2bf9..0886cf683 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 8a09b559d..22ab84771 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -36,8 +36,8 @@ import com.google.accompanist.adaptive.FoldAwareConfiguration import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane import com.google.accompanist.adaptive.calculateDisplayFeatures +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.components.getActivity -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt index abd71e4ae..c476689bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class CommunityFeedViewModel( val note: AddressableNote, val account: Account, -) : FeedViewModel(CommunityFeedFilter(note, account)) { +) : AndroidFeedViewModel(CommunityFeedFilter(note, account)) { class Factory( val note: AddressableNote, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt index 14b423fac..5d58f046e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class CommunityModerationFeedViewModel( val note: AddressableNote, val account: Account, -) : FeedViewModel(CommunityModerationFeedFilter(note, account)) { +) : AndroidFeedViewModel(CommunityModerationFeedFilter(note, account)) { class Factory( val note: AddressableNote, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index dfccfe818..018fdd5e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -55,11 +55,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index 95179f429..715d87000 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -46,9 +46,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt index 0b3e7cd86..35a7b1694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt @@ -24,14 +24,14 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel @Stable class NIP90ContentDiscoveryFeedViewModel( val account: Account, dvmKey: String, requestId: String, -) : FeedViewModel(NIP90ContentDiscoveryResponseFilter(account, dvmKey, requestId)) { +) : AndroidFeedViewModel(NIP90ContentDiscoveryResponseFilter(account, dvmKey, requestId)) { class Factory( val account: Account, val dvmKey: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt index 3851b9dfb..95b617b35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class FollowPackFeedConversationsFeedViewModel( val note: AddressableNote, val account: Account, -) : FeedViewModel(FollowPackFeedConversationsFeedFilter(note, account)) { +) : AndroidFeedViewModel(FollowPackFeedConversationsFeedFilter(note, account)) { class Factory( val note: AddressableNote, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt index 74b9159a6..86a590c49 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class FollowPackFeedNewThreadFeedViewModel( val note: AddressableNote, val account: Account, -) : FeedViewModel(FollowPackFeedNewThreadFeedFilter(note, account)) { +) : AndroidFeedViewModel(FollowPackFeedNewThreadFeedFilter(note, account)) { class Factory( val note: AddressableNote, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt index bd754f7a2..3f2767031 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -33,7 +33,7 @@ class GeoHashFeedViewModel( val geohash: String, val relays: Set, val account: Account, -) : FeedViewModel( +) : AndroidFeedViewModel( GeoHashFeedFilter(geohash, relays, account, LocalCache), ) { @Suppress("UNCHECKED_CAST") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt index b1b487941..77f81cc1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -33,7 +33,7 @@ class HashtagFeedViewModel( val hashtag: String, val relays: Set, val account: Account, -) : FeedViewModel( +) : AndroidFeedViewModel( HashtagFeedFilter(hashtag, relays, account, LocalCache), ) { @Suppress("UNCHECKED_CAST") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 3cde3f940..e0bff36ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -55,6 +55,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel @@ -63,8 +65,6 @@ import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 8dc2fd2f4..db70c3229 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent +import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache @@ -35,8 +37,6 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrderCard import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent -import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group @@ -460,20 +460,6 @@ class CardFeedContentState( } } -fun equalImmutableLists( - list1: ImmutableList, - list2: ImmutableList, -): Boolean { - if (list1 === list2) return true - if (list1.size != list2.size) return false - for (i in 0 until list1.size) { - if (list1[i] !== list2[i]) { - return false - } - } - return true -} - @Immutable data class CombinedZap( val request: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt index 776841efc..7f696f9bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji -import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt new file mode 100644 index 000000000..eefb211e0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt @@ -0,0 +1,31 @@ +/** + * 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.ui.screen.loggedIn.notifications + +import kotlinx.collections.immutable.ImmutableList + +// Re-export from commons for backwards compatibility +fun equalImmutableLists( + list1: ImmutableList, + list2: ImmutableList, +): Boolean = + com.vitorpamplona.amethyst.commons.utils + .equalImmutableLists(list1, list2) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt index 8a84b7086..ed1b57d89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserProfileBookmarksFeedViewModel( val user: User, val account: Account, -) : FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) { +) : AndroidFeedViewModel(UserProfileBookmarksFeedFilter(user, account)) { class Factory( val user: User, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt index 3b276e1a7..589786bf6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserProfileConversationsFeedViewModel( val user: User, val account: Account, -) : FeedViewModel(UserProfileConversationsFeedFilter(user, account)) { +) : AndroidFeedViewModel(UserProfileConversationsFeedFilter(user, account)) { class Factory( val user: User, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt index 553d165ab..9ca66f9b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt @@ -33,10 +33,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt index 93ff86b75..583a7106b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserProfileGalleryFeedViewModel( val user: User, val account: Account, -) : FeedViewModel(UserProfileGalleryFeedFilter(user, account)) { +) : AndroidFeedViewModel(UserProfileGalleryFeedFilter(user, account)) { class Factory( val user: User, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt index fc6b2359b..deced7a95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt index 2d169e701..16aa6e938 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt @@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserAppRecommendationsFeedViewModel( val user: User, -) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) { +) : AndroidFeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) { class Factory( val user: User, ) : ViewModelProvider.Factory { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt index 36dfbc4ce..f6b09f751 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserProfileMutualFeedViewModel( val user: User, val account: Account, -) : FeedViewModel(UserProfileMutualFeedFilter(user, account)) { +) : AndroidFeedViewModel(UserProfileMutualFeedFilter(user, account)) { class Factory( val user: User, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt index 0a92c613f..81b573e33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserProfileNewThreadsFeedViewModel( val user: User, val account: Account, -) : FeedViewModel(UserProfileNewThreadFeedFilter(user, account)) { +) : AndroidFeedViewModel(UserProfileNewThreadFeedFilter(user, account)) { class Factory( val user: User, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 81440c8ec..1d400f1f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -25,9 +25,9 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt index b01ab9936..f34a77200 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt @@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel class UserProfileReportFeedViewModel( val user: User, -) : FeedViewModel(UserProfileReportsFeedFilter(user)) { +) : AndroidFeedViewModel(UserProfileReportsFeedFilter(user)) { class Factory( val user: User, ) : ViewModelProvider.Factory { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 583083c8c..c7b959b56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -29,11 +29,11 @@ import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt index 6741a207d..dd13b7ab8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e3f5068b9..7e497301d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -74,6 +74,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus @@ -83,7 +84,6 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav 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 53742b7c3..7f77116c3 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 @@ -20,76 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal -import androidx.compose.foundation.interaction.DragInteraction -import androidx.compose.foundation.lazy.LazyListState -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.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.feeds.FeedState -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest -abstract class LevelFeedViewModel( +// Re-export from commons for backwards compatibility +typealias LevelFeedViewModel = com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel + +/** + * Android-specific LevelFeedViewModel base class that provides LocalCache as the cache provider. + * Subclasses can extend this to automatically use LocalCache without passing cacheProvider. + */ +abstract class AndroidLevelFeedViewModel( localFilter: FeedFilter, -) : FeedViewModel(localFilter) { - var llState: LazyListState by mutableStateOf(LazyListState(0, 0)) - - val hasDragged = mutableStateOf(false) - - val selectedIDHex = - llState.interactionSource.interactions - .onEach { - if (it is DragInteraction.Start) { - hasDragged.value = true - } - }.stateIn( - viewModelScope, - SharingStarted.Eagerly, - null, - ) - - @OptIn(ExperimentalCoroutinesApi::class) - val levelCacheFlow: StateFlow> = - feedState.feedContent - .transformLatest { feed -> - emitAll( - if (feed is FeedState.Loaded) { - feed.feed.map { - val cache = mutableMapOf() - it.list.forEach { - ThreadLevelCalculator.replyLevel(it, cache) - } - cache - } - } else { - MutableStateFlow(mapOf()) - }, - ) - }.flowOn(Dispatchers.IO) - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - mapOf(), - ) - - fun levelFlowForItem(note: Note) = - levelCacheFlow - .map { - it[note] ?: 0 - }.distinctUntilChanged() -} +) : LevelFeedViewModel(localFilter, LocalCache) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt index 66db36a8d..5c408f2f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt @@ -23,11 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache class ThreadFeedViewModel( account: Account, noteId: String, -) : LevelFeedViewModel(ThreadFeedFilter(account, noteId)) { +) : com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel(ThreadFeedFilter(account, noteId), LocalCache) { class Factory( val account: Account, val noteId: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 16a001a81..225559dce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -51,14 +51,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status -import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.android.kt new file mode 100644 index 000000000..4e62cb18d --- /dev/null +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.android.kt @@ -0,0 +1,25 @@ +/** + * 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.utils + +// Debug flag for commons module - kept false to avoid application dependencies +// Application-level modules (amethyst, desktopApp) can implement their own debug timing +actual val isDebug: Boolean = false diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheEventStream.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheEventStream.kt new file mode 100644 index 000000000..b51a5b257 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheEventStream.kt @@ -0,0 +1,48 @@ +/** + * 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.cache + +import com.vitorpamplona.amethyst.commons.model.Note +import kotlinx.coroutines.flow.SharedFlow + +/** + * Event stream interface for cache updates. + * + * Abstracts the real-time event notification system used by ViewModels + * to react to new notes and deletions. Platform implementations + * (Android LocalCache, Desktop cache) provide these streams. + * + * ViewModels collect these flows to incrementally update feed state + * without full refresh. + */ +interface ICacheEventStream { + /** + * Flow of new note bundles added to the cache. + * Emits sets of Note objects when new events arrive from relays. + */ + val newEventBundles: SharedFlow> + + /** + * Flow of deleted note bundles removed from the cache. + * Emits sets of Note objects when deletion events are processed. + */ + val deletedEventBundles: SharedFlow> +} 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 d1d0d8de8..7d1f32f98 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 @@ -79,6 +79,23 @@ interface ICacheProvider { * @return The Note (existing or newly created) */ fun checkGetOrCreateNote(hexKey: HexKey): Any? + + /** + * Gets the event stream for cache updates. + * Used by ViewModels to react to new notes and deletions. + * + * @return The event stream interface + */ + fun getEventStream(): ICacheEventStream + + /** + * Checks if an event has been deleted via NIP-09 deletion events. + * Used by feed state to filter out deleted notes. + * + * @param event The event to check + * @return true if the event has been deleted, false otherwise + */ + fun hasBeenDeleted(event: Any): Boolean } /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdate.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BundledUpdate.kt similarity index 74% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdate.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BundledUpdate.kt index 2c5db2a8d..5311e1b20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdate.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BundledUpdate.kt @@ -18,7 +18,7 @@ * 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.service +package com.vitorpamplona.amethyst.commons.service import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineDispatcher @@ -30,9 +30,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.atomic.AtomicBoolean /** This class is designed to have a waiting time between two calls of invalidate */ class BundledUpdate( @@ -65,21 +65,25 @@ class BasicBundledUpdate( val dispatcher: CoroutineDispatcher = Dispatchers.IO, val scope: CoroutineScope, ) { - private var onlyOneInBlock = AtomicBoolean() + private val mutex = Mutex() + private var isProcessing = false private var invalidatesAgain = false fun invalidate( ignoreIfDoing: Boolean = false, onUpdate: suspend () -> Unit, ) { - if (onlyOneInBlock.getAndSet(true)) { - if (!ignoreIfDoing) { - invalidatesAgain = true - } - return - } - scope.launch(dispatcher) { + mutex.withLock { + if (isProcessing) { + if (!ignoreIfDoing) { + invalidatesAgain = true + } + return@launch + } + isProcessing = true + } + try { onUpdate() delay(delay) @@ -88,8 +92,10 @@ class BasicBundledUpdate( } } finally { withContext(NonCancellable) { - invalidatesAgain = false - onlyOneInBlock.set(false) + mutex.withLock { + invalidatesAgain = false + isProcessing = false + } } } } @@ -127,37 +133,44 @@ class BasicBundledInsert( val dispatcher: CoroutineDispatcher = Dispatchers.IO, val scope: CoroutineScope, ) { - private var onlyOneInBlock = AtomicBoolean() - private var queue = LinkedBlockingQueue() + private val mutex = Mutex() + private var isProcessing = false + private val queue = mutableListOf() fun invalidateList( newObject: T, onUpdate: suspend (Set) -> Unit, ) { - queue.put(newObject) - - if (onlyOneInBlock.getAndSet(true)) { - // if it was true already, returns. - return - } - scope.launch(dispatcher) { - try { - while (true) { - val batch = mutableSetOf() - queue.drainTo(batch) - if (batch.isNotEmpty()) { - onUpdate(batch) - } else { - break + mutex.withLock { + queue.add(newObject) + + if (isProcessing) { + return@launch + } + isProcessing = true + } + + processLoop@ while (true) { + val batch = + mutex.withLock { + if (queue.isEmpty()) { + isProcessing = false + null + } else { + val items = queue.toSet() + queue.clear() + items + } } - delay(delay) - } - } finally { - withContext(NonCancellable) { - onlyOneInBlock.set(false) + if (batch == null) break@processLoop + + if (batch.isNotEmpty()) { + onUpdate(batch) } + + delay(delay) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/AdditiveFeedFilter.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/AdditiveFeedFilter.kt index 019296d92..2f70736e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/AdditiveFeedFilter.kt @@ -18,9 +18,9 @@ * 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.ui.dal +package com.vitorpamplona.amethyst.commons.ui.feeds -import com.vitorpamplona.amethyst.logTime +import com.vitorpamplona.amethyst.commons.utils.logTime abstract class AdditiveFeedFilter : FeedFilter(), @@ -30,7 +30,7 @@ abstract class AdditiveFeedFilter : newItems: Set, ): List = logTime( - debugMessage = { "${this.javaClass.simpleName} AdditiveFeedFilter updating ${newItems.size} new items to ${it.size} items" }, + debugMessage = { "${this::class.simpleName} AdditiveFeedFilter updating ${newItems.size} new items to ${it.size} items" }, ) { val newItemsToBeAdded = applyFilter(newItems) if (newItemsToBeAdded.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt index 283735f7a..ea6c118c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt @@ -18,19 +18,17 @@ * 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.ui.feeds +package com.vitorpamplona.amethyst.commons.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.BasicBundledInsert -import com.vitorpamplona.amethyst.service.BasicBundledUpdate -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter -import com.vitorpamplona.amethyst.ui.dal.IFeedFilter -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.service.BasicBundledInsert +import com.vitorpamplona.amethyst.commons.service.BasicBundledUpdate +import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread +import com.vitorpamplona.amethyst.commons.utils.equalImmutableLists import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.collections.immutable.ImmutableList @@ -45,6 +43,7 @@ import kotlinx.coroutines.launch class FeedContentState( val localFilter: IFeedFilter, val viewModelScope: CoroutineScope, + val cacheProvider: ICacheProvider, ) : InvalidatableContent { private val _feedContent = MutableStateFlow(FeedState.Loading) val feedContent = _feedContent.asStateFlow() @@ -152,7 +151,7 @@ class FeedContentState( .filter { val noteEvent = it.event if (noteEvent != null) { - !LocalCache.deletionIndex.hasBeenDeleted(noteEvent) + !cacheProvider.hasBeenDeleted(noteEvent) } else { false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedFilter.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedFilter.kt index 27b12cee1..3ffc9db4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedFilter.kt @@ -18,15 +18,15 @@ * 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.ui.dal +package com.vitorpamplona.amethyst.commons.ui.feeds -import com.vitorpamplona.amethyst.logTime +import com.vitorpamplona.amethyst.commons.utils.logTime abstract class FeedFilter : IFeedFilter { override fun loadTop(): List { val feed = logTime( - debugMessage = { "${this.javaClass.simpleName} FeedFilter returning ${it.size} objects" }, + debugMessage = { "${this::class.simpleName} FeedFilter returning ${it.size} objects" }, block = ::feed, ) return feed.take(limit()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedState.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedState.kt index c6e831f75..7fced7260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedState.kt @@ -18,11 +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.ui.feeds +package com.vitorpamplona.amethyst.commons.ui.feeds import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/IAdditiveFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/IAdditiveFeedFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/IAdditiveFeedFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/IAdditiveFeedFilter.kt index 189ce046a..ea607ddcb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/IAdditiveFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/IAdditiveFeedFilter.kt @@ -18,7 +18,7 @@ * 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.ui.dal +package com.vitorpamplona.amethyst.commons.ui.feeds interface IAdditiveFeedFilter : IFeedFilter { fun applyFilter(newItems: Set): Set diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/IFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/IFeedFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/IFeedFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/IFeedFilter.kt index 6c5dd8a5a..83d9a50c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/IFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/IFeedFilter.kt @@ -18,7 +18,7 @@ * 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.ui.dal +package com.vitorpamplona.amethyst.commons.ui.feeds interface IFeedFilter { fun loadTop(): List diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/InvalidatableContent.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/InvalidatableContent.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/InvalidatableContent.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/InvalidatableContent.kt index b4f4fab93..e9a59ecb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/InvalidatableContent.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/InvalidatableContent.kt @@ -18,7 +18,7 @@ * 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.ui.feeds +package com.vitorpamplona.amethyst.commons.ui.feeds import androidx.compose.runtime.State diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.kt new file mode 100644 index 000000000..1fcdab2c9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.kt @@ -0,0 +1,62 @@ +/** + * 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.utils + +import com.vitorpamplona.quartz.utils.Log +import kotlin.time.DurationUnit +import kotlin.time.measureTimedValue + +/** + * Platform-specific debug flag. + * Android: checks BuildConfig.DEBUG + * Desktop: can be configured via system property + */ +expect val isDebug: Boolean + +inline fun logTime( + debugMessage: String, + minToReportMs: Int = 1, + block: () -> T, +): T = + if (isDebug) { + val (result, elapsed) = measureTimedValue(block) + if (elapsed.inWholeMilliseconds > minToReportMs) { + Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage") + } + result + } else { + block() + } + +inline fun logTime( + debugMessage: (T) -> String, + minToReportMs: Int = 1, + block: () -> T, +): T = + if (isDebug) { + val (result, elapsed) = measureTimedValue(block) + if (elapsed.inWholeMilliseconds > minToReportMs) { + Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}") + } + result + } else { + block() + } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/ListUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/ListUtils.kt new file mode 100644 index 000000000..c5723901a --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/utils/ListUtils.kt @@ -0,0 +1,37 @@ +/** + * 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.utils + +import kotlinx.collections.immutable.ImmutableList + +fun equalImmutableLists( + list1: ImmutableList, + list2: ImmutableList, +): Boolean { + if (list1 === list2) return true + if (list1.size != list2.size) return false + for (i in 0 until list1.size) { + if (list1[i] !== list2[i]) { + return false + } + } + return true +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/FeedViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/FeedViewModel.kt new file mode 100644 index 000000000..c49eda6f7 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/FeedViewModel.kt @@ -0,0 +1,72 @@ +/** + * 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.viewmodels + +import androidx.compose.runtime.Stable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter +import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Stable +abstract class FeedViewModel( + localFilter: FeedFilter, + val cacheProvider: ICacheProvider, +) : ViewModel(), + InvalidatableContent { + val feedState = FeedContentState(localFilter, viewModelScope, cacheProvider) + + override val isRefreshing = feedState.isRefreshing + + fun sendToTop() = feedState.sendToTop() + + suspend fun sentToTop() = feedState.sentToTop() + + override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing) + + init { + Log.d("Init", "Starting new Model: ${this::class.simpleName}") + viewModelScope.launch(Dispatchers.IO) { + cacheProvider.getEventStream().newEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel::class.simpleName} with ${newNotes.size}") + feedState.updateFeedWith(newNotes) + } + } + + viewModelScope.launch(Dispatchers.IO) { + cacheProvider.getEventStream().deletedEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Delete from feeds: ${this@FeedViewModel::class.simpleName} with ${newNotes.size}") + feedState.deleteFromFeed(newNotes) + } + } + } + + override fun onCleared() { + Log.d("Init", "OnCleared: ${this::class.simpleName}") + super.onCleared() + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/LevelFeedViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/LevelFeedViewModel.kt new file mode 100644 index 000000000..556289659 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/LevelFeedViewModel.kt @@ -0,0 +1,97 @@ +/** + * 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.viewmodels.thread + +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.lazy.LazyListState +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.Note +import com.vitorpamplona.amethyst.commons.model.ThreadLevelCalculator +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState +import com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest + +abstract class LevelFeedViewModel( + localFilter: FeedFilter, + cacheProvider: ICacheProvider, +) : FeedViewModel(localFilter, cacheProvider) { + var llState: LazyListState by mutableStateOf(LazyListState(0, 0)) + + val hasDragged = mutableStateOf(false) + + val selectedIDHex = + llState.interactionSource.interactions + .onEach { + if (it is DragInteraction.Start) { + hasDragged.value = true + } + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + null, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val levelCacheFlow: StateFlow> = + feedState.feedContent + .transformLatest { feed -> + emitAll( + if (feed is FeedState.Loaded) { + feed.feed.map { + val cache = mutableMapOf() + it.list.forEach { + ThreadLevelCalculator.replyLevel(it, cache) + } + cache + } + } else { + MutableStateFlow(mapOf()) + }, + ) + }.flowOn(Dispatchers.IO) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + mapOf(), + ) + + fun levelFlowForItem(note: Note) = + levelCacheFlow + .map { + it[note] ?: 0 + }.distinctUntilChanged() +} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.jvm.kt new file mode 100644 index 000000000..59bef6ebe --- /dev/null +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/utils/DebugUtils.jvm.kt @@ -0,0 +1,23 @@ +/** + * 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.utils + +actual val isDebug: Boolean = System.getProperty("amethyst.debug", "false").toBoolean() From a6f49665a7bc95e3a0e0eaed9fa322915346c993 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 9 Jan 2026 07:14:52 +0200 Subject: [PATCH 04/47] feat: Extract drawReplyLevel modifier to commons Move the thread reply level visualization modifier to shared commons module for use by both Android and Desktop platforms. - Add ThreadModifiers.kt to commons/ui/thread/ - Update Android ThreadFeedView to use shared modifier - Add overload for non-State level parameter Co-Authored-By: Claude Opus 4.5 --- .../loggedIn/threadview/ThreadFeedView.kt | 32 +------ .../commons/ui/thread/ThreadModifiers.kt | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/thread/ThreadModifiers.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 7e497301d..de1fc1602 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -48,7 +48,6 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -56,8 +55,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.TextStyle @@ -75,6 +72,7 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState +import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus @@ -360,34 +358,6 @@ fun RenderThreadFeed( } } -// Creates a Zebra pattern where each bar is a reply level. -fun Modifier.drawReplyLevel( - level: State, - color: Color, - selected: Color, -): Modifier = - this - .drawBehind { - val paddingDp = 2 - val strokeWidthDp = 2 - val levelWidthDp = strokeWidthDp + 1 - - val padding = paddingDp.dp.toPx() - val strokeWidth = strokeWidthDp.dp.toPx() - val levelWidth = levelWidthDp.dp.toPx() - - repeat(level.value) { - this.drawLine( - if (it == level.value - 1) selected else color, - Offset(padding + it * levelWidth, 0f), - Offset(padding + it * levelWidth, size.height), - strokeWidth = strokeWidth, - ) - } - - return@drawBehind - }.padding(start = (2 + (level.value * 3)).dp) - @OptIn(ExperimentalFoundationApi::class) @Composable fun NoteMaster( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/thread/ThreadModifiers.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/thread/ThreadModifiers.kt new file mode 100644 index 000000000..c8779c0c7 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/thread/ThreadModifiers.kt @@ -0,0 +1,94 @@ +/** + * 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.ui.thread + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * Creates a zebra pattern where each bar represents a reply level in a thread. + * Used to visually indicate nesting depth in thread conversations. + * + * @param level The current nesting level (0 = root, 1 = first reply, etc.) + * @param color The color used for non-selected level bars + * @param selected The color used for the current/selected level bar + */ +fun Modifier.drawReplyLevel( + level: State, + color: Color, + selected: Color, +): Modifier = + this + .drawBehind { + val paddingDp = 2 + val strokeWidthDp = 2 + val levelWidthDp = strokeWidthDp + 1 + + val padding = paddingDp.dp.toPx() + val strokeWidth = strokeWidthDp.dp.toPx() + val levelWidth = levelWidthDp.dp.toPx() + + repeat(level.value) { + this.drawLine( + if (it == level.value - 1) selected else color, + Offset(padding + it * levelWidth, 0f), + Offset(padding + it * levelWidth, size.height), + strokeWidth = strokeWidth, + ) + } + + return@drawBehind + }.padding(start = (2 + (level.value * 3)).dp) + +/** + * Overload for non-State level value. + */ +fun Modifier.drawReplyLevel( + level: Int, + color: Color, + selected: Color, +): Modifier = + this + .drawBehind { + val paddingDp = 2 + val strokeWidthDp = 2 + val levelWidthDp = strokeWidthDp + 1 + + val padding = paddingDp.dp.toPx() + val strokeWidth = strokeWidthDp.dp.toPx() + val levelWidth = levelWidthDp.dp.toPx() + + repeat(level) { + this.drawLine( + if (it == level - 1) selected else color, + Offset(padding + it * levelWidth, 0f), + Offset(padding + it * levelWidth, size.height), + strokeWidth = strokeWidth, + ) + } + + return@drawBehind + }.padding(start = (2 + (level * 3)).dp) From 6aacb8e654917ebbfdbc7d81b99619dafac8c774 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 9 Jan 2026 14:40:30 +0200 Subject: [PATCH 05/47] move ThreadFilter to commons --- .../ThreadDualAxisChartAssemblerTest.kt | 4 +-- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../threadview/dal/ThreadFeedViewModel.kt | 3 +- .../amethyst/commons/model/IAccount.kt | 3 ++ .../viewmodels/thread}/ThreadFeedFilter.kt | 29 +++++++++++++------ 5 files changed, 28 insertions(+), 13 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread}/ThreadFeedFilter.kt (73%) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index dea5d5d39..347c5f035 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler -import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.verify @@ -174,7 +174,7 @@ class ThreadDualAxisChartAssemblerTest { null, ) - val filter = ThreadFeedFilter(account, naddr.toTag()) + val filter = ThreadFeedFilter(account, naddr.toTag(), LocalCache) val calculatedFeed = filter.feed() val expectedOrder = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 5b24119a6..e2171228c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1662,7 +1662,7 @@ class Account( fun isHidden(userHex: String): Boolean = hiddenUsers.flow.value.isUserHidden(userHex) - fun followingKeySet(): Set = kind3FollowList.flow.value.authors + override fun followingKeySet(): Set = kind3FollowList.flow.value.authors fun isAcceptable(user: User): Boolean { if (userProfile().pubkeyHex == user.pubkeyHex) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt index 5c408f2f1..52155155f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt @@ -22,13 +22,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache class ThreadFeedViewModel( account: Account, noteId: String, -) : com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel(ThreadFeedFilter(account, noteId), LocalCache) { +) : com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel(ThreadFeedFilter(account, noteId, LocalCache), LocalCache) { class Factory( val account: Account, val noteId: String, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt index 2b2a4510e..7460b925a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt @@ -81,4 +81,7 @@ interface IAccount { val hiddenWordsCase: List val hiddenUsersHashCodes: Set val spammersHashCodes: Set + + /** Set of followed user pubkeys (for feed ordering/highlighting) */ + fun followingKeySet(): Set } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt similarity index 73% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt index ec0fd2ed7..4f96bc0e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt @@ -18,35 +18,46 @@ * 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.ui.screen.loggedIn.threadview.dal +package com.vitorpamplona.amethyst.commons.viewmodels.thread import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.model.IAccount import com.vitorpamplona.amethyst.commons.model.LevelSignature +import com.vitorpamplona.amethyst.commons.model.Note 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.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.dal.FeedFilter +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableSet +/** + * Filter for assembling and sorting thread feeds. + * + * This filter uses ThreadAssembler to find all notes in a thread and + * ThreadLevelCalculator to sort them by reply level and relevance. + * + * @param account The current user's account (provides user profile and following set) + * @param noteId The root note ID of the thread to display + * @param cacheProvider The cache provider for accessing notes + */ @Immutable class ThreadFeedFilter( - val account: Account, + val account: IAccount, private val noteId: String, + private val cacheProvider: ICacheProvider, ) : FeedFilter() { override fun feedKey(): String = noteId override fun feed(): List { val cachedSignatures: MutableMap = mutableMapOf() - val followingKeySet = account.kind3FollowList.flow.value.authors - val eventsToWatch = ThreadAssembler(LocalCache).findThreadFor(noteId) ?: return emptyList() + val followingKeySet = account.followingKeySet() + val eventsToWatch = ThreadAssembler(cacheProvider).findThreadFor(noteId) ?: return emptyList() // Filter out drafts made by other accounts on device val filteredEvents = eventsToWatch.allNotes - .filter { !it.isDraft() || (it.author?.pubkeyHex == account.userProfile().pubkeyHex) } + .filter { !it.isDraft() || (it.author?.pubkeyHex == account.pubKey) } .toImmutableSet() val filteredThreadInfo = ThreadAssembler.ThreadInfo(eventsToWatch.root, filteredEvents) From 58b97e069506f20ac342cb5ff32b41a079c74e62 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 18:47:19 +0100 Subject: [PATCH 06/47] =?UTF-8?q?Voice=20reply=20to=20VoiceEvent/VoiceRepl?= =?UTF-8?q?yEvent=20(KIND=201222/1244)=20=E2=86=92=20Creates=20VoiceReplyE?= =?UTF-8?q?vent=20(KIND=201244)=20Voice=20reply=20to=20TextNoteEvent=20(KI?= =?UTF-8?q?ND=201)=20=E2=86=92=20Creates=20TextNoteEvent=20(KIND=201)=20wi?= =?UTF-8?q?th=20audio=20IMeta=20attachment=20and=20proper=20reply=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/loggedIn/AccountViewModel.kt | 49 +++++++++++++++---- .../loggedIn/home/ShortNotePostViewModel.kt | 43 +++++++++++++--- .../loggedIn/home/VoiceReplyViewModel.kt | 36 +++++++++++--- 3 files changed, 105 insertions(+), 23 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 9c983e475..085653a95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -107,6 +107,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -134,6 +137,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log @@ -1154,8 +1158,6 @@ class AccountViewModel( context: Context, ) { if (isWriteable()) { - val hint = note.toEventHint() ?: return - launchSigner { val uploader = UploadOrchestrator() val result = @@ -1171,14 +1173,41 @@ class AccountViewModel( ) if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - account.sendVoiceReplyMessage( - result.result.url, - result.result.fileHeader.mimeType ?: recording.mimeType, - result.result.fileHeader.hash, - recording.duration, - recording.amplitudes, - hint, - ) + val audioMeta = + AudioMeta( + url = result.result.url, + mimeType = result.result.fileHeader.mimeType ?: recording.mimeType, + hash = result.result.fileHeader.hash, + duration = recording.duration, + waveform = recording.amplitudes, + ) + + // Check if replying to a voice event + val voiceHint = note.toEventHint() + if (voiceHint != null) { + // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies + account.sendVoiceReplyMessage( + result.result.url, + result.result.fileHeader.mimeType ?: recording.mimeType, + result.result.fileHeader.hash, + recording.duration, + recording.amplitudes, + voiceHint, + ) + } else { + // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes + val template = + TextNoteEvent.build(audioMeta.url) { + val replyingTo = note.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } + account.signAndComputeBroadcast(template) + } } else if (result is UploadingState.Error) { toastManager.toast( R.string.failed_to_upload_media_no_details, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index cd479b134..8495a3083 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -542,18 +542,49 @@ open class ShortNotePostViewModel : voiceMetadata?.let { audioMeta -> // Only create voice reply if original note is also a voice message val originalVoiceHint = originalNote?.toEventHint() - return if (originalVoiceHint != null) { - // Create voice reply event - VoiceReplyEvent.build( + if (originalVoiceHint != null) { + // Create voice reply event (KIND 1244) + return VoiceReplyEvent.build( voiceMessage = audioMeta, replyingTo = originalVoiceHint, ) - } else { - // Create root voice event (no reply or original is not a voice message) - VoiceEvent.build( + } + // If no original note, create a standalone voice event (KIND 1222) + if (originalNote == null) { + return VoiceEvent.build( voiceMessage = audioMeta, ) } + // Otherwise, original note exists but is not a voice message + // Create a TextNoteEvent (KIND 1) with audio as IMeta attachment + return TextNoteEvent.build(audioMeta.url) { + val replyingTo = originalNote?.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + tags.forEach { + val note = accountViewModel.getNoteIfExists(it.eventId) + val ourAuthor = note?.author?.pubkeyHex + val ourHint = note?.relayHintUrl() + if (it.author == null || it.author?.isBlank() == true) { + it.author = ourAuthor + } else { + if (ourAuthor != null && it.author != ourAuthor) { + it.author = ourAuthor + } + } + if (it.relay == null) { + it.relay = ourHint + } else { + if (ourHint != null && it.relay != ourHint) { + it.relay = ourHint + } + } + } + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } } val tagger = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index faa5d677a..097aac737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -37,6 +37,10 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent @@ -200,12 +204,6 @@ class VoiceReplyViewModel : ViewModel() { is UploadingState.Finished -> { when (val orchestratorResult = result.result) { is UploadOrchestrator.OrchestratorResult.ServerResult -> { - val hint = note.toEventHint() - if (hint == null) { - accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) - return - } - val audioMeta = AudioMeta( url = orchestratorResult.url, @@ -215,7 +213,31 @@ class VoiceReplyViewModel : ViewModel() { waveform = recording.amplitudes, ) - accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, hint)) + // Check if replying to a voice event + val voiceHint = note.toEventHint() + if (voiceHint != null) { + // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies + accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, voiceHint)) + } else { + // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes + val textHint = note.toEventHint() + if (textHint == null) { + accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) + return + } + + val template = + TextNoteEvent.build(audioMeta.url) { + val replyingTo = note.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } + accountViewModel.account.signAndComputeBroadcast(template) + } if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) From d8093f64b10044d3199a1270099cad4442466672 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 10:27:12 +0100 Subject: [PATCH 07/47] Display kind 1 voice replies as audio waveform --- .../amethyst/ui/note/types/Text.kt | 7 ++ .../amethyst/ui/note/types/VoiceTrack.kt | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 8432deaa2..1a35de21e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -68,6 +68,13 @@ fun RenderTextEvent( ) { val noteEvent = note.event ?: return + // Check if this is an audio-only event (content is just an audio URL with waveform IMeta) + val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() } + if (isAudioOnly) { + RenderAudioFromIMeta(note, accountViewModel, nav) + return + } + val showReply by remember(note) { derivedStateOf { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 0a0210fb0..34939c9a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -69,8 +69,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.ui.theme.Size75Modifier import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip14Subject.subject +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @Composable @@ -263,3 +266,84 @@ fun PlayPauseButton(controllerState: MediaControllerState) { } } } + +/** + * Extracts AudioMeta from an event's IMeta tags if it has audio content with waveform. + * Returns the first audio IMeta that has a waveform, or null if none found. + */ +fun Event.getAudioMetaWithWaveform(): AudioMeta? { + val audioMetas = imetas().map { AudioMeta.parse(it) } + return audioMetas.firstOrNull { meta -> + meta.waveform != null && + meta.mimeType?.startsWith("audio/") == true + } +} + +/** + * Checks if the event content is primarily an audio attachment (content is just the audio URL). + */ +fun Event.isAudioOnlyContent(): Boolean { + val audioMeta = getAudioMetaWithWaveform() ?: return false + return content.trim() == audioMeta.url +} + +/** + * Renders audio with waveform for any event type that has audio IMeta attachment. + * This allows KIND 1 (TextNoteEvent) and KIND 1111 (CommentEvent) with voice + * attachments to display the same waveform UI as VoiceEvent. + */ +@Composable +fun RenderAudioFromIMeta( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event ?: return + + val audioMeta = remember(noteEvent) { noteEvent.getAudioMetaWithWaveform() } ?: return + + val waveform = + remember(audioMeta) { + audioMeta.waveform?.let { WaveformData(it) } + } + + val callbackUri = remember(note) { note.toNostrUri() } + + Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + GetMediaItem( + videoUri = audioMeta.url, + title = null, + artworkUri = null, + authorName = note.author?.toBestDisplayName(), + callbackUri = callbackUri, + mimeType = audioMeta.mimeType, + aspectRatio = null, + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(audioMeta.url), + keepPlaying = false, + waveformData = waveform, + ) { mediaItem -> + GetVideoController( + mediaItem = mediaItem, + muted = false, + ) { controller -> + RenderVoicePlayer( + mediaItem = mediaItem, + controllerState = controller, + waveform = waveform, + borderModifier = MaterialTheme.colorScheme.imageModifier, + accountViewModel = accountViewModel, + ) + } + } + } + + if (noteEvent.hasHashtags()) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) + } + } + } +} From 70c638ad9043f55be8ec393f46af3858766d8bc7 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 10:33:05 +0100 Subject: [PATCH 08/47] refactor to a shared composable to eliminate waveform code duplication --- .../amethyst/ui/note/types/VoiceTrack.kt | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 34939c9a8..632d6775b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -110,6 +110,32 @@ fun VoiceHeader( ?.let { WaveformData(it) } } + RenderAudioWithWaveform( + mediaUrl = media, + title = noteEvent.subject(), + mimeType = null, + waveform = waveform, + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +/** + * Shared composable for rendering audio with waveform visualization. + * Used by both VoiceHeader (for BaseVoiceEvent) and RenderAudioFromIMeta (for other events with audio IMeta). + */ +@Composable +fun RenderAudioWithWaveform( + mediaUrl: String, + title: String?, + mimeType: String?, + waveform: WaveformData?, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event ?: return val callbackUri = remember(note) { note.toNostrUri() } Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { @@ -117,14 +143,14 @@ fun VoiceHeader( verticalAlignment = Alignment.CenterVertically, ) { GetMediaItem( - videoUri = media, - title = noteEvent.subject(), + videoUri = mediaUrl, + title = title, artworkUri = null, authorName = note.author?.toBestDisplayName(), callbackUri = callbackUri, - mimeType = null, + mimeType = mimeType, aspectRatio = null, - proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media), + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(mediaUrl), keepPlaying = false, waveformData = waveform, ) { mediaItem -> @@ -171,7 +197,7 @@ fun RenderVoicePlayer( factory = { context: Context -> PlayerView(context).apply { player = controllerState.controller - // if we alrady know the size of the frame, this forces the player to stay in the size + // if we already know the size of the frame, this forces the player to stay in the size layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, @@ -307,43 +333,13 @@ fun RenderAudioFromIMeta( audioMeta.waveform?.let { WaveformData(it) } } - val callbackUri = remember(note) { note.toNostrUri() } - - Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - GetMediaItem( - videoUri = audioMeta.url, - title = null, - artworkUri = null, - authorName = note.author?.toBestDisplayName(), - callbackUri = callbackUri, - mimeType = audioMeta.mimeType, - aspectRatio = null, - proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(audioMeta.url), - keepPlaying = false, - waveformData = waveform, - ) { mediaItem -> - GetVideoController( - mediaItem = mediaItem, - muted = false, - ) { controller -> - RenderVoicePlayer( - mediaItem = mediaItem, - controllerState = controller, - waveform = waveform, - borderModifier = MaterialTheme.colorScheme.imageModifier, - accountViewModel = accountViewModel, - ) - } - } - } - - if (noteEvent.hasHashtags()) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) - } - } - } + RenderAudioWithWaveform( + mediaUrl = audioMeta.url, + title = null, + mimeType = audioMeta.mimeType, + waveform = waveform, + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) } From f51d0fb56e5741fffea45c7f7c48d27da5f8dd03 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 17:39:08 +0100 Subject: [PATCH 09/47] code review: added p-tag for voice reply to kind 1 allow waveform IMeta with a missing mimeType to render as audio --- .../vitorpamplona/amethyst/ui/note/types/Text.kt | 14 +++++++------- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 15 ++++++++------- .../loggedIn/home/ShortNotePostViewModel.kt | 12 ++++++++++++ .../screen/loggedIn/home/VoiceReplyViewModel.kt | 3 +++ 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 1a35de21e..76c68e29b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -68,13 +68,6 @@ fun RenderTextEvent( ) { val noteEvent = note.event ?: return - // Check if this is an audio-only event (content is just an audio URL with waveform IMeta) - val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() } - if (isAudioOnly) { - RenderAudioFromIMeta(note, accountViewModel, nav) - return - } - val showReply by remember(note) { derivedStateOf { @@ -107,6 +100,13 @@ fun RenderTextEvent( } } + // Check if this is an audio-only event (content is just an audio URL with waveform IMeta) + val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() } + if (isAudioOnly) { + RenderAudioFromIMeta(note, accountViewModel, nav) + return + } + LoadDecryptedContent( note, accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 632d6775b..1475f1e36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -301,7 +301,7 @@ fun Event.getAudioMetaWithWaveform(): AudioMeta? { val audioMetas = imetas().map { AudioMeta.parse(it) } return audioMetas.firstOrNull { meta -> meta.waveform != null && - meta.mimeType?.startsWith("audio/") == true + meta.mimeType?.startsWith("audio/") != false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 085653a95..a439463d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -105,10 +105,12 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser +import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.notify import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -394,7 +396,7 @@ class AccountViewModel( note.flow().author(), note.flow().metadata.stateFlow, note.flow().reports.stateFlow, - ) { hiddenUsers, followingUsers, autor, metadata, reports -> + ) { hiddenUsers, followingUsers, _, metadata, _ -> emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors)) }.onStart { emit( @@ -1015,11 +1017,11 @@ class AccountViewModel( LocalCache.findLatestModificationForNote(note) } - fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key) + fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key) - fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key) + fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key) - fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key) + fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel = LocalCache.getOrCreateEphemeralChannel(key) fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex) @@ -1202,6 +1204,7 @@ class AccountViewModel( if (replyingTo != null) { val tags = prepareETagsAsReplyTo(replyingTo, null) markedETags(tags) + notify(replyingTo.toPTag()) } // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) @@ -1451,7 +1454,7 @@ class AccountViewModel( // First check if we have an actual response from the DVM in LocalCache val response = LocalCache.notes.maxOrNullOf( - filter = { key, note -> + filter = { _, note -> val noteEvent = note.event noteEvent is NIP90ContentDiscoveryResponseEvent && noteEvent.pubKey == pubkeyHex && @@ -1560,8 +1563,6 @@ class AccountViewModel( } } - fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account) - fun convertAccounts(loggedInAccounts: List?): Set = loggedInAccounts ?.mapNotNull { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 8495a3083..8eec5fd70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -582,6 +582,18 @@ open class ShortNotePostViewModel : } markedETags(tags) } + pTags?.let { userList -> + val tags = + userList.map { + val tag = it.toPTag() + if (tag.relayHint == null) { + tag.copy(relayHint = LocalCache.relayHints.hintsForKey(it.pubkeyHex).firstOrNull()) + } else { + tag + } + } + notify(tags) + } // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 097aac737..a34e4f1cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -38,8 +38,10 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.notify import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @@ -232,6 +234,7 @@ class VoiceReplyViewModel : ViewModel() { if (replyingTo != null) { val tags = prepareETagsAsReplyTo(replyingTo, null) markedETags(tags) + notify(replyingTo.toPTag()) } // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) From 1e898d57b2a4d9ae6e22bd74d56c9adf56e1d7f2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 18:34:59 +0100 Subject: [PATCH 10/47] code review fixes: textHint variable was retrieved but never used extracted logic for fixing relay hints and author information in tags made mimeType check logic clearer --- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 112 +++++------------- .../loggedIn/home/ShortNotePostViewModel.kt | 22 +--- .../loggedIn/home/VoiceReplyViewModel.kt | 13 +- 4 files changed, 36 insertions(+), 113 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 1475f1e36..6f4d34a2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -301,7 +301,7 @@ fun Event.getAudioMetaWithWaveform(): AudioMeta? { val audioMetas = imetas().map { AudioMeta.parse(it) } return audioMetas.firstOrNull { meta -> meta.waveform != null && - meta.mimeType?.startsWith("audio/") != false + (meta.mimeType == null || meta.mimeType?.startsWith("audio/") == true) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index a439463d0..b24b67d58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.rememberCoroutineScope -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -67,12 +66,8 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.feeds.FeedState @@ -105,13 +100,9 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.tags.markedETags -import com.vitorpamplona.quartz.nip10Notes.tags.notify -import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -139,8 +130,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta -import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @@ -1004,6 +993,32 @@ class AccountViewModel( fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex) + /** + * Fixes author and relay hints in MarkedETag list by looking up notes from cache. + * This ensures reply tags have proper author pubkeys and relay hints for threading. + */ + fun fixReplyTagHints(tags: List) { + tags.forEach { tag -> + val note = getNoteIfExists(tag.eventId) + val cachedAuthor = note?.author?.pubkeyHex + val cachedRelay = note?.relayHintUrl() + + // Fix author if missing or different from cached + if (tag.author.isNullOrBlank() && cachedAuthor != null) { + tag.author = cachedAuthor + } else if (cachedAuthor != null && tag.author != cachedAuthor) { + tag.author = cachedAuthor + } + + // Fix relay hint if missing or different from cached + if (tag.relay == null && cachedRelay != null) { + tag.relay = cachedRelay + } else if (cachedRelay != null && tag.relay != cachedRelay) { + tag.relay = cachedRelay + } + } + } + override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address) fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) @@ -1154,79 +1169,6 @@ class AccountViewModel( super.onCleared() } - fun sendVoiceReply( - note: Note, - recording: RecordingResult, - context: Context, - ) { - if (isWriteable()) { - launchSigner { - val uploader = UploadOrchestrator() - val result = - uploader.upload( - uri = recording.file.toUri(), - mimeType = recording.mimeType, - alt = null, - contentWarningReason = null, - compressionQuality = CompressorQuality.UNCOMPRESSED, - server = account.settings.defaultFileServer, - account = account, - context = context, - ) - - if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - val audioMeta = - AudioMeta( - url = result.result.url, - mimeType = result.result.fileHeader.mimeType ?: recording.mimeType, - hash = result.result.fileHeader.hash, - duration = recording.duration, - waveform = recording.amplitudes, - ) - - // Check if replying to a voice event - val voiceHint = note.toEventHint() - if (voiceHint != null) { - // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies - account.sendVoiceReplyMessage( - result.result.url, - result.result.fileHeader.mimeType ?: recording.mimeType, - result.result.fileHeader.hash, - recording.duration, - recording.amplitudes, - voiceHint, - ) - } else { - // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes - val template = - TextNoteEvent.build(audioMeta.url) { - val replyingTo = note.toEventHint() - if (replyingTo != null) { - val tags = prepareETagsAsReplyTo(replyingTo, null) - markedETags(tags) - notify(replyingTo.toPTag()) - } - // Add audio as IMeta attachment - add(audioMeta.toIMetaArray()) - } - account.signAndComputeBroadcast(template) - } - } else if (result is UploadingState.Error) { - toastManager.toast( - R.string.failed_to_upload_media_no_details, - result.errorResource, - *result.params, - ) - } - } - } else { - toastManager.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_reply, - ) - } - } - fun loadThumb( context: Context, thumbUri: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 8eec5fd70..d810088ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -561,26 +562,9 @@ open class ShortNotePostViewModel : val replyingTo = originalNote?.toEventHint() if (replyingTo != null) { val tags = prepareETagsAsReplyTo(replyingTo, null) - tags.forEach { - val note = accountViewModel.getNoteIfExists(it.eventId) - val ourAuthor = note?.author?.pubkeyHex - val ourHint = note?.relayHintUrl() - if (it.author == null || it.author?.isBlank() == true) { - it.author = ourAuthor - } else { - if (ourAuthor != null && it.author != ourAuthor) { - it.author = ourAuthor - } - } - if (it.relay == null) { - it.relay = ourHint - } else { - if (ourHint != null && it.relay != ourHint) { - it.relay = ourHint - } - } - } + accountViewModel.fixReplyTagHints(tags) markedETags(tags) + notify(replyingTo.toPTag()) } pTags?.let { userList -> val tags = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index a34e4f1cf..3bd707f81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -37,7 +37,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags @@ -222,7 +221,7 @@ class VoiceReplyViewModel : ViewModel() { accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, voiceHint)) } else { // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes - val textHint = note.toEventHint() + val textHint = note.toEventHint() if (textHint == null) { accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) return @@ -230,12 +229,10 @@ class VoiceReplyViewModel : ViewModel() { val template = TextNoteEvent.build(audioMeta.url) { - val replyingTo = note.toEventHint() - if (replyingTo != null) { - val tags = prepareETagsAsReplyTo(replyingTo, null) - markedETags(tags) - notify(replyingTo.toPTag()) - } + val tags = prepareETagsAsReplyTo(textHint, null) + accountViewModel.fixReplyTagHints(tags) + markedETags(tags) + notify(textHint.toPTag()) // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) } From 45207857c68c802cc93a42231c34fbc6c2f66384 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 12 Jan 2026 06:23:57 +0200 Subject: [PATCH 11/47] finish ThreadScreen --- .../commons/subscriptions/FeedSubscription.kt | 45 +++ .../vitorpamplona/amethyst/desktop/Main.kt | 21 ++ .../amethyst/desktop/ui/FeedScreen.kt | 11 +- .../amethyst/desktop/ui/ThreadScreen.kt | 277 ++++++++++++++++++ 4 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt index abe3dc240..60852ec49 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt @@ -86,3 +86,48 @@ fun createContactListSubscription( onEvent = onEvent, onEose = onEose, ) + +/** + * Creates a subscription config for fetching a specific note by ID. + */ +fun createNoteSubscription( + relays: Set, + noteId: String, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("note-${noteId.take(8)}"), + filters = listOf(FilterBuilders.byIds(listOf(noteId))), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) + +/** + * Creates a subscription config for fetching all replies to a note (thread). + * + * @param noteId The root note ID to fetch replies for + * @param limit Maximum number of reply events to request + */ +fun createThreadRepliesSubscription( + relays: Set, + noteId: String, + limit: Int = 200, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("thread-${noteId.take(8)}"), + filters = + listOf( + FilterBuilders.byETags( + eventIds = listOf(noteId), + kinds = listOf(1), // TextNoteEvent + limit = limit, + ), + ), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index eaf54d7d5..86b01d92d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog import com.vitorpamplona.amethyst.desktop.ui.FeedScreen import com.vitorpamplona.amethyst.desktop.ui.LoginScreen import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen +import com.vitorpamplona.amethyst.desktop.ui.ThreadScreen import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -110,6 +111,10 @@ sealed class DesktopScreen { val pubKeyHex: String, ) : DesktopScreen() + data class Thread( + val noteId: String, + ) : DesktopScreen() + object Settings : DesktopScreen() } @@ -351,6 +356,9 @@ fun MainContent( onNavigateToProfile = { pubKeyHex -> onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) }, + onNavigateToThread = { noteId -> + onScreenChange(DesktopScreen.Thread(noteId)) + }, ) DesktopScreen.Search -> SearchPlaceholder() DesktopScreen.Messages -> MessagesPlaceholder() @@ -377,6 +385,19 @@ fun MainContent( onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) }, ) + is DesktopScreen.Thread -> + ThreadScreen( + noteId = currentScreen.noteId, + relayManager = relayManager, + account = account, + onBack = { onScreenChange(DesktopScreen.Feed) }, + onNavigateToProfile = { pubKeyHex -> + onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) + }, + onNavigateToThread = { noteId -> + onScreenChange(DesktopScreen.Thread(noteId)) + }, + ) DesktopScreen.Settings -> RelaySettingsScreen(relayManager, account) } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 0636f2041..00cef6714 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.desktop.ui +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -75,8 +76,14 @@ fun FeedNoteCard( account: AccountState.LoggedIn?, onReply: () -> Unit, onNavigateToProfile: (String) -> Unit = {}, + onNavigateToThread: (String) -> Unit = {}, ) { - Column { + Column( + modifier = + Modifier.clickable { + onNavigateToThread(event.id) + }, + ) { NoteCard( note = event.toNoteDisplayData(), onAuthorClick = onNavigateToProfile, @@ -101,6 +108,7 @@ fun FeedScreen( account: AccountState.LoggedIn? = null, onCompose: () -> Unit = {}, onNavigateToProfile: (String) -> Unit = {}, + onNavigateToThread: (String) -> Unit = {}, ) { val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState() @@ -274,6 +282,7 @@ fun FeedScreen( account = account, onReply = { replyToEvent = event }, onNavigateToProfile = onNavigateToProfile, + onNavigateToThread = onNavigateToThread, ) } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt new file mode 100644 index 000000000..0c469f085 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -0,0 +1,277 @@ +/** + * 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.desktop.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.state.EventCollectionState +import com.vitorpamplona.amethyst.commons.subscriptions.createNoteSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.createThreadRepliesSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription +import com.vitorpamplona.amethyst.commons.ui.components.LoadingState +import com.vitorpamplona.amethyst.commons.ui.note.NoteCard +import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel +import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.quartz.nip01Core.core.Event + +/** + * Desktop Thread Screen - displays a note and all its replies in a thread view. + * + * Uses the shared drawReplyLevel modifier from commons to display reply nesting. + */ +@Composable +fun ThreadScreen( + noteId: String, + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn?, + onBack: () -> Unit, + onNavigateToProfile: (String) -> Unit = {}, + onNavigateToThread: (String) -> Unit = {}, +) { + val connectedRelays by relayManager.connectedRelays.collectAsState() + val relayStatuses by relayManager.relayStatuses.collectAsState() + val scope = rememberCoroutineScope() + + // State for the root note + var rootNote by remember { mutableStateOf(null) } + + // State for reply events + val replyEventState = + remember(noteId) { + EventCollectionState( + getId = { it.id }, + sortComparator = compareBy { it.createdAt }, + maxSize = 500, + scope = scope, + ) + } + val replyEvents by replyEventState.items.collectAsState() + + // Cache for calculating reply levels + val levelCache = remember(noteId) { mutableMapOf() } + + // Subscribe to the root note + rememberSubscription(relayStatuses, noteId, relayManager = relayManager) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty()) { + createNoteSubscription( + relays = configuredRelays, + noteId = noteId, + onEvent = { event, _, _, _ -> + if (event.id == noteId) { + rootNote = event + levelCache[event.id] = 0 + } + }, + ) + } else { + null + } + } + + // Subscribe to replies + rememberSubscription(relayStatuses, noteId, relayManager = relayManager) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty()) { + createThreadRepliesSubscription( + relays = configuredRelays, + noteId = noteId, + onEvent = { event, _, _, _ -> + replyEventState.addItem(event) + }, + ) + } else { + null + } + } + + // Calculate reply level for an event based on e-tags + fun calculateLevel(event: Event): Int { + levelCache[event.id]?.let { return it } + + // Find the event this is replying to (last e-tag or marked reply/root) + val replyToId = findReplyToId(event) + val level = + if (replyToId == null || replyToId == noteId) { + 1 // Direct reply to root + } else { + (levelCache[replyToId] ?: 0) + 1 + } + levelCache[event.id] = level + return level + } + + Column(modifier = Modifier.fillMaxSize()) { + // Header with back button + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.size(24.dp), + ) + } + Spacer(Modifier.width(8.dp)) + Text( + "Thread", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + ) + } + + if (connectedRelays.isEmpty()) { + LoadingState("Connecting to relays...") + } else if (rootNote == null) { + LoadingState("Loading thread...") + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(0.dp), + ) { + // Root note (no reply level indicator) + item(key = noteId) { + Column( + modifier = + Modifier.clickable { + // Already viewing this thread, no-op + }, + ) { + NoteCard( + note = rootNote!!.toNoteDisplayData(), + onAuthorClick = onNavigateToProfile, + ) + if (account != null) { + NoteActionsRow( + event = rootNote!!, + relayManager = relayManager, + account = account, + onReplyClick = { /* TODO: Open reply dialog */ }, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + ) + } + } + HorizontalDivider(thickness = 1.dp) + } + + // Reply notes with level indicators + items(replyEvents, key = { it.id }) { event -> + val level = calculateLevel(event) + + Column( + modifier = + Modifier + .drawReplyLevel( + level = level, + color = MaterialTheme.colorScheme.outlineVariant, + selected = + if (event.id == noteId) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outlineVariant + }, + ).clickable { + onNavigateToThread(event.id) + }, + ) { + NoteCard( + note = event.toNoteDisplayData(), + onAuthorClick = onNavigateToProfile, + ) + if (account != null) { + NoteActionsRow( + event = event, + relayManager = relayManager, + account = account, + onReplyClick = { /* TODO: Open reply dialog */ }, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + ) + } + } + HorizontalDivider(thickness = 1.dp) + } + + // Empty state for no replies + if (replyEvents.isEmpty()) { + item { + Spacer(Modifier.height(32.dp)) + Text( + "No replies yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) + } + } + } + } + } +} + +/** + * Finds the event ID this event is replying to. + * Uses NIP-10 markers (reply/root) or falls back to last e-tag. + */ +private fun findReplyToId(event: Event): String? { + val eTags = event.tags.filter { it.size >= 2 && it[0] == "e" } + if (eTags.isEmpty()) return null + + // Check for NIP-10 marked tags first + val replyTag = eTags.find { it.size >= 4 && it[3] == "reply" } + if (replyTag != null) return replyTag[1] + + val rootTag = eTags.find { it.size >= 4 && it[3] == "root" } + if (rootTag != null && eTags.size == 1) return rootTag[1] + + // Fall back to positional (last e-tag is the reply-to) + return eTags.lastOrNull()?.get(1) +} From e561492705bcd347b98046862adf8b9e765641da Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Jan 2026 15:28:33 -0500 Subject: [PATCH 12/47] Removes unused aliases --- amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index d203c59e5..132811377 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -24,6 +24,4 @@ package com.vitorpamplona.amethyst.model typealias Note = com.vitorpamplona.amethyst.commons.model.Note typealias NotesGatherer = com.vitorpamplona.amethyst.commons.model.NotesGatherer typealias AddressableNote = com.vitorpamplona.amethyst.commons.model.AddressableNote -typealias NoteFlowSet = com.vitorpamplona.amethyst.commons.model.NoteFlowSet -typealias NoteBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.NoteBundledRefresherFlow typealias NoteState = com.vitorpamplona.amethyst.commons.model.NoteState From e9210872bdd84ddb0d06ba717a7cfb72aecb78b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Jan 2026 15:29:33 -0500 Subject: [PATCH 13/47] Improves performance by precaching signatures before sorting them Fixes out of order crash when the same signature came back. --- .../commons/viewmodels/thread/ThreadFeedFilter.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt index 4f96bc0e6..2d9200179 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt @@ -64,9 +64,8 @@ class ThreadFeedFilter( val eventsInHex = filteredThreadInfo.allNotes.map { it.idHex }.toSet() val now = TimeUtils.now() - // Currently orders by date of each event, descending, at each level of the reply stack - val order = - compareByDescending { + val signatures = + filteredThreadInfo.allNotes.associateWith { ThreadLevelCalculator .replyLevelSignature( it, @@ -78,6 +77,11 @@ class ThreadFeedFilter( ).signature } + // Currently orders by date of each event, descending, at each level of the reply stack + val order = + compareByDescending { signatures[it] } + .thenBy { it.idHex } + return filteredThreadInfo.allNotes.sortedWith(order) } } From c585b058793972d6f5bb250113339f6cfc753d01 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 12 Jan 2026 20:32:01 +0000 Subject: [PATCH 14/47] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 9605e4c53..275889023 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -30,7 +30,7 @@ Kopírovat text Kopírovat ID autora Kopírovat ID poznámky - Vysílání + Rozšířit na relé Časové razítko Časové razítko: Čeká na potvrzení OTS: Čeká @@ -114,7 +114,7 @@ "O nás…" Co vás napadá? Napište zprávu… - Příspěvek + Publikovat Uložit Vytvořit Přejmenovat @@ -741,7 +741,7 @@ Pouze Wi-Fi Neomezená WiFi Nikdy - Hotovo + Kompletní Zjednodušené Výkonost Klasický From bfd0fc01b94f400d91dfb45386b7ec665a280b9c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 19:08:47 +0100 Subject: [PATCH 15/47] add TarsosDSP dependency for voice anonymization --- amethyst/build.gradle | 3 +++ gradle/libs.versions.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index ee42fb543..2df5a4149 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -348,6 +348,9 @@ dependencies { // Image compression lib implementation libs.zelory.image.compressor + // Voice anonymization DSP + implementation libs.tarsosdsp + // Cbor for cashuB format implementation libs.kotlinx.serialization.cbor diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f1c53630..8f26e8b51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.22.0" securityCryptoKtx = "1.1.0" spotless = "8.1.0" +tarsosdsp = "2.5" torAndroid = "0.4.8.21.1" translate = "17.0.3" unifiedpush = "3.0.10" @@ -145,6 +146,7 @@ rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } +tarsosdsp = { group = "be.tarsos.dsp", name = "core", version.ref = "tarsosdsp" } tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" } From 83d4d3e756befeedbfe5db17ee416f47bcfdd810 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 11:44:40 +0100 Subject: [PATCH 16/47] add VoicePreset enum for voice anonymization Define preset options (None, Deep, High, Neutral) --- .../ui/actions/uploads/VoicePreset.kt | 35 +++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 4 +++ settings.gradle | 1 + 3 files changed, 40 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt new file mode 100644 index 000000000..1d16a3c8e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -0,0 +1,35 @@ +/** + * 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.ui.actions.uploads + +import androidx.annotation.StringRes +import com.vitorpamplona.amethyst.R + +enum class VoicePreset( + val pitchFactor: Double, + val formantShift: Double, + @StringRes val labelRes: Int, +) { + NONE(1.0, 0.0, R.string.voice_preset_none), + DEEP(0.75, -3.0, R.string.voice_preset_deep), + HIGH(1.4, 4.0, R.string.voice_preset_high), + NEUTRAL(1.0, -2.0, R.string.voice_preset_neutral), +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e9b34537c..ebec66ae8 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -184,6 +184,10 @@ Unexpected upload state NIP-95 is not supported for voice messages yet Voice upload failed: %1$s + None + Deep + High + Neutral User does not have a lightning address set up to receive sats "reply here.. " Copies the Note ID to the clipboard for sharing in Nostr diff --git a/settings.gradle b/settings.gradle index cad6e3772..5603e440e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,7 @@ dependencyResolutionManagement { mavenCentral() maven { url = "https://jitpack.io" } maven { url = "https://raw.githubusercontent.com/guardianproject/gpmaven/master" } + maven { url = "https://mvn.0110.be/releases" } } } From 8de148e56248b96b80fa9c86ce8aacfa6f1df1ce Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 11:59:53 +0100 Subject: [PATCH 17/47] add VoiceAnonymizer for audio pitch/formant shifting --- .../ui/actions/uploads/VoiceAnonymizer.kt | 435 ++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt new file mode 100644 index 000000000..08d15c859 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -0,0 +1,435 @@ +/** + * 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.ui.actions.uploads + +import android.media.MediaCodec +import android.media.MediaCodecInfo +import android.media.MediaExtractor +import android.media.MediaFormat +import android.media.MediaMuxer +import android.util.Log +import be.tarsos.dsp.AudioDispatcher +import be.tarsos.dsp.AudioEvent +import be.tarsos.dsp.AudioProcessor +import be.tarsos.dsp.WaveformSimilarityBasedOverlapAdd +import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter +import be.tarsos.dsp.io.TarsosDSPAudioFormat +import be.tarsos.dsp.resample.RateTransposer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import java.io.File +import java.nio.ByteOrder +import kotlin.coroutines.coroutineContext +import kotlin.math.abs + +data class AnonymizedResult( + val file: File, + val waveform: List, + val duration: Int, +) + +class VoiceAnonymizer { + companion object { + private const val TAG = "VoiceAnonymizer" + private const val SAMPLE_RATE = 44100 + private const val CHANNELS = 1 + private const val BIT_RATE = 128000 + } + + suspend fun anonymize( + inputFile: File, + preset: VoicePreset, + onProgress: (Float) -> Unit = {}, + ): Result = + withContext(Dispatchers.IO) { + if (preset == VoicePreset.NONE) { + return@withContext Result.failure( + IllegalArgumentException("Cannot anonymize with NONE preset"), + ) + } + + try { + val outputFile = createOutputFile(inputFile, preset) + val (pcmData, sampleRate, duration) = + decodeAudioToPcm(inputFile) { progress -> + onProgress(progress * 0.3f) + } + + val processedPcm = + processPcmWithTarsos(pcmData, preset, sampleRate) { progress -> + onProgress(0.3f + progress * 0.4f) + } + + val waveform = extractWaveform(processedPcm, sampleRate) + + encodePcmToAac(processedPcm, sampleRate, outputFile) { progress -> + onProgress(0.7f + progress * 0.3f) + } + + onProgress(1f) + Result.success(AnonymizedResult(outputFile, waveform, duration)) + } catch (e: Exception) { + Log.e(TAG, "Failed to anonymize audio", e) + Result.failure(e) + } + } + + private fun createOutputFile( + inputFile: File, + preset: VoicePreset, + ): File { + val baseName = inputFile.nameWithoutExtension + val presetSuffix = preset.name.lowercase() + return File(inputFile.parentFile, "${baseName}_$presetSuffix.mp4") + } + + private data class DecodedAudio( + val pcmData: FloatArray, + val sampleRate: Int, + val duration: Int, + ) + + private suspend fun decodeAudioToPcm( + inputFile: File, + onProgress: (Float) -> Unit, + ): DecodedAudio { + val extractor = MediaExtractor() + extractor.setDataSource(inputFile.absolutePath) + + var audioTrackIndex = -1 + var format: MediaFormat? = null + for (i in 0 until extractor.trackCount) { + val trackFormat = extractor.getTrackFormat(i) + val mime = trackFormat.getString(MediaFormat.KEY_MIME) + if (mime?.startsWith("audio/") == true) { + audioTrackIndex = i + format = trackFormat + break + } + } + + if (audioTrackIndex == -1 || format == null) { + extractor.release() + throw IllegalStateException("No audio track found in file") + } + + extractor.selectTrack(audioTrackIndex) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" + val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + val durationUs = format.getLong(MediaFormat.KEY_DURATION) + val duration = (durationUs / 1_000_000).toInt() + + val decoder = MediaCodec.createDecoderByType(mime) + decoder.configure(format, null, null, 0) + decoder.start() + + val pcmSamples = mutableListOf() + val bufferInfo = MediaCodec.BufferInfo() + var inputDone = false + var outputDone = false + + while (!outputDone && coroutineContext.isActive) { + if (!inputDone) { + val inputBufferIndex = decoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!! + val sampleSize = extractor.readSampleData(inputBuffer, 0) + if (sampleSize < 0) { + decoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + val presentationTimeUs = extractor.sampleTime + decoder.queueInputBuffer( + inputBufferIndex, + 0, + sampleSize, + presentationTimeUs, + 0, + ) + extractor.advance() + if (durationUs > 0) { + onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f)) + } + } + } + } + + val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000) + if (outputBufferIndex >= 0) { + val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!! + val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer() + while (shortBuffer.hasRemaining()) { + pcmSamples.add(shortBuffer.get() / 32768f) + } + decoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true + } + } + } + + decoder.stop() + decoder.release() + extractor.release() + + return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) + } + + private fun processPcmWithTarsos( + pcmData: FloatArray, + preset: VoicePreset, + sampleRate: Int, + onProgress: (Float) -> Unit, + ): FloatArray { + val factor = preset.pitchFactor + val processedSamples = mutableListOf() + val totalSamples = pcmData.size + + val wsola = + WaveformSimilarityBasedOverlapAdd( + WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults( + factor, + sampleRate.toDouble(), + ), + ) + val rateTransposer = RateTransposer(factor) + + val bufferSize = wsola.inputBufferSize + val overlap = wsola.overlap + + val tarsosDspFormat = + TarsosDSPAudioFormat( + sampleRate.toFloat(), + 16, + 1, + true, + false, + ) + + val collector = + object : AudioProcessor { + override fun process(audioEvent: AudioEvent): Boolean { + val buffer = audioEvent.floatBuffer + for (i in 0 until audioEvent.bufferSize) { + processedSamples.add(buffer[i]) + } + return true + } + + override fun processingFinished() {} + } + + val dispatcher = + AudioDispatcher( + FloatArrayAudioInputStream(pcmData, tarsosDspFormat, pcmData.size.toLong()), + bufferSize, + overlap, + ) + + wsola.setDispatcher(dispatcher) + dispatcher.addAudioProcessor(wsola) + dispatcher.addAudioProcessor(rateTransposer) + dispatcher.addAudioProcessor(collector) + + var samplesProcessed = 0 + val progressProcessor = + object : AudioProcessor { + override fun process(audioEvent: AudioEvent): Boolean { + samplesProcessed += audioEvent.bufferSize + onProgress((samplesProcessed.toFloat() / totalSamples).coerceIn(0f, 1f)) + return true + } + + override fun processingFinished() {} + } + dispatcher.addAudioProcessor(progressProcessor) + + dispatcher.run() + + return processedSamples.toFloatArray() + } + + private fun extractWaveform( + pcmData: FloatArray, + sampleRate: Int, + ): List { + val waveform = mutableListOf() + var offset = 0 + + while (offset < pcmData.size) { + val end = minOf(offset + sampleRate, pcmData.size) + var maxAmplitude = 0f + for (i in offset until end) { + val amplitude = abs(pcmData[i]) + if (amplitude > maxAmplitude) { + maxAmplitude = amplitude + } + } + waveform.add(maxAmplitude * 32768f) + offset += sampleRate + } + + return waveform + } + + private fun encodePcmToAac( + pcmData: FloatArray, + sampleRate: Int, + outputFile: File, + onProgress: (Float) -> Unit, + ) { + val format = + MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, CHANNELS) + format.setInteger( + MediaFormat.KEY_AAC_PROFILE, + MediaCodecInfo.CodecProfileLevel.AACObjectLC, + ) + format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE) + + val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC) + encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) + encoder.start() + + val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) + var audioTrackIndex = -1 + var muxerStarted = false + + val bufferInfo = MediaCodec.BufferInfo() + var inputOffset = 0 + var inputDone = false + var outputDone = false + val totalSamples = pcmData.size + + while (!outputDone) { + if (!inputDone) { + val inputBufferIndex = encoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!! + inputBuffer.clear() + + val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset) + if (samplesToWrite <= 0) { + encoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + for (i in 0 until samplesToWrite) { + val sample = + (pcmData[inputOffset + i] * 32767) + .toInt() + .coerceIn(-32768, 32767) + .toShort() + inputBuffer.putShort(sample) + } + val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate + encoder.queueInputBuffer( + inputBufferIndex, + 0, + inputBuffer.position(), + presentationTimeUs, + 0, + ) + inputOffset += samplesToWrite + onProgress(inputOffset.toFloat() / totalSamples) + } + } + } + + val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000) + when { + outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { + audioTrackIndex = muxer.addTrack(encoder.outputFormat) + muxer.start() + muxerStarted = true + } + + outputBufferIndex >= 0 -> { + val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!! + if (muxerStarted && bufferInfo.size > 0) { + outputBuffer.position(bufferInfo.offset) + outputBuffer.limit(bufferInfo.offset + bufferInfo.size) + muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo) + } + encoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true + } + } + } + } + + encoder.stop() + encoder.release() + muxer.stop() + muxer.release() + } +} + +private class FloatArrayAudioInputStream( + private val floatArray: FloatArray, + private val format: TarsosDSPAudioFormat, + private val frameLength: Long, +) : be.tarsos.dsp.io.TarsosDSPAudioInputStream { + private var position = 0 + + override fun getFormat(): TarsosDSPAudioFormat = format + + override fun getFrameLength(): Long = frameLength + + override fun read( + buffer: ByteArray, + offset: Int, + length: Int, + ): Int { + val converter = TarsosDSPAudioFloatConverter.getConverter(format) + val floatBuffer = FloatArray(length / 2) + val samplesToRead = minOf(floatBuffer.size, floatArray.size - position) + + if (samplesToRead <= 0) return -1 + + System.arraycopy(floatArray, position, floatBuffer, 0, samplesToRead) + position += samplesToRead + + converter.toByteArray(floatBuffer, samplesToRead, buffer, offset) + return samplesToRead * 2 + } + + override fun skip(bytesToSkip: Long): Long { + val samplesToSkip = (bytesToSkip / 2).toInt() + val actualSkip = minOf(samplesToSkip, floatArray.size - position) + position += actualSkip + return actualSkip.toLong() * 2 + } + + override fun close() {} +} From 8182268a00585228a7fa8edf77f82ca5650492ee Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 12:02:31 +0100 Subject: [PATCH 18/47] add voice anonymization state to VoiceReplyViewModel --- .../loggedIn/home/VoiceReplyViewModel.kt | 92 ++++++++++++++++++- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 3bd707f81..799fbf0a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -34,7 +34,10 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag @@ -67,6 +70,27 @@ class VoiceReplyViewModel : ViewModel() { var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null) var isUploading: Boolean by mutableStateOf(false) + var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) + var isProcessingPreset: Boolean by mutableStateOf(false) + var distortedFiles: Map by mutableStateOf(emptyMap()) + private var processingJob: Job? = null + + val activeFile: File? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceLocalFile + } else { + distortedFiles[selectedPreset]?.file + } + + val activeWaveform: List? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceRecording?.amplitudes + } else { + distortedFiles[selectedPreset]?.waveform + } + private var uploadJob: Job? = null fun init(accountVM: AccountViewModel) { @@ -113,10 +137,12 @@ class VoiceReplyViewModel : ViewModel() { fun selectRecording(recording: RecordingResult) { cancelUpload() + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file voiceMetadata = null + selectedPreset = VoicePreset.NONE } private fun cancelUpload() { @@ -135,13 +161,67 @@ class VoiceReplyViewModel : ViewModel() { Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } + + distortedFiles.values.forEach { result -> + try { + if (result.file.exists()) { + result.file.delete() + Log.d("VoiceReplyViewModel", "Deleted distorted file: ${result.file.absolutePath}") + } + } catch (e: Exception) { + Log.w("VoiceReplyViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) + } + } + distortedFiles = emptyMap() } - fun canSend(): Boolean = voiceRecording != null && !isUploading + fun canSend(): Boolean = voiceRecording != null && !isUploading && !isProcessingPreset + + fun selectPreset(preset: VoicePreset) { + if (isProcessingPreset || preset == selectedPreset) return + + if (preset == VoicePreset.NONE) { + selectedPreset = preset + return + } + + if (distortedFiles.containsKey(preset)) { + selectedPreset = preset + return + } + + val originalFile = voiceLocalFile ?: return + + processingJob?.cancel() + processingJob = + viewModelScope.launch { + isProcessingPreset = true + try { + val anonymizer = VoiceAnonymizer() + val result = anonymizer.anonymize(originalFile, preset) + + result + .onSuccess { anonymizedResult -> + distortedFiles = distortedFiles + (preset to anonymizedResult) + selectedPreset = preset + }.onFailure { error -> + Log.w("VoiceReplyViewModel", "Failed to anonymize voice", error) + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + } + } finally { + isProcessingPreset = false + processingJob = null + } + } + } fun sendVoiceReply(onSuccess: () -> Unit) { val note = replyToNote ?: return val recording = voiceRecording ?: return + val fileToUpload = activeFile ?: recording.file val serverToUse = voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer cancelUpload() @@ -154,7 +234,7 @@ class VoiceReplyViewModel : ViewModel() { try { val result = withContext(Dispatchers.IO) { - val uri = android.net.Uri.fromFile(recording.file) + val uri = android.net.Uri.fromFile(fileToUpload) orchestrator.upload( uri = uri, mimeType = recording.mimeType, @@ -168,7 +248,7 @@ class VoiceReplyViewModel : ViewModel() { ) } - handleUploadResult(note, recording, serverToUse, result, onSuccess) + handleUploadResult(note, recording, activeWaveform ?: recording.amplitudes, serverToUse, result, onSuccess) } catch (e: CancellationException) { Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e) } catch (e: Exception) { @@ -192,6 +272,7 @@ class VoiceReplyViewModel : ViewModel() { private suspend fun handleUploadResult( note: Note, recording: RecordingResult, + waveform: List, server: ServerName, result: UploadingState, onSuccess: () -> Unit, @@ -211,7 +292,7 @@ class VoiceReplyViewModel : ViewModel() { mimeType = recording.mimeType, hash = orchestratorResult.fileHeader.hash, duration = recording.duration, - waveform = recording.amplitudes, + waveform = waveform, ) // Check if replying to a voice event @@ -266,6 +347,7 @@ class VoiceReplyViewModel : ViewModel() { fun cancel() { cancelUpload() + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -273,6 +355,8 @@ class VoiceReplyViewModel : ViewModel() { voiceSelectedServer = null isUploading = false voiceOrchestrator = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false } override fun onCleared() { From 63d7ba07b95353c6fb23676fd99062e22a0a5e55 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 15:11:33 +0100 Subject: [PATCH 19/47] add voice preset selector UI to VoiceReplyScreen --- .../ui/actions/uploads/VoicePresetSelector.kt | 80 +++++++++++++++++++ .../screen/loggedIn/home/VoiceReplyScreen.kt | 17 +++- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt new file mode 100644 index 000000000..aae4818af --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt @@ -0,0 +1,80 @@ +/** + * 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.ui.actions.uploads + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun VoicePresetSelector( + selectedPreset: VoicePreset, + isProcessing: Boolean, + onPresetSelected: (VoicePreset) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + ) { + VoicePreset.entries.forEach { preset -> + val isSelected = preset == selectedPreset + val isThisProcessing = isProcessing && preset == selectedPreset + val isEnabled = !isProcessing || preset == VoicePreset.NONE + + FilterChip( + selected = isSelected, + onClick = { if (isEnabled) onPresetSelected(preset) }, + enabled = isEnabled, + label = { + if (isThisProcessing) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Text(stringRes(context, preset.labelRes)) + } + }, + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedLabelColor = MaterialTheme.colorScheme.onPrimary, + ), + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 15f30bb19..3794060bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar @@ -153,15 +154,27 @@ private fun VoiceReplyScreenBody( UploadProgressIndicator(orchestrator) } ?: run { viewModel.getVoicePreviewMetadata()?.let { metadata -> + val displayMetadata = + metadata.copy( + waveform = viewModel.activeWaveform ?: metadata.waveform, + ) VoiceMessagePreview( - voiceMetadata = metadata, - localFile = viewModel.voiceLocalFile, + voiceMetadata = displayMetadata, + localFile = viewModel.activeFile, onRemove = { viewModel.cancel() nav.popBack() }, ) } + + // Voice preset selector (only show when not uploading) + Spacer(modifier = Modifier.height(12.dp)) + VoicePresetSelector( + selectedPreset = viewModel.selectedPreset, + isProcessing = viewModel.isProcessingPreset, + onPresetSelected = { viewModel.selectPreset(it) }, + ) } Spacer(modifier = Modifier.height(16.dp)) From 9c0fbbf8a40b6ff21db286bd491026b7edd70456 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 15:54:15 +0100 Subject: [PATCH 20/47] correct pitch selection Add to voice note screen (new note) --- .../ui/actions/uploads/VoicePreset.kt | 6 +- .../loggedIn/home/ShortNotePostScreen.kt | 19 +++- .../loggedIn/home/ShortNotePostViewModel.kt | 99 ++++++++++++++++++- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt index 1d16a3c8e..176baab62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -29,7 +29,7 @@ enum class VoicePreset( @StringRes val labelRes: Int, ) { NONE(1.0, 0.0, R.string.voice_preset_none), - DEEP(0.75, -3.0, R.string.voice_preset_deep), - HIGH(1.4, 4.0, R.string.voice_preset_high), - NEUTRAL(1.0, -2.0, R.string.voice_preset_neutral), + DEEP(1.4, -3.0, R.string.voice_preset_deep), + HIGH(0.75, 4.0, R.string.voice_preset_high), + NEUTRAL(0.9, -2.0, R.string.voice_preset_neutral), } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 504c723be..325d4b3e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar @@ -366,11 +367,25 @@ private fun NewPostScreenBody( postViewModel.voiceOrchestrator?.let { orchestrator -> UploadProgressIndicator(orchestrator) } ?: run { + val displayMetadata = + metadata.copy( + waveform = postViewModel.activeWaveform ?: metadata.waveform, + ) VoiceMessagePreview( - voiceMetadata = metadata, - localFile = postViewModel.voiceLocalFile, + voiceMetadata = displayMetadata, + localFile = postViewModel.activeFile, onRemove = { postViewModel.removeVoiceMessage() }, ) + + // Voice preset selector (only show when not uploading and voice is pending) + if (postViewModel.voiceRecording != null) { + Spacer(modifier = Modifier.height(12.dp)) + VoicePresetSelector( + selectedPreset = postViewModel.selectedPreset, + isProcessing = postViewModel.isProcessingPreset, + onPresetSelected = { postViewModel.selectPreset(it) }, + ) + } } FileServerSelectionRow( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index d810088ed..9f4e5c237 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -50,9 +50,12 @@ import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber @@ -128,6 +131,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -194,6 +198,28 @@ open class ShortNotePostViewModel : var voiceSelectedServer by mutableStateOf(null) var voiceOrchestrator by mutableStateOf(null) + // Voice Anonymization + var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) + var isProcessingPreset: Boolean by mutableStateOf(false) + var distortedFiles: Map by mutableStateOf(emptyMap()) + private var processingJob: Job? = null + + val activeFile: java.io.File? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceLocalFile + } else { + distortedFiles[selectedPreset]?.file + } + + val activeWaveform: List? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceRecording?.amplitudes + } else { + distortedFiles[selectedPreset]?.waveform + } + // Polls var canUsePoll by mutableStateOf(false) var wantsPoll by mutableStateOf(false) @@ -794,6 +820,7 @@ open class ShortNotePostViewModel : multiOrchestrator = null isUploadingImage = false + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -801,6 +828,8 @@ open class ShortNotePostViewModel : voiceMetadata = null voiceSelectedServer = null voiceOrchestrator = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false pTags = null wantsPoll = false @@ -922,7 +951,7 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage + return !isUploadingVoice && !isUploadingImage && !isProcessingPreset } // Regular text/media posts require text @@ -951,10 +980,14 @@ open class ShortNotePostViewModel : } fun selectVoiceRecording(recording: RecordingResult) { - // Delete any existing temp file before replacing + // Cancel any ongoing processing and delete existing files + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file + voiceMetadata = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false } fun getVoicePreviewMetadata(): AudioMeta? = @@ -967,7 +1000,49 @@ open class ShortNotePostViewModel : ) } + fun selectPreset(preset: VoicePreset) { + if (isProcessingPreset || preset == selectedPreset) return + + if (preset == VoicePreset.NONE) { + selectedPreset = preset + return + } + + if (distortedFiles.containsKey(preset)) { + selectedPreset = preset + return + } + + val originalFile = voiceLocalFile ?: return + + processingJob?.cancel() + processingJob = + viewModelScope.launch { + isProcessingPreset = true + try { + val anonymizer = VoiceAnonymizer() + val result = anonymizer.anonymize(originalFile, preset) + + result + .onSuccess { anonymizedResult -> + distortedFiles = distortedFiles + (preset to anonymizedResult) + selectedPreset = preset + }.onFailure { error -> + Log.w("ShortNotePostViewModel", "Failed to anonymize voice", error) + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + } + } finally { + isProcessingPreset = false + processingJob = null + } + } + } + fun removeVoiceMessage() { + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -975,6 +1050,8 @@ open class ShortNotePostViewModel : voiceSelectedServer = null isUploadingVoice = false voiceOrchestrator = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false } private fun deleteVoiceLocalFile() { @@ -988,6 +1065,18 @@ open class ShortNotePostViewModel : Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } + + distortedFiles.values.forEach { result -> + try { + if (result.file.exists()) { + result.file.delete() + Log.d("ShortNotePostViewModel", "Deleted distorted file: ${result.file.absolutePath}") + } + } catch (e: Exception) { + Log.w("ShortNotePostViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) + } + } + distortedFiles = emptyMap() } suspend fun uploadVoiceMessageSync( @@ -995,6 +1084,8 @@ open class ShortNotePostViewModel : onError: (title: String, message: String) -> Unit, ) { val recording = voiceRecording ?: return + val fileToUpload = activeFile ?: recording.file + val waveform = activeWaveform ?: recording.amplitudes val appContext = Amethyst.instance.appContext val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title) val uploadVoiceNip95NotSupported = stringRes(appContext, R.string.upload_error_voice_message_nip95_not_supported) @@ -1006,7 +1097,7 @@ open class ShortNotePostViewModel : isUploadingVoice = true try { - val uri = android.net.Uri.fromFile(recording.file) + val uri = android.net.Uri.fromFile(fileToUpload) val orchestrator = UploadOrchestrator() voiceOrchestrator = orchestrator @@ -1033,7 +1124,7 @@ open class ShortNotePostViewModel : mimeType = recording.mimeType, hash = orchestratorResult.fileHeader.hash, duration = recording.duration, - waveform = recording.amplitudes, + waveform = waveform, ) // Delete the local file after successful upload deleteVoiceLocalFile() From 6f8533f5e1f5435cb4e6cf58b50b410f6e1375bc Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 21:51:22 +0100 Subject: [PATCH 21/47] added explainer to UI --- .../loggedIn/home/ShortNotePostScreen.kt | 29 ++++++++++++++++++- .../screen/loggedIn/home/VoiceReplyScreen.kt | 28 +++++++++++++++++- amethyst/src/main/res/values/strings.xml | 2 ++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 325d4b3e1..029fdc454 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -25,6 +25,7 @@ import android.net.Uri import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -38,20 +39,24 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel @@ -96,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp @@ -377,9 +383,30 @@ private fun NewPostScreenBody( onRemove = { postViewModel.removeVoiceMessage() }, ) - // Voice preset selector (only show when not uploading and voice is pending) + // Voice anonymization section (only show when not uploading and voice is pending) if (postViewModel.voiceRecording != null) { + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(thickness = DividerThickness) Spacer(modifier = Modifier.height(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.voice_anonymize_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.voice_anonymize_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = postViewModel.selectedPreset, isProcessing = postViewModel.isProcessingPreset, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 3794060bc..a6d7b888a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -47,6 +48,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R @@ -61,7 +64,9 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -168,8 +173,29 @@ private fun VoiceReplyScreenBody( ) } - // Voice preset selector (only show when not uploading) + // Voice anonymization section + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(thickness = DividerThickness) Spacer(modifier = Modifier.height(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.voice_anonymize_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.voice_anonymize_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = viewModel.selectedPreset, isProcessing = viewModel.isProcessingPreset, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index ebec66ae8..a78c01f31 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -188,6 +188,8 @@ Deep High Neutral + Anonymize + Alters your voice pitch. Note: basic pitch changes can potentially be reversed by determined listeners. User does not have a lightning address set up to receive sats "reply here.. " Copies the Note ID to the clipboard for sharing in Nostr From a4af706ebd9ad3f291d086fa785954069400385f Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 21:58:40 +0100 Subject: [PATCH 22/47] correct the item where progress spinner happens on --- .../ui/actions/uploads/VoicePresetSelector.kt | 5 +++-- .../screen/loggedIn/home/ShortNotePostScreen.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 16 ++++++++-------- .../ui/screen/loggedIn/home/VoiceReplyScreen.kt | 2 +- .../screen/loggedIn/home/VoiceReplyViewModel.kt | 13 +++++++------ 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt index aae4818af..44da46683 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt @@ -39,11 +39,12 @@ import com.vitorpamplona.amethyst.ui.stringRes @Composable fun VoicePresetSelector( selectedPreset: VoicePreset, - isProcessing: Boolean, + processingPreset: VoicePreset?, onPresetSelected: (VoicePreset) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current + val isProcessing = processingPreset != null Row( modifier = modifier.fillMaxWidth(), @@ -51,7 +52,7 @@ fun VoicePresetSelector( ) { VoicePreset.entries.forEach { preset -> val isSelected = preset == selectedPreset - val isThisProcessing = isProcessing && preset == selectedPreset + val isThisProcessing = preset == processingPreset val isEnabled = !isProcessing || preset == VoicePreset.NONE FilterChip( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 029fdc454..88ac4f5a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -409,7 +409,7 @@ private fun NewPostScreenBody( Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = postViewModel.selectedPreset, - isProcessing = postViewModel.isProcessingPreset, + processingPreset = postViewModel.processingPreset, onPresetSelected = { postViewModel.selectPreset(it) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 9f4e5c237..cd0030f0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -200,7 +200,7 @@ open class ShortNotePostViewModel : // Voice Anonymization var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var isProcessingPreset: Boolean by mutableStateOf(false) + var processingPreset: VoicePreset? by mutableStateOf(null) var distortedFiles: Map by mutableStateOf(emptyMap()) private var processingJob: Job? = null @@ -829,7 +829,7 @@ open class ShortNotePostViewModel : voiceSelectedServer = null voiceOrchestrator = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null pTags = null wantsPoll = false @@ -951,7 +951,7 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage && !isProcessingPreset + return !isUploadingVoice && !isUploadingImage && processingPreset == null } // Regular text/media posts require text @@ -987,7 +987,7 @@ open class ShortNotePostViewModel : voiceLocalFile = recording.file voiceMetadata = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null } fun getVoicePreviewMetadata(): AudioMeta? = @@ -1001,7 +1001,7 @@ open class ShortNotePostViewModel : } fun selectPreset(preset: VoicePreset) { - if (isProcessingPreset || preset == selectedPreset) return + if (processingPreset != null || preset == selectedPreset) return if (preset == VoicePreset.NONE) { selectedPreset = preset @@ -1018,7 +1018,7 @@ open class ShortNotePostViewModel : processingJob?.cancel() processingJob = viewModelScope.launch { - isProcessingPreset = true + processingPreset = preset try { val anonymizer = VoiceAnonymizer() val result = anonymizer.anonymize(originalFile, preset) @@ -1035,7 +1035,7 @@ open class ShortNotePostViewModel : ) } } finally { - isProcessingPreset = false + processingPreset = null processingJob = null } } @@ -1051,7 +1051,7 @@ open class ShortNotePostViewModel : isUploadingVoice = false voiceOrchestrator = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null } private fun deleteVoiceLocalFile() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index a6d7b888a..f1dae3aa5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -198,7 +198,7 @@ private fun VoiceReplyScreenBody( Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = viewModel.selectedPreset, - isProcessing = viewModel.isProcessingPreset, + processingPreset = viewModel.processingPreset, onPresetSelected = { viewModel.selectPreset(it) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 799fbf0a5..1c99db810 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -71,7 +71,7 @@ class VoiceReplyViewModel : ViewModel() { var isUploading: Boolean by mutableStateOf(false) var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var isProcessingPreset: Boolean by mutableStateOf(false) + var processingPreset: VoicePreset? by mutableStateOf(null) var distortedFiles: Map by mutableStateOf(emptyMap()) private var processingJob: Job? = null @@ -138,6 +138,7 @@ class VoiceReplyViewModel : ViewModel() { fun selectRecording(recording: RecordingResult) { cancelUpload() processingJob?.cancel() + processingPreset = null deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file @@ -175,10 +176,10 @@ class VoiceReplyViewModel : ViewModel() { distortedFiles = emptyMap() } - fun canSend(): Boolean = voiceRecording != null && !isUploading && !isProcessingPreset + fun canSend(): Boolean = voiceRecording != null && !isUploading && processingPreset == null fun selectPreset(preset: VoicePreset) { - if (isProcessingPreset || preset == selectedPreset) return + if (processingPreset != null || preset == selectedPreset) return if (preset == VoicePreset.NONE) { selectedPreset = preset @@ -195,7 +196,7 @@ class VoiceReplyViewModel : ViewModel() { processingJob?.cancel() processingJob = viewModelScope.launch { - isProcessingPreset = true + processingPreset = preset try { val anonymizer = VoiceAnonymizer() val result = anonymizer.anonymize(originalFile, preset) @@ -212,7 +213,7 @@ class VoiceReplyViewModel : ViewModel() { ) } } finally { - isProcessingPreset = false + processingPreset = null processingJob = null } } @@ -356,7 +357,7 @@ class VoiceReplyViewModel : ViewModel() { isUploading = false voiceOrchestrator = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null } override fun onCleared() { From 666015b8eb5b9050a42acdcc3b3959ff437789ea Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 22:27:00 +0100 Subject: [PATCH 23/47] reduce duplication: New shared controller for state + processing + distorted file cleanup New shared UI section for anonymization header + preset selector --- .../uploads/VoiceAnonymizationController.kt | 126 ++++++++++++++++++ .../uploads/VoiceAnonymizationSection.kt | 79 +++++++++++ .../ui/actions/uploads/VoiceAnonymizer.kt | 4 +- .../ui/actions/uploads/VoicePreset.kt | 12 +- .../loggedIn/home/ShortNotePostScreen.kt | 32 +---- .../loggedIn/home/ShortNotePostViewModel.kt | 103 ++++---------- .../screen/loggedIn/home/VoiceReplyScreen.kt | 31 +---- .../loggedIn/home/VoiceReplyViewModel.kt | 98 ++++---------- 8 files changed, 265 insertions(+), 220 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt new file mode 100644 index 000000000..b720e20d8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -0,0 +1,126 @@ +/** + * 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.ui.actions.uploads + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.io.File + +class VoiceAnonymizationController( + private val scope: CoroutineScope, + private val logTag: String, + private val onError: (Throwable) -> Unit, +) { + var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) + private set + var processingPreset: VoicePreset? by mutableStateOf(null) + private set + var distortedFiles: Map by mutableStateOf(emptyMap()) + private set + + private var processingJob: Job? = null + + fun activeFile(originalFile: File?): File? = + if (selectedPreset == VoicePreset.NONE) { + originalFile + } else { + distortedFiles[selectedPreset]?.file + } + + fun activeWaveform(originalWaveform: List?): List? = + if (selectedPreset == VoicePreset.NONE) { + originalWaveform + } else { + distortedFiles[selectedPreset]?.waveform + } + + fun selectPreset( + preset: VoicePreset, + originalFile: File?, + ) { + if (processingPreset != null || preset == selectedPreset) return + + if (preset == VoicePreset.NONE) { + selectedPreset = preset + return + } + + if (distortedFiles.containsKey(preset)) { + selectedPreset = preset + return + } + + val file = originalFile ?: return + + processingJob?.cancel() + processingJob = + scope.launch { + processingPreset = preset + try { + val anonymizer = VoiceAnonymizer() + val result = anonymizer.anonymize(file, preset) + + result + .onSuccess { anonymizedResult -> + distortedFiles = distortedFiles + (preset to anonymizedResult) + selectedPreset = preset + }.onFailure { error -> + Log.w(logTag, "Failed to anonymize voice", error) + onError(error) + } + } finally { + processingPreset = null + processingJob = null + } + } + } + + fun clear() { + cancelProcessing() + deleteDistortedFiles() + selectedPreset = VoicePreset.NONE + } + + fun deleteDistortedFiles() { + distortedFiles.values.forEach { result -> + try { + if (result.file.exists()) { + result.file.delete() + Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}") + } + } catch (e: Exception) { + Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e) + } + } + distortedFiles = emptyMap() + } + + private fun cancelProcessing() { + processingJob?.cancel() + processingJob = null + processingPreset = null + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt new file mode 100644 index 000000000..3b7f02028 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt @@ -0,0 +1,79 @@ +/** + * 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.ui.actions.uploads + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Size5dp + +@Composable +fun VoiceAnonymizationSection( + selectedPreset: VoicePreset, + processingPreset: VoicePreset?, + onPresetSelected: (VoicePreset) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + ) { + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(thickness = DividerThickness) + Spacer(modifier = Modifier.height(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.voice_anonymize_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.voice_anonymize_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + VoicePresetSelector( + selectedPreset = selectedPreset, + processingPreset = processingPreset, + onPresetSelected = onPresetSelected, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 08d15c859..1359f3318 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -34,11 +34,11 @@ import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter import be.tarsos.dsp.io.TarsosDSPAudioFormat import be.tarsos.dsp.resample.RateTransposer import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import java.io.File import java.nio.ByteOrder -import kotlin.coroutines.coroutineContext import kotlin.math.abs data class AnonymizedResult( @@ -147,7 +147,7 @@ class VoiceAnonymizer { var inputDone = false var outputDone = false - while (!outputDone && coroutineContext.isActive) { + while (!outputDone && currentCoroutineContext().isActive) { if (!inputDone) { val inputBufferIndex = decoder.dequeueInputBuffer(10000) if (inputBufferIndex >= 0) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt index 176baab62..49544e51a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -20,16 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.actions.uploads -import androidx.annotation.StringRes import com.vitorpamplona.amethyst.R enum class VoicePreset( val pitchFactor: Double, - val formantShift: Double, - @StringRes val labelRes: Int, + val labelRes: Int, ) { - NONE(1.0, 0.0, R.string.voice_preset_none), - DEEP(1.4, -3.0, R.string.voice_preset_deep), - HIGH(0.75, 4.0, R.string.voice_preset_high), - NEUTRAL(0.9, -2.0, R.string.voice_preset_neutral), + NONE(1.0, R.string.voice_preset_none), + DEEP(1.4, R.string.voice_preset_deep), + HIGH(0.75, R.string.voice_preset_high), + NEUTRAL(0.9, R.string.voice_preset_neutral), } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 88ac4f5a4..65a2b051d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -25,7 +25,6 @@ import android.net.Uri import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -39,24 +38,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel @@ -70,8 +65,8 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar @@ -101,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp @@ -385,29 +379,7 @@ private fun NewPostScreenBody( // Voice anonymization section (only show when not uploading and voice is pending) if (postViewModel.voiceRecording != null) { - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider(thickness = DividerThickness) - Spacer(modifier = Modifier.height(12.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(Size5dp), - ) { - Text( - text = stringRes(R.string.voice_anonymize_title), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(R.string.voice_anonymize_description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - VoicePresetSelector( + VoiceAnonymizationSection( selectedPreset = postViewModel.selectedPreset, processingPreset = postViewModel.processingPreset, onPresetSelected = { postViewModel.selectPreset(it) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index cd0030f0a..538057970 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -50,11 +50,10 @@ import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState @@ -131,7 +130,6 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -199,26 +197,29 @@ open class ShortNotePostViewModel : var voiceOrchestrator by mutableStateOf(null) // Voice Anonymization - var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var processingPreset: VoicePreset? by mutableStateOf(null) - var distortedFiles: Map by mutableStateOf(emptyMap()) - private var processingJob: Job? = null + private val voiceAnonymization = + VoiceAnonymizationController( + scope = viewModelScope, + logTag = "ShortNotePostViewModel", + onError = { error -> + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + }, + ) val activeFile: java.io.File? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceLocalFile - } else { - distortedFiles[selectedPreset]?.file - } + get() = voiceAnonymization.activeFile(voiceLocalFile) val activeWaveform: List? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceRecording?.amplitudes - } else { - distortedFiles[selectedPreset]?.waveform - } + get() = voiceAnonymization.activeWaveform(voiceRecording?.amplitudes) + + val selectedPreset: VoicePreset + get() = voiceAnonymization.selectedPreset + + val processingPreset: VoicePreset? + get() = voiceAnonymization.processingPreset // Polls var canUsePoll by mutableStateOf(false) @@ -820,7 +821,7 @@ open class ShortNotePostViewModel : multiOrchestrator = null isUploadingImage = false - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -828,8 +829,6 @@ open class ShortNotePostViewModel : voiceMetadata = null voiceSelectedServer = null voiceOrchestrator = null - selectedPreset = VoicePreset.NONE - processingPreset = null pTags = null wantsPoll = false @@ -981,13 +980,11 @@ open class ShortNotePostViewModel : fun selectVoiceRecording(recording: RecordingResult) { // Cancel any ongoing processing and delete existing files - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file voiceMetadata = null - selectedPreset = VoicePreset.NONE - processingPreset = null } fun getVoicePreviewMetadata(): AudioMeta? = @@ -1001,48 +998,11 @@ open class ShortNotePostViewModel : } fun selectPreset(preset: VoicePreset) { - if (processingPreset != null || preset == selectedPreset) return - - if (preset == VoicePreset.NONE) { - selectedPreset = preset - return - } - - if (distortedFiles.containsKey(preset)) { - selectedPreset = preset - return - } - - val originalFile = voiceLocalFile ?: return - - processingJob?.cancel() - processingJob = - viewModelScope.launch { - processingPreset = preset - try { - val anonymizer = VoiceAnonymizer() - val result = anonymizer.anonymize(originalFile, preset) - - result - .onSuccess { anonymizedResult -> - distortedFiles = distortedFiles + (preset to anonymizedResult) - selectedPreset = preset - }.onFailure { error -> - Log.w("ShortNotePostViewModel", "Failed to anonymize voice", error) - accountViewModel.toastManager.toast( - stringRes(Amethyst.instance.appContext, R.string.error), - error.message ?: "Voice anonymization failed", - ) - } - } finally { - processingPreset = null - processingJob = null - } - } + voiceAnonymization.selectPreset(preset, voiceLocalFile) } fun removeVoiceMessage() { - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -1050,8 +1010,6 @@ open class ShortNotePostViewModel : voiceSelectedServer = null isUploadingVoice = false voiceOrchestrator = null - selectedPreset = VoicePreset.NONE - processingPreset = null } private fun deleteVoiceLocalFile() { @@ -1065,18 +1023,6 @@ open class ShortNotePostViewModel : Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } - - distortedFiles.values.forEach { result -> - try { - if (result.file.exists()) { - result.file.delete() - Log.d("ShortNotePostViewModel", "Deleted distorted file: ${result.file.absolutePath}") - } - } catch (e: Exception) { - Log.w("ShortNotePostViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) - } - } - distortedFiles = emptyMap() } suspend fun uploadVoiceMessageSync( @@ -1128,6 +1074,7 @@ open class ShortNotePostViewModel : ) // Delete the local file after successful upload deleteVoiceLocalFile() + voiceAnonymization.deleteDistortedFiles() voiceLocalFile = null voiceRecording = null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index f1dae3aa5..a2573663b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -48,25 +47,21 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -174,29 +169,7 @@ private fun VoiceReplyScreenBody( } // Voice anonymization section - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider(thickness = DividerThickness) - Spacer(modifier = Modifier.height(12.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(Size5dp), - ) { - Text( - text = stringRes(R.string.voice_anonymize_title), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(R.string.voice_anonymize_description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - VoicePresetSelector( + VoiceAnonymizationSection( selectedPreset = viewModel.selectedPreset, processingPreset = viewModel.processingPreset, onPresetSelected = { viewModel.selectPreset(it) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 1c99db810..eea2a0af6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -34,9 +34,8 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -70,26 +69,29 @@ class VoiceReplyViewModel : ViewModel() { var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null) var isUploading: Boolean by mutableStateOf(false) - var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var processingPreset: VoicePreset? by mutableStateOf(null) - var distortedFiles: Map by mutableStateOf(emptyMap()) - private var processingJob: Job? = null + private val voiceAnonymization = + VoiceAnonymizationController( + scope = viewModelScope, + logTag = "VoiceReplyViewModel", + onError = { error -> + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + }, + ) val activeFile: File? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceLocalFile - } else { - distortedFiles[selectedPreset]?.file - } + get() = voiceAnonymization.activeFile(voiceLocalFile) val activeWaveform: List? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceRecording?.amplitudes - } else { - distortedFiles[selectedPreset]?.waveform - } + get() = voiceAnonymization.activeWaveform(voiceRecording?.amplitudes) + + val selectedPreset: VoicePreset + get() = voiceAnonymization.selectedPreset + + val processingPreset: VoicePreset? + get() = voiceAnonymization.processingPreset private var uploadJob: Job? = null @@ -137,13 +139,11 @@ class VoiceReplyViewModel : ViewModel() { fun selectRecording(recording: RecordingResult) { cancelUpload() - processingJob?.cancel() - processingPreset = null + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file voiceMetadata = null - selectedPreset = VoicePreset.NONE } private fun cancelUpload() { @@ -162,61 +162,12 @@ class VoiceReplyViewModel : ViewModel() { Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } - - distortedFiles.values.forEach { result -> - try { - if (result.file.exists()) { - result.file.delete() - Log.d("VoiceReplyViewModel", "Deleted distorted file: ${result.file.absolutePath}") - } - } catch (e: Exception) { - Log.w("VoiceReplyViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) - } - } - distortedFiles = emptyMap() } fun canSend(): Boolean = voiceRecording != null && !isUploading && processingPreset == null fun selectPreset(preset: VoicePreset) { - if (processingPreset != null || preset == selectedPreset) return - - if (preset == VoicePreset.NONE) { - selectedPreset = preset - return - } - - if (distortedFiles.containsKey(preset)) { - selectedPreset = preset - return - } - - val originalFile = voiceLocalFile ?: return - - processingJob?.cancel() - processingJob = - viewModelScope.launch { - processingPreset = preset - try { - val anonymizer = VoiceAnonymizer() - val result = anonymizer.anonymize(originalFile, preset) - - result - .onSuccess { anonymizedResult -> - distortedFiles = distortedFiles + (preset to anonymizedResult) - selectedPreset = preset - }.onFailure { error -> - Log.w("VoiceReplyViewModel", "Failed to anonymize voice", error) - accountViewModel.toastManager.toast( - stringRes(Amethyst.instance.appContext, R.string.error), - error.message ?: "Voice anonymization failed", - ) - } - } finally { - processingPreset = null - processingJob = null - } - } + voiceAnonymization.selectPreset(preset, voiceLocalFile) } fun sendVoiceReply(onSuccess: () -> Unit) { @@ -326,6 +277,7 @@ class VoiceReplyViewModel : ViewModel() { } deleteVoiceLocalFile() + voiceAnonymization.deleteDistortedFiles() voiceLocalFile = null voiceRecording = null voiceMetadata = audioMeta @@ -348,7 +300,7 @@ class VoiceReplyViewModel : ViewModel() { fun cancel() { cancelUpload() - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -356,8 +308,6 @@ class VoiceReplyViewModel : ViewModel() { voiceSelectedServer = null isUploading = false voiceOrchestrator = null - selectedPreset = VoicePreset.NONE - processingPreset = null } override fun onCleared() { From 3287e3c031dd399f34ef3a16ee72393608524286 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 22:46:03 +0100 Subject: [PATCH 24/47] =?UTF-8?q?add=20a=20=C2=B110%=20random=20variation?= =?UTF-8?q?=20to=20HIGH=20and=20DEEP=20pitch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 1359f3318..ee036c482 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -50,7 +50,6 @@ data class AnonymizedResult( class VoiceAnonymizer { companion object { private const val TAG = "VoiceAnonymizer" - private const val SAMPLE_RATE = 44100 private const val CHANNELS = 1 private const val BIT_RATE = 128000 } @@ -206,7 +205,16 @@ class VoiceAnonymizer { sampleRate: Int, onProgress: (Float) -> Unit, ): FloatArray { - val factor = preset.pitchFactor + val baseFactor = preset.pitchFactor + val factor = + when (preset) { + VoicePreset.DEEP, VoicePreset.HIGH -> { + // Add ±10% random variation + val randomShift = 0.9 + (Math.random() * 0.2) + baseFactor * randomShift + } + else -> baseFactor + } val processedSamples = mutableListOf() val totalSamples = pcmData.size From f13a9164c2d931ad9b8998e2b3e944cf37d0e88b Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 22:55:53 +0100 Subject: [PATCH 25/47] code review: resource leaks and init guard --- .../ui/actions/uploads/VoiceAnonymizer.kt | 289 +++++++++--------- .../loggedIn/home/VoiceReplyViewModel.kt | 10 +- 2 files changed, 154 insertions(+), 145 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index ee036c482..391b79349 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -112,91 +112,94 @@ class VoiceAnonymizer { onProgress: (Float) -> Unit, ): DecodedAudio { val extractor = MediaExtractor() - extractor.setDataSource(inputFile.absolutePath) + var decoder: MediaCodec? = null - var audioTrackIndex = -1 - var format: MediaFormat? = null - for (i in 0 until extractor.trackCount) { - val trackFormat = extractor.getTrackFormat(i) - val mime = trackFormat.getString(MediaFormat.KEY_MIME) - if (mime?.startsWith("audio/") == true) { - audioTrackIndex = i - format = trackFormat - break + try { + extractor.setDataSource(inputFile.absolutePath) + + var audioTrackIndex = -1 + var format: MediaFormat? = null + for (i in 0 until extractor.trackCount) { + val trackFormat = extractor.getTrackFormat(i) + val mime = trackFormat.getString(MediaFormat.KEY_MIME) + if (mime?.startsWith("audio/") == true) { + audioTrackIndex = i + format = trackFormat + break + } } - } - if (audioTrackIndex == -1 || format == null) { - extractor.release() - throw IllegalStateException("No audio track found in file") - } + if (audioTrackIndex == -1 || format == null) { + throw IllegalStateException("No audio track found in file") + } - extractor.selectTrack(audioTrackIndex) - val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" - val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) - val durationUs = format.getLong(MediaFormat.KEY_DURATION) - val duration = (durationUs / 1_000_000).toInt() + extractor.selectTrack(audioTrackIndex) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" + val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + val durationUs = format.getLong(MediaFormat.KEY_DURATION) + val duration = (durationUs / 1_000_000).toInt() - val decoder = MediaCodec.createDecoderByType(mime) - decoder.configure(format, null, null, 0) - decoder.start() + decoder = MediaCodec.createDecoderByType(mime) + decoder.configure(format, null, null, 0) + decoder.start() - val pcmSamples = mutableListOf() - val bufferInfo = MediaCodec.BufferInfo() - var inputDone = false - var outputDone = false + val pcmSamples = mutableListOf() + val bufferInfo = MediaCodec.BufferInfo() + var inputDone = false + var outputDone = false - while (!outputDone && currentCoroutineContext().isActive) { - if (!inputDone) { - val inputBufferIndex = decoder.dequeueInputBuffer(10000) - if (inputBufferIndex >= 0) { - val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!! - val sampleSize = extractor.readSampleData(inputBuffer, 0) - if (sampleSize < 0) { - decoder.queueInputBuffer( - inputBufferIndex, - 0, - 0, - 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM, - ) - inputDone = true - } else { - val presentationTimeUs = extractor.sampleTime - decoder.queueInputBuffer( - inputBufferIndex, - 0, - sampleSize, - presentationTimeUs, - 0, - ) - extractor.advance() - if (durationUs > 0) { - onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f)) + while (!outputDone && currentCoroutineContext().isActive) { + if (!inputDone) { + val inputBufferIndex = decoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!! + val sampleSize = extractor.readSampleData(inputBuffer, 0) + if (sampleSize < 0) { + decoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + val presentationTimeUs = extractor.sampleTime + decoder.queueInputBuffer( + inputBufferIndex, + 0, + sampleSize, + presentationTimeUs, + 0, + ) + extractor.advance() + if (durationUs > 0) { + onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f)) + } } } } + + val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000) + if (outputBufferIndex >= 0) { + val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!! + val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer() + while (shortBuffer.hasRemaining()) { + pcmSamples.add(shortBuffer.get() / 32768f) + } + decoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true + } + } } - val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000) - if (outputBufferIndex >= 0) { - val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!! - val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer() - while (shortBuffer.hasRemaining()) { - pcmSamples.add(shortBuffer.get() / 32768f) - } - decoder.releaseOutputBuffer(outputBufferIndex, false) - if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { - outputDone = true - } - } + return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) + } finally { + decoder?.stop() + decoder?.release() + extractor.release() } - - decoder.stop() - decoder.release() - extractor.release() - - return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) } private fun processPcmWithTarsos( @@ -320,86 +323,90 @@ class VoiceAnonymizer { format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE) val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC) - encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) - encoder.start() - val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) - var audioTrackIndex = -1 var muxerStarted = false - val bufferInfo = MediaCodec.BufferInfo() - var inputOffset = 0 - var inputDone = false - var outputDone = false - val totalSamples = pcmData.size + try { + encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) + encoder.start() - while (!outputDone) { - if (!inputDone) { - val inputBufferIndex = encoder.dequeueInputBuffer(10000) - if (inputBufferIndex >= 0) { - val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!! - inputBuffer.clear() + var audioTrackIndex = -1 + val bufferInfo = MediaCodec.BufferInfo() + var inputOffset = 0 + var inputDone = false + var outputDone = false + val totalSamples = pcmData.size - val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset) - if (samplesToWrite <= 0) { - encoder.queueInputBuffer( - inputBufferIndex, - 0, - 0, - 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM, - ) - inputDone = true - } else { - for (i in 0 until samplesToWrite) { - val sample = - (pcmData[inputOffset + i] * 32767) - .toInt() - .coerceIn(-32768, 32767) - .toShort() - inputBuffer.putShort(sample) + while (!outputDone) { + if (!inputDone) { + val inputBufferIndex = encoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!! + inputBuffer.clear() + + val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset) + if (samplesToWrite <= 0) { + encoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + for (i in 0 until samplesToWrite) { + val sample = + (pcmData[inputOffset + i] * 32767) + .toInt() + .coerceIn(-32768, 32767) + .toShort() + inputBuffer.putShort(sample) + } + val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate + encoder.queueInputBuffer( + inputBufferIndex, + 0, + inputBuffer.position(), + presentationTimeUs, + 0, + ) + inputOffset += samplesToWrite + onProgress(inputOffset.toFloat() / totalSamples) + } + } + } + + val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000) + when { + outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { + audioTrackIndex = muxer.addTrack(encoder.outputFormat) + muxer.start() + muxerStarted = true + } + + outputBufferIndex >= 0 -> { + val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!! + if (muxerStarted && bufferInfo.size > 0) { + outputBuffer.position(bufferInfo.offset) + outputBuffer.limit(bufferInfo.offset + bufferInfo.size) + muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo) + } + encoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true } - val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate - encoder.queueInputBuffer( - inputBufferIndex, - 0, - inputBuffer.position(), - presentationTimeUs, - 0, - ) - inputOffset += samplesToWrite - onProgress(inputOffset.toFloat() / totalSamples) } } } - - val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000) - when { - outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { - audioTrackIndex = muxer.addTrack(encoder.outputFormat) - muxer.start() - muxerStarted = true - } - - outputBufferIndex >= 0 -> { - val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!! - if (muxerStarted && bufferInfo.size > 0) { - outputBuffer.position(bufferInfo.offset) - outputBuffer.limit(bufferInfo.offset + bufferInfo.size) - muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo) - } - encoder.releaseOutputBuffer(outputBufferIndex, false) - if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { - outputDone = true - } - } + } finally { + encoder.stop() + encoder.release() + if (muxerStarted) { + muxer.stop() } + muxer.release() } - - encoder.stop() - encoder.release() - muxer.stop() - muxer.release() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index eea2a0af6..5d0099b89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -74,10 +74,12 @@ class VoiceReplyViewModel : ViewModel() { scope = viewModelScope, logTag = "VoiceReplyViewModel", onError = { error -> - accountViewModel.toastManager.toast( - stringRes(Amethyst.instance.appContext, R.string.error), - error.message ?: "Voice anonymization failed", - ) + if (::accountViewModel.isInitialized) { + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + } }, ) From ec159ff85f8617429d30a27e7d1eb8e672717efa Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 23:02:33 +0100 Subject: [PATCH 26/47] add translations --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 6 ++++++ amethyst/src/main/res/values-de-rDE/strings.xml | 6 ++++++ amethyst/src/main/res/values-pt-rBR/strings.xml | 6 ++++++ amethyst/src/main/res/values-sv-rSE/strings.xml | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 275889023..6c1aa14d8 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1228,4 +1228,10 @@ Smazat seznam Smazat sadu doporučení Typy + Žádný + Hluboký + Vysoký + Neutrální + Anonymizovat + Upravuje výšku vašeho hlasu. Poznámka: základní změny výšky hlasu mohou být odhodlanými posluchači potenciálně zpětně rozpoznány. diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 7a521e907..586128b78 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1233,4 +1233,10 @@ anz der Bedingungen ist erforderlich Liste löschen Empfehlungspaket löschen Typen + Keine + Tief + Hoch + Neutral + Anonymisieren + Verändert die Tonhöhe deiner Stimme. Hinweis: einfache Tonhöhenänderungen können von entschlossenen Zuhörern möglicherweise rückgängig gemacht werden. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index a3f8b297c..97ff4065d 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1228,4 +1228,10 @@ Excluir lista Excluir pacote de recomendações Tipos + Nenhum + Grave + Agudo + Neutro + Anonimizar + Altera o tom da sua voz. Nota: alterações básicas de tom podem potencialmente ser revertidas por ouvintes determinados. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 7a56f1c6e..de6dae781 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1227,4 +1227,10 @@ Ta bort lista Ta bort rekommendationspaket Typer + Ingen + Djup + Hög + Neutral + Anonymisera + Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. From 5241a6dfa79cedd1e0cdd54f3fb3e59b078f2d8d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 11:27:45 -0500 Subject: [PATCH 27/47] Fixes missing cache after AI PR for the desktop app Removes dependency on the Cache from User. Updates interface methods to match. --- .../amethyst/model/LocalCache.kt | 8 +++-- .../com/vitorpamplona/amethyst/model/User.kt | 3 -- .../reqCommand/user/UserObservers.kt | 5 +++- .../amethyst/commons/model/User.kt | 29 ++++--------------- .../commons/model/cache/ICacheProvider.kt | 3 +- 5 files changed, 18 insertions(+), 30 deletions(-) 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 beac2ade5..34224a4b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -324,7 +324,7 @@ object LocalCache : ILocalCache, ICacheProvider { return users.get(key) } - override fun countUsers(predicate: (String, Any) -> Boolean): Int { + override fun countUsers(predicate: (String, User) -> Boolean): Int { var count = 0 users.forEach { key, user -> if (predicate(key, user)) count++ @@ -531,9 +531,13 @@ object LocalCache : ILocalCache, ICacheProvider { // avoids processing empty contact lists. if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty() && (wasVerified || justVerify(event))) { - user.updateContactList(event) + val needsToUpdateFollowers = user.updateContactList(event) // Log.d("CL", "Consumed contact list ${user.toNostrUri()} ${event.relays()?.size}") + needsToUpdateFollowers.forEach { + getUserIfExists(it)?.flowSet?.followers?.invalidateData() + } + updateObservables(event) return true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index bfa68c725..860631c0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -21,9 +21,6 @@ package com.vitorpamplona.amethyst.model // Re-export from commons for backwards compatibility -typealias UserDependencies = com.vitorpamplona.amethyst.commons.model.UserDependencies typealias User = com.vitorpamplona.amethyst.commons.model.User -typealias UserFlowSet = com.vitorpamplona.amethyst.commons.model.UserFlowSet typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.RelayInfo -typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.UserBundledRefresherFlow typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index d2d2b59da..7e0496b7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import kotlinx.collections.immutable.ImmutableList @@ -392,7 +393,9 @@ fun observeUserFollowerCount( .followers.stateFlow .sample(200) .mapLatest { userState -> - userState.user.transientFollowerCount() + LocalCache.countUsers { _, user -> + user.latestContactList?.isTaggedUser(user.pubkeyHex) ?: false + } }.distinctUntilChanged() .flowOn(Dispatchers.IO) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index 287992db5..558cf168c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.lightning.Lud06 +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata @@ -51,7 +51,6 @@ class User( val pubkeyHex: String, val nip65RelayListNote: Note, val dmRelayListNote: Note, - private val cacheProvider: ICacheProvider? = null, ) { private var reports: UserReportCache? = null private var cards: UserCardsCache? = null @@ -120,8 +119,8 @@ class User( fun profilePicture(): String? = info?.picture - fun updateContactList(event: ContactListEvent) { - if (event.id == latestContactList?.id) return + fun updateContactList(event: ContactListEvent): Set { + if (event.id == latestContactList?.id) return emptySet() val oldContactListEvent = latestContactList latestContactList = event @@ -129,20 +128,9 @@ class User( // Update following of the current user flowSet?.follows?.invalidateData() - // Update Followers of the past user list - // Update Followers of the new contact list - (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - (cacheProvider?.getUserIfExists(it) as? User) - ?.flowSet - ?.followers - ?.invalidateData() - } - (latestContactList)?.unverifiedFollowKeySet()?.forEach { - (cacheProvider?.getUserIfExists(it) as? User) - ?.flowSet - ?.followers - ?.invalidateData() - } + val affectedUsers = event.verifiedFollowKeySet() + (oldContactListEvent?.verifiedFollowKeySet() ?: emptySet()) + + return affectedUsers } fun addZap( @@ -217,11 +205,6 @@ class User( fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size - fun transientFollowerCount(): Int = - cacheProvider?.countUsers { _, it -> - (it as? User)?.latestContactList?.isTaggedUser(pubkeyHex) ?: false - } ?: 0 - fun reportsOrNull(): UserReportCache? = reports fun reports(): UserReportCache = reports ?: UserReportCache().also { reports = it } 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 7d1f32f98..16d8fae4e 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey /** @@ -60,7 +61,7 @@ interface ICacheProvider { * @param predicate Filter function for counting users * @return Count of users matching the predicate */ - fun countUsers(predicate: (String, Any) -> Boolean): Int + fun countUsers(predicate: (String, User) -> Boolean): Int /** * Gets a Note if it exists in cache. From 6d15e4861c95f9dac8808a1ac75daa8bb73b0043 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 12:22:40 -0500 Subject: [PATCH 28/47] Moves channels to commons and removes the dependency in the cache from Note --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/LocalCache.kt | 15 ++------- .../model/emphChat/EphemeralChatChannel.kt | 2 +- .../nip28PublicChats/PublicChatChannel.kt | 2 +- .../LiveActivitiesChannel.kt | 2 +- .../amethyst/model/privateChats/Chatroom.kt | 2 +- .../ChannelFinderFilterAssemblyGroup.kt | 2 +- ...ChannelFinderFilterAssemblySubscription.kt | 2 +- .../reqCommand/channel/ChannelObservers.kt | 4 +-- .../ChannelMetadataWatcherSubAssembler.kt | 2 +- .../LiveActivityWatcherSubAssembly.kt | 2 +- .../amethyst/ui/dal/ChangesFlowFilter.kt | 2 +- .../ui/feeds/ChannelFeedContentState.kt | 2 +- .../amethyst/ui/feeds/ChannelFeedState.kt | 2 +- .../privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../dal/ChannelFeedViewModel.kt | 2 +- .../datasource/ChannelFilterAssembler.kt | 2 +- .../ChannelFilterAssemblerSubscription.kt | 2 +- .../ChannelFromUserFilterSubAssembler.kt | 2 +- .../ChannelPublicFilterSubAssembler.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../loggedIn/home/live/LiveStatusIndicator.kt | 2 +- .../amethyst/commons}/model/Channel.kt | 10 ++++-- .../amethyst/commons/model/IAccount.kt | 2 ++ .../amethyst/commons}/model/ListChange.kt | 2 +- .../amethyst/commons/model/Note.kt | 33 +++++++++++++------ .../commons/model/cache/ICacheProvider.kt | 17 ++-------- 29 files changed, 62 insertions(+), 65 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/Channel.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/ListChange.kt (96%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e2171228c..b0797e6c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1658,7 +1658,7 @@ class Account( fun isAllHidden(users: Set): Boolean = users.all { isHidden(it) } - fun isHidden(user: User) = isHidden(user.pubkeyHex) + override fun isHidden(user: User) = isHidden(user.pubkeyHex) fun isHidden(userHex: String): Boolean = hiddenUsers.flow.value.isUserHidden(userHex) 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 34224a4b1..f030b6898 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -23,8 +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.Channel 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 @@ -332,17 +332,6 @@ object LocalCache : ILocalCache, ICacheProvider { 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) @@ -1409,7 +1398,7 @@ object LocalCache : ILocalCache, ICacheProvider { } } - fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) } + override fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) } fun getAnyChannel(noteEvent: Event): Channel? = when (noteEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt index 12a28673d..99f001fca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.emphChat import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt index 75fa41af7..2330dd92c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.nip28PublicChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.toShortDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index d595164c2..df732bd66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.nip53LiveActivities import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.toShortDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index c49b79eab..3850b3116 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.ListChange +import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NotesGatherer import com.vitorpamplona.amethyst.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index 5600979a5..3a46ae22e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt index eebcea166..7a5534b39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 1add909d1..c251e230c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ChannelState +import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.ChannelState import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index 2932b0d86..f00993da3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 0a3a9129d..c13bd4497 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt index dab6bd150..c9ca76ec6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.ListChange +import com.vitorpamplona.amethyst.commons.model.ListChange import kotlinx.coroutines.flow.MutableSharedFlow interface ChangesFlowFilter : IAdditiveFeedFilter { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index cf62c2cf5..46b3c3fe0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt index 123a3e4d6..e6f003813 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import kotlinx.coroutines.flow.MutableStateFlow @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 95173849d..a1f09c95a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -24,10 +24,10 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.ListChange import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index db66af206..cff3e85d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt index 23c1c7fb7..94bf576dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ListChangeFeedViewModel class ChannelFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt index 8beefc621..91d249db6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelFromUserFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelPublicFilterSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt index 0dc9e50f8..e9ff59571 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datas import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index 700e04709..28bba13f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index f5730d428..2eb7bb5a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 4d3a5b6b6..2bec459d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -33,9 +33,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 62f55d898..00f2a0e6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 8a9c2966f..cd7cb3030 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt index 8ea68969b..5844cf49c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt @@ -18,10 +18,9 @@ * 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.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.cache.LargeCache @@ -32,6 +31,11 @@ import java.lang.ref.WeakReference @Stable abstract class Channel : NotesGatherer { + companion object { + val DefaultFeedOrder: Comparator = + compareByDescending { it.createdAt() }.thenBy { it.idHex } + } + val notes = LargeCache() var lastNote: Note? = null @@ -143,7 +147,7 @@ abstract class Channel : NotesGatherer { return toBeRemoved.toSet() } - fun pruneHiddenMessages(account: Account): Set { + fun pruneHiddenMessages(account: IAccount): Set { val hidden = notes .filter { key, it -> diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt index 7460b925a..ff2345b6d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt @@ -84,4 +84,6 @@ interface IAccount { /** Set of followed user pubkeys (for feed ordering/highlighting) */ fun followingKeySet(): Set + + fun isHidden(user: User): Boolean } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ListChange.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ListChange.kt index 1c0530571..fe62b8659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ListChange.kt @@ -18,7 +18,7 @@ * 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 sealed class ListChange { data class Addition( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index 49af94c47..5ef037053 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -109,7 +110,6 @@ class AddressableNote( @Stable open class Note( val idHex: String, - private val cacheProvider: ICacheProvider? = null, ) : NotesGatherer { // These fields are only available after the Text Note event is received. // They are immutable after that. @@ -196,15 +196,28 @@ open class Note( } fun relayHintUrl(): NormalizedRelayUrl? { - val noteEvent = event - val communityPostRelays = - when (noteEvent) { - is CommunityDefinitionEvent -> noteEvent.relayUrls().ifEmpty { null }?.toSet() - is IsInPublicChatChannel -> cacheProvider?.getAnyChannel(this)?.relays() - else -> null + // checks Community Events first + when (val noteEvent = event) { + is CommunityDefinitionEvent -> noteEvent.relayUrls().firstOrNull()?.let { return it } + is IsInPublicChatChannel -> { + inGatherers?.forEach { + if (it is com.vitorpamplona.amethyst.commons.model.Channel) { + it.relays().firstOrNull()?.let { return it } + } + } } - - if (!communityPostRelays.isNullOrEmpty()) return (communityPostRelays as? Collection)?.firstOrNull() + is LiveActivitiesEvent -> { + noteEvent.relays().ifEmpty { null }?.toSet() + } + is LiveActivitiesChatMessageEvent -> { + inGatherers?.forEach { + if (it is com.vitorpamplona.amethyst.commons.model.Channel) { + it.relays().firstOrNull()?.let { return it } + } + } + } + is EphemeralChatEvent -> noteEvent.roomId()?.let { return it.relayUrl } + } val currentOutbox = author?.outboxRelays()?.toSet() 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 16d8fae4e..b62cae5c8 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 @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -43,7 +45,7 @@ interface ICacheProvider { * @param note The note to look up channel for * @return The channel if found, null otherwise */ - fun getAnyChannel(note: Any?): IChannel? + fun getAnyChannel(note: Note): Channel? /** * Gets a User by public key hex. @@ -98,16 +100,3 @@ interface ICacheProvider { */ fun hasBeenDeleted(event: Any): Boolean } - -/** - * Minimal channel interface for relay resolution. - * Full channel implementations (PublicChatChannel, LiveActivitiesChannel) - * implement this interface. - */ -interface IChannel { - /** - * Gets the relay URLs for this channel. - * @return List of relay URLs or null if none configured - */ - fun relays(): List? -} From a8dd229fb23babc979ede8e3a2bef98b8b0eee45 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:30:51 -0500 Subject: [PATCH 29/47] Moves Channels (public chats, ephemeral channels and live streams) Account modules to commons --- .../vitorpamplona/amethyst/model/Account.kt | 12 ++++----- .../amethyst/model/AccountSettings.kt | 13 +++++++--- .../amethyst/model/LocalCache.kt | 10 ++++---- .../reqCommand/channel/ChannelObservers.kt | 4 +-- ...adataAndLiveActivityWatcherSubAssembler.kt | 4 +-- .../ChannelMetadataWatcherSubAssembler.kt | 2 +- .../FilterChannelMetadataCreationById.kt | 2 +- .../FilterChannelMetadataUpdatesById.kt | 2 +- .../FilterLiveStreamUpdatesByAddress.kt | 2 +- .../LiveActivityWatcherSubAssembly.kt | 2 +- .../reqCommand/user/UserObservers.kt | 4 +-- .../ui/navigation/routes/RouteMaker.kt | 6 ++--- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 6 ++--- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 6 ++--- .../ChannelFromUserFilterSubAssembler.kt | 6 ++--- .../ChannelPublicFilterSubAssembler.kt | 6 ++--- .../FilterMessagesToEphemeralChat.kt | 2 +- .../FilterMessagesToLiveStream.kt | 2 +- .../FilterMessagesToPublicChat.kt | 2 +- .../FilterMyMessagesToEphemeralChat.kt | 2 +- .../FilterMyMessagesToLiveActivities.kt | 2 +- .../FilterMyMessagesToPublicChat.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 2 +- .../ephemChat/LoadEphemeralChatChannel.kt | 2 +- .../header/EphemeralChatChannelHeader.kt | 2 +- .../ephemChat/header/EphemeralChatTopBar.kt | 2 +- .../header/ShortEphemeralChatChannelHeader.kt | 2 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../nip28PublicChat/ChannelView.kt | 2 +- .../header/LongPublicChatChannelHeader.kt | 2 +- .../header/PublicChatChannelHeader.kt | 2 +- .../header/PublicChatTopBar.kt | 2 +- .../header/ShortPublicChatChannelHeader.kt | 2 +- .../header/actions/EditChatButton.kt | 2 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../header/actions/LinkChatButton.kt | 2 +- .../header/actions/OpenChatButton.kt | 2 +- .../header/actions/ShareChatButton.kt | 2 +- .../metadata/ChannelMetadataScreen.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 2 +- .../nip53LiveActivities/ChannelView.kt | 2 +- .../nip53LiveActivities/ShowVideoStreaming.kt | 2 +- .../header/LiveActivitiesChannelHeader.kt | 2 +- .../header/LiveActivityTopBar.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 2 +- .../send/ChannelFileUploadDialog.kt | 4 +-- .../send/ChannelNewMessageViewModel.kt | 6 ++--- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +-- .../RenderPublicChatChannelThumb.kt | 2 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 4 +-- .../loggedIn/home/dal/HomeLiveFilter.kt | 4 +-- .../loggedIn/home/live/LiveStatusIndicator.kt | 4 +-- .../home/live/RenderEphemeralBubble.kt | 2 +- .../home/live/RenderLiveActivityBubble.kt | 2 +- .../commons/model/cache/ICacheProvider.kt | 16 +++++++++++- .../model/emphChat/EphemeralChatChannel.kt | 2 +- .../EphemeralChatListDecryptionCache.kt | 2 +- .../model/emphChat/EphemeralChatListState.kt | 25 +++++++++++-------- .../nip28PublicChats/PublicChatChannel.kt | 8 +++--- .../PublicChatListDecryptionCache.kt | 2 +- .../nip28PublicChats/PublicChatListState.kt | 25 +++++++++++-------- .../nip38UserStatuses/UserStatusAction.kt | 2 +- .../LiveActivitiesChannel.kt | 8 +++--- 67 files changed, 153 insertions(+), 122 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/emphChat/EphemeralChatChannel.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/emphChat/EphemeralChatListDecryptionCache.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/emphChat/EphemeralChatListState.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip28PublicChats/PublicChatChannel.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip28PublicChats/PublicChatListDecryptionCache.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip28PublicChats/PublicChatListState.kt (90%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip38UserStatuses/UserStatusAction.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip53LiveActivities/LiveActivitiesChannel.kt (92%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b0797e6c7..fc51b78cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -24,15 +24,18 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.model.IAccount +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState @@ -46,9 +49,6 @@ import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index a654dc436..fb10ec4ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.screen.FeedDefinition @@ -143,7 +145,8 @@ class AccountSettings( val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), var hasDonatedInVersion: MutableStateFlow> = MutableStateFlow(setOf()), val pendingAttestations: MutableStateFlow> = MutableStateFlow>(mapOf()), -) { +) : EphemeralChatRepository, + PublicChatListRepository { val saveable = MutableStateFlow(AccountSettingsUpdater(null)) val syncedSettings: AccountSyncedSettings = AccountSyncedSettings(AccountSyncedSettingsInternal()) @@ -389,7 +392,9 @@ class AccountSettings( } } - fun updateChannelListTo(newChannelList: ChannelListEvent?) { + override fun channelList() = backupChannelList + + override fun updateChannelListTo(newChannelList: ChannelListEvent?) { if (newChannelList == null || newChannelList.tags.isEmpty()) return // Events might be different objects, we have to compare their ids. @@ -429,7 +434,9 @@ class AccountSettings( } } - fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) { + override fun ephemeralChatList() = backupEphemeralChatList + + override fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) { if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return // Events might be different objects, we have to compare their ids. 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 f030b6898..fed602b28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.model.privateChats.ChatroomList @@ -459,7 +459,7 @@ object LocalCache : ILocalCache, ICacheProvider { fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = addressables.getOrCreate(key) { AddressableNote(key) } - fun getOrCreateAddressableNote(key: Address): AddressableNote { + override fun getOrCreateAddressableNote(key: Address): AddressableNote { val note = getOrCreateAddressableNoteInternal(key) // Loads the user outside a Syncronized block to avoid blocking if (note.author == null) { @@ -2717,7 +2717,7 @@ object LocalCache : ILocalCache, ICacheProvider { } } - fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) + override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) fun justConsume( event: Event, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index c251e230c..f71d0b6f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.ChannelState +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index 56781d1f7..07a032201 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index f00993da3..9c6094653 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt index 884155614..a9c496d8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt index 86c876a9e..1e4f9f3bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt index 07f98233b..e44356eef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index c13bd4497..7de27e8b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 7e0496b7c..7ad81ba07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -25,14 +25,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 374c68d92..94deb91a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.navigation.routes +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index ca5886d93..8e3dd0add 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -32,13 +32,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index aa5928c7f..84cb7b979 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -53,9 +53,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 8e1c4bcc4..fcfa5b4d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -40,6 +40,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account @@ -50,9 +53,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index 28bba13f2..98f043e84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index 2eb7bb5a7..487f997df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt index f6cde11b5..e3d8e9f9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt index 78aa37ce2..58d908227 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt index 256c3862a..c59ba6e59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt index 84988cf43..dfed4a693 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt index eef197437..165d00e26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt index 3fded09c0..3eaa0234a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 2a8adefca..de34a1df2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -30,8 +30,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt index b7c943c69..18c70810f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt index 7a529fd86..8480f0073 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt index bcc891f4e..7ae747123 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 32fcefd1f..2bbeec845 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt index 403881bd0..4b80c9cf2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt index ea4ff20c2..79a8133eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 89f03d6b4..e149d8380 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -30,8 +30,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index e6946e763..5b0d32f43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt index 3279af82d..de1904944 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index 2ba8c3ca0..ad7702f91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index 22106578a..70d569541 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt index 37e17b278..78bbf142c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt index 230a8b4f5..d9964f55e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt index 10a623343..49a788879 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt index abfd1b7ce..d57fb31ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt index 1a95f0364..6404c5074 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt index 57013ff8a..f9e74b35a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index 8407ceb71..67cb04ab8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 7c9850706..e2260f14f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -31,9 +31,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 5c14da323..8176b91d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt index 98167d056..9a6106b87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelInfo import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt index b9fe94e32..8bb6e64af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt index b2eeca605..056a59a8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 97fa81a62..9ad0b1e88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -40,9 +40,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index b66031baa..4b6b8fb9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 3e7b79bd3..ea40fa5fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 2bec459d6..20c5b3f55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -34,15 +34,15 @@ import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 86d975767..3bc08c640 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -41,11 +41,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 0d4c93a03..cb8deb029 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index e0bff36ad..7c55ca565 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -55,11 +55,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.AROUND_ME -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 00f2a0e6e..0e165c931 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index cd7cb3030..b49297f35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt index 4a7886545..af28d1708 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt index 7e85e3504..556df134f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor 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 b62cae5c8..721fdc6f8 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 @@ -20,9 +20,12 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey /** @@ -81,7 +84,16 @@ interface ICacheProvider { * @param hexKey The note's ID in hex format * @return The Note (existing or newly created) */ - fun checkGetOrCreateNote(hexKey: HexKey): Any? + fun checkGetOrCreateNote(hexKey: HexKey): Note? + + /** + * Gets an existing AddressableNote or creates a new one if it doesn't exist. + * Used by ThreadAssembler for building thread structures. + * + * @param address The note's ID in address format + * @return The AddressableNote (existing or newly created) + */ + fun getOrCreateAddressableNote(key: Address): AddressableNote /** * Gets the event stream for cache updates. @@ -99,4 +111,6 @@ interface ICacheProvider { * @return true if the event has been deleted, false otherwise */ fun hasBeenDeleted(event: Any): Boolean + + fun justConsumeMyOwnEvent(event: Event): Boolean } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatChannel.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatChannel.kt index 99f001fca..ac8548a97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatChannel.kt @@ -18,7 +18,7 @@ * 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.emphChat +package com.vitorpamplona.amethyst.commons.model.emphChat import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListDecryptionCache.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListDecryptionCache.kt index 60ff2f80f..c5f48b760 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.emphChat +package com.vitorpamplona.amethyst.commons.model.emphChat import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt index 3899d0aef..cbdde212f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt @@ -18,12 +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.emphChat +package com.vitorpamplona.amethyst.commons.model.emphChat -import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner @@ -41,12 +40,18 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +interface EphemeralChatRepository { + fun ephemeralChatList(): EphemeralChatListEvent? + + fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) +} + class EphemeralChatListState( val signer: NostrSigner, - val cache: LocalCache, + val cache: ICacheProvider, val decryptionCache: EphemeralChatListDecryptionCache, val scope: CoroutineScope, - val settings: AccountSettings, + val settings: EphemeralChatRepository, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self val ephemeralChatListNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress()) @@ -58,7 +63,7 @@ class EphemeralChatListState( fun getEphemeralChatList(): EphemeralChatListEvent? = ephemeralChatListNote.event as? EphemeralChatListEvent suspend fun ephemeralChatListWithBackup(note: Note): Set { - val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList + val event = note.event as? EphemeralChatListEvent ?: settings.ephemeralChatList() return event?.let { decryptionCache.roomSet(it) } ?: emptySet() } @@ -109,11 +114,11 @@ class EphemeralChatListState( } init { - settings.backupEphemeralChatList?.let { event -> + settings.ephemeralChatList()?.let { event -> Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) + cache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt index 2330dd92c..3e9b04b38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt @@ -18,13 +18,13 @@ * 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.nip28PublicChats +package com.vitorpamplona.amethyst.commons.model.nip28PublicChats import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt index a0a54ff93..4ba77febf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.nip28PublicChats +package com.vitorpamplona.amethyst.commons.model.nip28PublicChats import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt index f52bd4687..4ac2acd91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt @@ -18,12 +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.nip28PublicChats +package com.vitorpamplona.amethyst.commons.model.nip28PublicChats -import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent @@ -43,12 +42,18 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +interface PublicChatListRepository { + fun channelList(): ChannelListEvent? + + fun updateChannelListTo(newChannelList: ChannelListEvent?) +} + class PublicChatListState( val signer: NostrSigner, - val cache: LocalCache, + val cache: ICacheProvider, val decryptionCache: PublicChatListDecryptionCache, val scope: CoroutineScope, - val settings: AccountSettings, + val settings: PublicChatListRepository, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress()) @@ -60,7 +65,7 @@ class PublicChatListState( fun getChannelList(): ChannelListEvent? = publicChatListNote.event as? ChannelListEvent suspend fun publicChatListWithBackup(note: Note): Set { - val event = note.event as? ChannelListEvent ?: settings.backupChannelList + val event = note.event as? ChannelListEvent ?: settings.channelList() return event?.let { decryptionCache.channelSet(it) } ?: emptySet() } @@ -124,11 +129,11 @@ class PublicChatListState( } init { - settings.backupChannelList?.let { event -> + settings.channelList()?.let { event -> Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) + cache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 834e2437f..415e9f255 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -18,7 +18,7 @@ * 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.nip38UserStatuses +package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt index df732bd66..a02287264 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -18,13 +18,13 @@ * 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.nip53LiveActivities +package com.vitorpamplona.amethyst.commons.model.nip53LiveActivities import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress From afe66cd55fef527cce58d6caac9a4edfbda2cd12 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:31:04 -0500 Subject: [PATCH 30/47] Moves user actions to commons --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../commons/model/nip38UserStatuses/UserStatusAction.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index fc51b78cc..16575da67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache @@ -50,7 +51,6 @@ import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 415e9f255..725cb6c1a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent From 9b7a83796b105c11dbbf4b2b704e229287c5b76c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:33:24 -0500 Subject: [PATCH 31/47] Moves private chatroom models to commons --- .../com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- .../ui/screen/loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../commons}/model/privateChats/Chatroom.kt | 13 +++++++------ .../commons}/model/privateChats/ChatroomList.kt | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/privateChats/Chatroom.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/privateChats/ChatroomList.kt (95%) 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 fed602b28..a636b1068 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -28,11 +28,11 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag -import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.dateFormatter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 0bf7a6cb9..2b0bb78fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.IEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt index 3850b3116..5485bf5bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt @@ -18,14 +18,14 @@ * 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.privateChats +package com.vitorpamplona.amethyst.commons.model.privateChats import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.Channel.Companion.DefaultFeedOrder import com.vitorpamplona.amethyst.commons.model.ListChange -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NotesGatherer -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NotesGatherer +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject @@ -34,6 +34,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import java.lang.ref.WeakReference +import kotlin.collections.plus @Stable class Chatroom : NotesGatherer { @@ -66,7 +67,7 @@ class Chatroom : NotesGatherer { msg.author?.let { author -> if (author !in activeSenders) { - activeSenders += author + activeSenders + author } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatroomList.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatroomList.kt index 6a81dcf37..40ba62fa1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatroomList.kt @@ -18,10 +18,10 @@ * 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.privateChats +package com.vitorpamplona.amethyst.commons.model.privateChats -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable From 7c1053df2dd779180dddf87eea060d6f3a6f6f62 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:37:32 -0500 Subject: [PATCH 32/47] Moves reports to common Adds support for additional information --- .../java/com/vitorpamplona/amethyst/model/Account.kt | 5 +++-- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 3 ++- .../commons}/model/nip56Reports/ReportAction.kt | 11 ++++++----- .../vitorpamplona/quartz/nip56Reports/ReportEvent.kt | 6 ++++-- 4 files changed, 15 insertions(+), 10 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip56Reports/ReportAction.kt (88%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 16575da67..aff2f7ea1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction +import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache @@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListD import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState -import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState @@ -571,7 +571,8 @@ class Account( suspend fun report( user: User, type: ReportType, - ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer)) + content: String = "", + ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, content, userProfile(), signer)) suspend fun delete(note: Note) = delete(listOf(note)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index fcfa5b4d6..fffbe41a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -701,9 +701,10 @@ class AccountViewModel( fun report( user: User, type: ReportType, + content: String = "", ) { launchSigner { - account.report(user, type) + account.report(user, type, content) account.hideUser(user.pubkeyHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/ReportAction.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/ReportAction.kt index 1dfc00f11..ffd1c21c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/ReportAction.kt @@ -18,10 +18,10 @@ * 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.nip56Reports +package com.vitorpamplona.amethyst.commons.model.nip56Reports -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -31,6 +31,7 @@ class ReportAction { suspend fun report( user: User, type: ReportType, + content: String = "", by: User, signer: NostrSigner, ): ReportEvent? { @@ -39,7 +40,7 @@ class ReportAction { return null } - val template = ReportEvent.build(user.pubkeyHex, type) + val template = ReportEvent.build(user.pubkeyHex, type, content) return signer.sign(template) } @@ -57,7 +58,7 @@ class ReportAction { } return note.event?.let { - signer.sign(ReportEvent.build(it, type)) + signer.sign(ReportEvent.build(it, type, content)) } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index f1f601c64..7a72281ff 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -77,8 +77,9 @@ class ReportEvent( fun build( reportedPost: Event, type: ReportType, + comment: String = "", createdAt: Long = TimeUtils.now(), - ) = eventTemplate(KIND, "", createdAt) { + ) = eventTemplate(KIND, comment, createdAt) { alt(ALT_PREFIX + type.code) event(reportedPost.id, type) user(reportedPost.pubKey, type) @@ -91,8 +92,9 @@ class ReportEvent( fun build( reportedUser: HexKey, type: ReportType, + comment: String = "", createdAt: Long = TimeUtils.now(), - ) = eventTemplate(KIND, "", createdAt) { + ) = eventTemplate(KIND, comment, createdAt) { alt(ALT_PREFIX + type.code) user(reportedUser, type) } From 42bdd1c831146ba34b54f06abd819be4bb97023e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:57:29 -0500 Subject: [PATCH 33/47] Emoji State to commons --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../emojiSuggestions/EmojiSuggestionState.kt | 4 +- .../ShowEmojiSuggestionList.kt | 38 +++++++++---------- .../nip22Comments/CommentPostViewModel.kt | 10 ++--- .../nip22Comments/GenericCommentPostScreen.kt | 1 - .../privateDM/send/ChatNewMessageViewModel.kt | 10 ++--- .../chats/privateDM/send/NewGroupDMScreen.kt | 1 - .../send/PrivateMessageEditFieldRow.kt | 1 - .../send/ChannelNewMessageViewModel.kt | 10 ++--- .../chats/publicChannels/send/EditFieldRow.kt | 1 - .../nip99Classifieds/NewProductScreen.kt | 1 - .../nip99Classifieds/NewProductViewModel.kt | 10 ++--- .../loggedIn/home/ShortNotePostScreen.kt | 1 - .../loggedIn/home/ShortNotePostViewModel.kt | 10 ++--- .../publicMessages/NewPublicMessageScreen.kt | 1 - .../NewPublicMessageViewModel.kt | 10 ++--- .../model/nip30CustomEmojis/EmojiPackState.kt | 15 ++++---- 17 files changed, 59 insertions(+), 67 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip30CustomEmojis/EmojiPackState.kt (93%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index aff2f7ea1..9b08ad91e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt index c3af93f09..f49a10eb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt @@ -20,14 +20,16 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn +@Stable class EmojiSuggestionState( val account: Account, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt index fd81456ac..a788a0488 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.heightIn @@ -43,10 +42,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -57,7 +55,6 @@ fun ShowEmojiSuggestionList( emojiSuggestions: EmojiSuggestionState, onSelect: (EmojiPackState.EmojiMedia) -> Unit, onFullSize: (EmojiPackState.EmojiMedia) -> Unit, - accountViewModel: AccountViewModel, modifier: Modifier = Modifier.heightIn(0.dp, 200.dp), ) { val suggestions by emojiSuggestions.results.collectAsStateWithLifecycle(emptyList()) @@ -79,22 +76,23 @@ fun ShowEmojiSuggestionList( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(Size10dp), ) { - Box(Size40Modifier) { - UrlImageView(it.link, accountViewModel) - } + AsyncImage( + it.link, + contentDescription = it.code, + modifier = Size40Modifier, + ) Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) - Box(Size40Modifier, contentAlignment = Alignment.Center) { - IconButton( - onClick = { - onFullSize(it) - }, - ) { - Icon( - imageVector = Icons.Outlined.OpenInFull, - contentDescription = stringRes(R.string.use_direct_url), - modifier = Modifier.size(20.dp), - ) - } + IconButton( + modifier = Size40Modifier, + onClick = { + onFullSize(it) + }, + ) { + Icon( + imageVector = Icons.Outlined.OpenInFull, + contentDescription = stringRes(R.string.use_direct_url), + modifier = Modifier.size(20.dp), + ) } } HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 1a99000a7..0b634d26a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator @@ -435,7 +435,7 @@ open class CommentPostViewModel : myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag( it.code, - it.link.url, + it.link, ) } } @@ -634,11 +634,11 @@ open class CommentPostViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 6f5d1e758..74c923e82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -357,7 +357,6 @@ private fun GenericCommentPostBody( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, modifier = Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index c2ce04d5e..eabc0d274 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -32,11 +32,11 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -519,7 +519,7 @@ class ChatNewMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -659,11 +659,11 @@ class ChatNewMessageViewModel : } fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index ab72a1d18..ef9b3c7fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -311,7 +311,6 @@ fun GroupDMScreenContent( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 34f637ef7..bf168bc2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -127,7 +127,6 @@ fun PrivateMessageEditFieldRow( it, channelScreenModel::autocompleteWithEmoji, channelScreenModel::autocompleteWithEmojiUrl, - accountViewModel, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 20c5b3f55..d0b2de1ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -36,13 +36,13 @@ import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator @@ -484,7 +484,7 @@ open class ChannelNewMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -575,11 +575,11 @@ open class ChannelNewMessageViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 6cc1c9370..c03285025 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -104,7 +104,6 @@ fun EditFieldRow( it, channelScreenModel::autocompleteWithEmoji, channelScreenModel::autocompleteWithEmojiUrl, - accountViewModel, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 4128694c3..8a92c7f27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -327,7 +327,6 @@ private fun NewProductBody( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, modifier = Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 90bf27713..af0b07c96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator @@ -365,7 +365,7 @@ open class NewProductViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -534,11 +534,11 @@ open class NewProductViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaDescription.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaDescription.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 65a2b051d..4318f13c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -454,7 +454,6 @@ private fun NewPostScreenBody( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, modifier = Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 538057970..90ee62b3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor @@ -719,7 +719,7 @@ open class ShortNotePostViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -929,11 +929,11 @@ open class ShortNotePostViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 1643a6798..629a97cba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -284,7 +284,6 @@ fun PublicMessageScreenContent( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 19ba54057..b44ac347b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator @@ -385,7 +385,7 @@ class NewPublicMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -581,11 +581,11 @@ class NewPublicMessageViewModel : } fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt index 52d0f1aee..d7d914594 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt @@ -18,12 +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.nip30CustomEmojis +package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis -import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent @@ -43,12 +42,12 @@ import kotlinx.coroutines.flow.transformLatest class EmojiPackState( val signer: NostrSigner, - val cache: LocalCache, + val cache: ICacheProvider, val scope: CoroutineScope, ) { class EmojiMedia( val code: String, - val link: MediaUrlImage, + val link: String, ) // Creates a long-term reference for this note so that the GC doesn't collect the note it self @@ -84,7 +83,7 @@ class EmojiPackState( fun convertEmojiPack(pack: EmojiPackEvent): List = pack.taggedEmojis().map { - EmojiMedia(it.code, MediaUrlImage(it.url)) + EmojiMedia(it.code, it.url) } fun mergePack(list: Array): List = From 33c51b9a103e4edfd76b83ed641af8cd14e33c7a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 13 Jan 2026 20:00:04 +0000 Subject: [PATCH 34/47] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 12 ++++++------ amethyst/src/main/res/values-de-rDE/strings.xml | 11 +++++------ amethyst/src/main/res/values-pt-rBR/strings.xml | 12 ++++++------ amethyst/src/main/res/values-sv-rSE/strings.xml | 11 +++++------ 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 6c1aa14d8..e97ff5381 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -172,6 +172,12 @@ Neočekávaný stav nahrávání NIP-95 zatím není pro hlasové zprávy podporován Nahrávání hlasu selhalo: %1$s + Žádný + Hluboký + Vysoký + Neutrální + Anonymizovat + Upravuje výšku vašeho hlasu. Poznámka: základní změny výšky hlasu mohou být odhodlanými posluchači potenciálně zpětně rozpoznány. Uživatel nemá nastavenou LN adresu pro přijímání sats "Odpověď zde…" Zkopíruje ID poznámky do schránky pro sdílení @@ -1228,10 +1234,4 @@ Smazat seznam Smazat sadu doporučení Typy - Žádný - Hluboký - Vysoký - Neutrální - Anonymizovat - Upravuje výšku vašeho hlasu. Poznámka: základní změny výšky hlasu mohou být odhodlanými posluchači potenciálně zpětně rozpoznány. diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 586128b78..e2cfb90cb 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -174,6 +174,11 @@ erie gespeichert Unerwarteter Upload-Status NIP-95 wird für Sprachnachrichten noch nicht unterstützt Sprach-Upload fehlgeschlagen: %1$s + Keine + Tief + Hoch + Anonymisieren + Verändert die Tonhöhe deiner Stimme. Hinweis: einfache Tonhöhenänderungen können von entschlossenen Zuhörern möglicherweise rückgängig gemacht werden. Der Benutzer hat keine Lightning-Adresse eingerichtet, um Sats zu empfangen "Hier antworten…" Kopiert die Notiz-ID zum Teilen in die Zwischenablage @@ -1233,10 +1238,4 @@ anz der Bedingungen ist erforderlich Liste löschen Empfehlungspaket löschen Typen - Keine - Tief - Hoch - Neutral - Anonymisieren - Verändert die Tonhöhe deiner Stimme. Hinweis: einfache Tonhöhenänderungen können von entschlossenen Zuhörern möglicherweise rückgängig gemacht werden. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 97ff4065d..a90421341 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -172,6 +172,12 @@ Estado de upload inesperado O NIP-95 ainda não é suportado para mensagens de voz Falha no upload de voz: %1$s + Nenhum + Grave + Agudo + Neutro + Anonimizar + Altera o tom da sua voz. Nota: alterações básicas de tom podem potencialmente ser revertidas por ouvintes determinados. Usuário não tem um endereço lightning configurado para receber sats "responda aqui.. " Copia o ID do canal (note) para compartilhar @@ -1228,10 +1234,4 @@ Excluir lista Excluir pacote de recomendações Tipos - Nenhum - Grave - Agudo - Neutro - Anonimizar - Altera o tom da sua voz. Nota: alterações básicas de tom podem potencialmente ser revertidas por ouvintes determinados. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index de6dae781..8a89c5993 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -172,6 +172,11 @@ Oväntat uppladdningstillstånd NIP-95 stöds ännu inte för röstmeddelanden Uppladdning av röst misslyckades: %1$s + Ingen + Djup + Hög + Anonymisera + Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. Användaren har inte en Lightningadressinställning för att ta emot sats "svara här.. " Kopierar antecknings-ID till urklipp för delning @@ -1227,10 +1232,4 @@ Ta bort lista Ta bort rekommendationspaket Typer - Ingen - Djup - Hög - Neutral - Anonymisera - Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. From ba5c86fbba549a1a55634a306f9cdac73669233b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 15:31:26 -0500 Subject: [PATCH 35/47] Migrates rich text parser from jvm to commons --- .../amethyst/service/images/Base64Fetcher.kt | 2 +- .../commons/base64Image/Base64ImageBitmap.kt | 1 + .../ExpandableTextCutOffCalculator.kt | 0 .../commons/richtext/GalleryParser.kt | 0 .../commons/richtext/MediaContentModels.kt | 0 .../amethyst/commons/richtext/Patterns.kt | 15 ++-- .../commons/richtext/RichTextParser.kt | 70 +++++++++---------- .../richtext/RichTextParserSegments.kt | 0 .../commons/base64Image/Base64Image.kt | 36 ---------- .../base64Image/Base64ImagePlatform.jvm.kt | 1 + 10 files changed, 47 insertions(+), 78 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt (86%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt (88%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt (100%) delete mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt index bbab146da..41a89a151 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt @@ -30,8 +30,8 @@ import coil3.fetch.Fetcher import coil3.fetch.ImageFetchResult import coil3.key.Keyer import coil3.request.Options -import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.base64Image.toBitmap +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt index 9340c5d08..c5b8f2ca3 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt @@ -24,6 +24,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.util.Base64 fun Base64Image.toBitmap(content: String): Bitmap { diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt similarity index 86% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt index 5e9ddefbb..b54a9c760 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import java.util.regex.Pattern - /** * Pattern constants for email and phone validation. * These replace android.util.Patterns for KMP compatibility. @@ -30,16 +28,21 @@ object Patterns { /** * Email address pattern from RFC 5322... From android.util.Patterns. */ - val EMAIL_ADDRESS: Pattern = - Pattern.compile( + val EMAIL_ADDRESS: Regex = + Regex( "[a-zA-Z0-9+._%-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}(\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25})+", ) /** * Phone number pattern - matches common phone formats. */ - val PHONE: Pattern = - Pattern.compile( + val PHONE: Regex = + Regex( "^[+]?[(]?[0-9]{1,4}[)]?[-\\s./0-9]*\$", ) + + val BASE64_IMAGE: Regex = + Regex( + "data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|")});base64,([a-zA-Z0-9+/]+={0,2})", + ) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt similarity index 88% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 243a07957..f51662962 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.commons.richtext import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions -import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists @@ -45,8 +44,8 @@ import kotlinx.collections.immutable.toPersistentList import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL -import java.util.regex.Pattern import kotlin.coroutines.cancellation.CancellationException +import kotlin.text.iterator class RichTextParser { fun createMediaContent( @@ -108,7 +107,7 @@ class RichTextParser { return urls.mapNotNullTo(LinkedHashSet(urls.size)) { if (it.originalUrl.contains("@")) { - if (Patterns.EMAIL_ADDRESS.matcher(it.originalUrl).matches()) { + if (Patterns.EMAIL_ADDRESS.matches(it.originalUrl)) { null } else { it.originalUrl @@ -201,7 +200,7 @@ class RichTextParser { }.toImmutableList() } - private fun isNumber(word: String) = numberPattern.matcher(word).matches() + private fun isNumber(word: String) = numberPattern.matches(word) private fun isPhoneNumberChar(c: Char): Boolean = when (c) { @@ -225,7 +224,7 @@ class RichTextParser { return isPotentialNumber } - fun isDate(word: String): Boolean = shortDatePattern.matcher(word).matches() || longDatePattern.matcher(word).matches() + fun isDate(word: String): Boolean = shortDatePattern.matches(word) || longDatePattern.matches(word) private fun isArabic(text: String): Boolean = text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } @@ -239,7 +238,9 @@ class RichTextParser { ): Segment { if (word.isEmpty()) return RegularTextSegment(word) - if (word.startsWith("data:image/") && Base64Image.isBase64(word)) return Base64Segment(word) + if (word.startsWith("data:image/")) { + if (Patterns.BASE64_IMAGE.matches(word)) return Base64Segment(word) + } if (images.contains(word)) return ImageSegment(word) @@ -260,25 +261,22 @@ class RichTextParser { if (EmojiCoder.isCoded(word)) return SecretEmoji(word) if (word.contains("@")) { - if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) return EmailSegment(word) + if (Patterns.EMAIL_ADDRESS.matches(word)) return EmailSegment(word) } if (startsWithNIP19Scheme(word)) return BechSegment(word) if (isPotentialPhoneNumber(word) && !isDate(word)) { - if (Patterns.PHONE.matcher(word).matches()) return PhoneSegment(word) + if (Patterns.PHONE.matches(word)) return PhoneSegment(word) } val indexOfPeriod = word.indexOf(".") if (indexOfPeriod > 0 && indexOfPeriod < word.length - 1) { // periods cannot be the last one - val schemelessMatcher = noProtocolUrlValidator.matcher(word) - if (schemelessMatcher.find()) { - val url = schemelessMatcher.group(1) // url - val additionalChars = schemelessMatcher.group(4).ifEmpty { null } // additional chars - val pattern = - """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" - .toRegex(RegexOption.IGNORE_CASE) - if (pattern.find(word) != null && url != null) { + val schemelessMatcher = noProtocolUrlValidator.find(word) + if (schemelessMatcher != null) { + val url = schemelessMatcher.groups[1]?.value // url + val additionalChars = schemelessMatcher.groups[4]?.value?.ifEmpty { null } // additional chars + if (additionalUrlSchema.find(word) != null && url != null) { return SchemelessUrlSegment(word, url, additionalChars) } } @@ -292,12 +290,11 @@ class RichTextParser { tags: ImmutableListOfLists, ): Segment { // First #[n] - - val matcher = tagIndex.matcher(word) try { - if (matcher.find()) { - val index = matcher.group(1)?.toInt() - val suffix = matcher.group(2) + val matcher = tagIndex.find(word) + if (matcher != null) { + val index = matcher.groups[1]?.value?.toInt() + val suffix = matcher.groups[2]?.value if (index != null && index >= 0 && index < tags.lists.size) { val tag = tags.lists[index] @@ -317,13 +314,12 @@ class RichTextParser { } // Second #Amethyst - val hashtagMatcher = hashTagsPattern.matcher(word) - try { - if (hashtagMatcher.find()) { - val hashtag = hashtagMatcher.group(1) + val hashtagMatcher = hashTagsPattern.find(word) + if (hashtagMatcher != null) { + val hashtag = hashtagMatcher.groups[1]?.value if (hashtag != null) { - return HashTagSegment(word, hashtag, hashtagMatcher.group(2).ifEmpty { null }) + return HashTagSegment(word, hashtag, hashtagMatcher.groups[2]?.value?.ifEmpty { null }) } } } catch (e: Exception) { @@ -335,22 +331,26 @@ class RichTextParser { } companion object { - val longDatePattern: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$") - val shortDatePattern: Pattern = Pattern.compile("^\\d{2}-\\d{2}-\\d{2}$") - val numberPattern: Pattern = Pattern.compile("^(-?[\\d.]+)([a-zA-Z%]*)$") + val longDatePattern: Regex = Regex("^\\d{4}-\\d{2}-\\d{2}$") + val shortDatePattern: Regex = Regex("^\\d{2}-\\d{2}-\\d{2}$") + val numberPattern: Regex = Regex("^(-?[\\d.]+)([a-zA-Z%]*)$") // Android9 seems to have an issue starting this regex. val noProtocolUrlValidator = try { - Pattern.compile( + Regex( "(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)", ) } catch (e: Exception) { - Pattern.compile( + Regex( "(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)", ) } + val additionalUrlSchema = + """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" + .toRegex(RegexOption.IGNORE_CASE) + val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?" .toRegex(RegexOption.IGNORE_CASE) @@ -361,9 +361,9 @@ class RichTextParser { val imageExtensions = imageExt + imageExt.map { it.uppercase() } val videoExtensions = videoExt + videoExt.map { it.uppercase() } - val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)") - val hashTagsPattern: Pattern = - Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) + val tagIndex = Regex("\\#\\[([0-9]+)\\](.*)") + val hashTagsPattern: Regex = + Regex("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", RegexOption.IGNORE_CASE) val acceptedNIP19schemes = listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed") + @@ -436,6 +436,6 @@ class RichTextParser { } } - fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matcher(url).matches() + fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matches(url) } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt deleted file mode 100644 index ac7bef89c..000000000 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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.base64Image - -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import java.util.regex.Pattern - -object Base64Image { - val pattern: Pattern = - Pattern.compile( - "data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|")});base64,([a-zA-Z0-9+/]+={0,2})", - ) - - fun isBase64(content: String): Boolean { - val matcher = pattern.matcher(content) - return matcher.find() - } -} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt index 6c523a33e..50fc059a2 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.base64Image import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.io.ByteArrayInputStream import java.util.Base64 import javax.imageio.ImageIO From 73fe985a65e38dc1da71b0699267d3a597c0f148 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 15:49:08 -0500 Subject: [PATCH 36/47] Fixes tests --- .../commons/base64Image/Base64ImageBitmap.kt | 12 +----- .../amethyst/commons/richtext/Base64Image.kt | 41 +++++++++++++++++++ .../base64Image/Base64ImagePlatform.jvm.kt | 17 +++----- 3 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt index c5b8f2ca3..fc7a2c232 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt @@ -25,18 +25,10 @@ import android.graphics.BitmapFactory import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage import com.vitorpamplona.amethyst.commons.richtext.Base64Image -import java.util.Base64 fun Base64Image.toBitmap(content: String): Bitmap { - val matcher = pattern.matcher(content) - - if (matcher.find()) { - val base64String = matcher.group(2) - val byteArray = Base64.getDecoder().decode(base64String) - return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) - } - - throw Exception("Unable to convert base64 to image $content") + val byteArray = parse(content) + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) } /** diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt new file mode 100644 index 000000000..b588ea572 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt @@ -0,0 +1,41 @@ +/** + * 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.richtext + +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import java.util.Base64 + +object Base64Image { + val pattern = Patterns.BASE64_IMAGE + + fun isBase64(content: String): Boolean = Patterns.BASE64_IMAGE.matches(content) + + fun parse(content: String): ByteArray { + val matcher = pattern.find(content) + if (matcher != null) { + val base64String = matcher.groups[2]?.value + val byteArray = Base64.getDecoder().decode(base64String) + return byteArray + } + + throw Exception("Unable to convert base64 to image $content") + } +} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt index 50fc059a2..a5d23e969 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt @@ -24,23 +24,16 @@ import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.io.ByteArrayInputStream -import java.util.Base64 import javax.imageio.ImageIO /** * Converts a base64 image data URI to a PlatformImage (BufferedImage wrapper). */ fun Base64Image.toPlatformImage(content: String): PlatformImage { - val matcher = pattern.matcher(content) + val byteArray = parse(content) - if (matcher.find()) { - val base64String = matcher.group(2) - val byteArray = Base64.getDecoder().decode(base64String) - val bufferedImage = - ImageIO.read(ByteArrayInputStream(byteArray)) - ?: throw Exception("Unable to decode base64 image: $content") - return bufferedImage.toPlatformImage() - } - - throw Exception("Unable to convert base64 to image $content") + val bufferedImage = + ImageIO.read(ByteArrayInputStream(byteArray)) + ?: throw Exception("Unable to decode base64 image: $content") + return bufferedImage.toPlatformImage() } From 09d0f4425cecb43fe43320d9fb9c9c5debb23355 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 16:04:02 -0500 Subject: [PATCH 37/47] Moves some of the stuff that was saved in Commons back to the desktop app because it is only used there --- .../com/vitorpamplona/amethyst/desktop/Main.kt | 8 ++++---- .../amethyst/desktop}/account/AccountManager.kt | 7 ++++--- .../network/DesktopRelayConnectionManager.kt | 2 +- .../desktop}/network/RelayConnectionManager.kt | 4 ++-- .../amethyst/desktop}/network/RelayStatus.kt | 2 +- .../desktop}/subscriptions/FeedSubscription.kt | 2 +- .../desktop}/subscriptions/FilterBuilders.kt | 2 +- .../desktop}/subscriptions/ProfileSubscription.kt | 2 +- .../desktop}/subscriptions/SubscriptionUtils.kt | 4 ++-- .../amethyst/desktop/ui/ComposeNoteDialog.kt | 2 +- .../amethyst/desktop/ui/DevSettingsSection.kt | 2 +- .../amethyst/desktop/ui}/EventExtensions.kt | 4 ++-- .../amethyst/desktop/ui/FeedScreen.kt | 15 +++++++-------- .../amethyst/desktop/ui/LoginScreen.kt | 8 ++++---- .../amethyst/desktop/ui/NoteActions.kt | 2 +- .../amethyst/desktop/ui/NotificationsScreen.kt | 6 +++--- .../amethyst/desktop/ui/ThreadScreen.kt | 11 +++++------ .../amethyst/desktop/ui/UserProfileScreen.kt | 10 +++++----- .../amethyst/desktop}/ui/auth/KeyInputField.kt | 4 ++-- .../amethyst/desktop}/ui/auth/LoginCard.kt | 4 ++-- .../desktop}/ui/auth/NewKeyWarningCard.kt | 4 ++-- .../amethyst/desktop}/ui/note/NoteCard.kt | 2 +- .../desktop}/ui/profile/ProfileInfoCard.kt | 2 +- .../amethyst/desktop}/ui/relay/RelayStatusCard.kt | 4 ++-- .../network/DesktopRelayConnectionManagerTest.kt | 2 +- 25 files changed, 57 insertions(+), 58 deletions(-) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/account/AccountManager.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/network/RelayConnectionManager.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/network/RelayStatus.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/FeedSubscription.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/FilterBuilders.kt (99%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/ProfileSubscription.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/SubscriptionUtils.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui}/EventExtensions.kt (94%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/auth/KeyInputField.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/auth/LoginCard.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/auth/NewKeyWarningCard.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/note/NoteCard.kt (99%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/profile/ProfileInfoCard.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/relay/RelayStatusCard.kt (97%) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 86b01d92d..7493784ee 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -73,12 +73,10 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -import com.vitorpamplona.amethyst.commons.account.AccountManager -import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.ui.profile.ProfileInfoCard -import com.vitorpamplona.amethyst.commons.ui.relay.RelayStatusCard import com.vitorpamplona.amethyst.commons.ui.screens.MessagesPlaceholder import com.vitorpamplona.amethyst.commons.ui.screens.SearchPlaceholder +import com.vitorpamplona.amethyst.desktop.account.AccountManager +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog import com.vitorpamplona.amethyst.desktop.ui.FeedScreen @@ -86,6 +84,8 @@ import com.vitorpamplona.amethyst.desktop.ui.LoginScreen import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen import com.vitorpamplona.amethyst.desktop.ui.ThreadScreen import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen +import com.vitorpamplona.amethyst.desktop.ui.profile.ProfileInfoCard +import com.vitorpamplona.amethyst.desktop.ui.relay.RelayStatusCard import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt index a2cffbd11..60c902d78 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt @@ -18,7 +18,7 @@ * 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.account +package com.vitorpamplona.amethyst.desktop.account import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNsec import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import java.io.File sealed class AccountState { data object LoggedOut : AccountState() @@ -221,8 +222,8 @@ class AccountManager private constructor( getPrefsFile().delete() } - private fun getPrefsFile(): java.io.File { + private fun getPrefsFile(): File { val homeDir = System.getProperty("user.home") - return java.io.File(homeDir, ".amethyst/last_account.txt") + return File(homeDir, ".amethyst/last_account.txt") } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt index 34dd8db54..18e7a9873 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.desktop.network -import com.vitorpamplona.amethyst.commons.network.RelayConnectionManager +import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket /** diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt index 3e0f7159d..8ef7207a2 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt @@ -18,7 +18,7 @@ * 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.network +package com.vitorpamplona.amethyst.desktop.network import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -65,7 +65,7 @@ open class RelayConnectionManager( } fun addRelay(url: String): NormalizedRelayUrl? { - val normalized = RelayUrlNormalizer.normalizeOrNull(url) ?: return null + val normalized = RelayUrlNormalizer.Companion.normalizeOrNull(url) ?: return null updateRelayStatus(normalized) { it.copy(connected = false, error = null) } return normalized } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayStatus.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayStatus.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt index f446c7eba..7b485a1ad 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayStatus.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt @@ -18,7 +18,7 @@ * 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.network +package com.vitorpamplona.amethyst.desktop.network import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt index 60852ec49..016d2eff4 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt @@ -18,7 +18,7 @@ * 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.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt similarity index 99% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt index 011d910fa..cd0766e10 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt @@ -18,7 +18,7 @@ * 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.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ProfileSubscription.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ProfileSubscription.kt index 2355c0fac..be45a7911 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ProfileSubscription.kt @@ -18,7 +18,7 @@ * 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.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt index 269901810..c0f592610 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt @@ -18,12 +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.commons.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.network.RelayConnectionManager +import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index aca7e6664..80690495c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -43,8 +43,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.model.nip10TextNotes.PublishAction +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt index 6c25bd22d..16813a366 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt @@ -51,7 +51,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.desktop.account.AccountState import java.awt.Toolkit import java.awt.datatransfer.StringSelection diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt similarity index 94% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt index 0c392b8f8..954864647 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt @@ -18,9 +18,9 @@ * 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.util +package com.vitorpamplona.amethyst.desktop.ui -import com.vitorpamplona.amethyst.commons.ui.note.NoteDisplayData +import com.vitorpamplona.amethyst.desktop.ui.note.NoteDisplayData import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip19Bech32.toNpub diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 00cef6714..79a92ddbe 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -52,17 +52,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.state.EventCollectionState -import com.vitorpamplona.amethyst.commons.subscriptions.FeedMode -import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createFollowingFeedSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState -import com.vitorpamplona.amethyst.commons.ui.note.NoteCard -import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode +import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createFollowingFeedSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createGlobalFeedSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription +import com.vitorpamplona.amethyst.desktop.ui.note.NoteCard import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt index be84b6375..bf32e84b5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt @@ -38,10 +38,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes -import com.vitorpamplona.amethyst.commons.account.AccountManager -import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard -import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard +import com.vitorpamplona.amethyst.desktop.account.AccountManager +import com.vitorpamplona.amethyst.desktop.account.AccountState +import com.vitorpamplona.amethyst.desktop.ui.auth.LoginCard +import com.vitorpamplona.amethyst.desktop.ui.auth.NewKeyWarningCard import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index fcc12c918..2a96f8399 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -39,11 +39,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.coroutines.Dispatchers diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 772f05219..82b75329a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -46,17 +46,17 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Zap import com.vitorpamplona.amethyst.commons.state.EventCollectionState -import com.vitorpamplona.amethyst.commons.subscriptions.createNotificationsSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.util.toTimeAgo +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.createNotificationsSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index 0c469f085..4b6390f42 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -50,16 +50,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.state.EventCollectionState -import com.vitorpamplona.amethyst.commons.subscriptions.createNoteSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createThreadRepliesSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState -import com.vitorpamplona.amethyst.commons.ui.note.NoteCard import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel -import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.createNoteSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createThreadRepliesSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription +import com.vitorpamplona.amethyst.desktop.ui.note.NoteCard import com.vitorpamplona.quartz.nip01Core.core.Event /** diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 475fe55b1..d08286dee 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -56,17 +56,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.model.nip02FollowList.FollowAction import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.state.FollowState -import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createMetadataSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createUserPostsSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createMetadataSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createUserPostsSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt index 21e0c4b94..b71a3077d 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt @@ -18,8 +18,9 @@ * 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.ui.auth +package com.vitorpamplona.amethyst.desktop.ui.auth +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -43,7 +44,6 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes import dev.icerock.moko.resources.compose.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview /** * Text field for entering Nostr keys (nsec or npub) with visibility toggle. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt index f97e9179b..4271baaf7 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt @@ -18,8 +18,9 @@ * 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.ui.auth +package com.vitorpamplona.amethyst.desktop.ui.auth +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -45,7 +46,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes import dev.icerock.moko.resources.compose.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview /** * Login card with Nostr key input field and action buttons. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt index 9952ee4fb..b473580a4 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt @@ -18,8 +18,9 @@ * 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.ui.auth +package com.vitorpamplona.amethyst.desktop.ui.auth +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -38,7 +39,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes import dev.icerock.moko.resources.compose.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview /** * Warning card displayed after generating a new Nostr key pair. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt similarity index 99% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt index 67a70a500..e40940c9a 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt @@ -18,7 +18,7 @@ * 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.ui.note +package com.vitorpamplona.amethyst.desktop.ui.note import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/profile/ProfileInfoCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/ProfileInfoCard.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/profile/ProfileInfoCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/ProfileInfoCard.kt index 53bf73bbc..d6023e4eb 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/profile/ProfileInfoCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/ProfileInfoCard.kt @@ -18,7 +18,7 @@ * 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.ui.profile +package com.vitorpamplona.amethyst.desktop.ui.profile import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/relay/RelayStatusCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/RelayStatusCard.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/relay/RelayStatusCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/RelayStatusCard.kt index c82c5102b..59dafe047 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/relay/RelayStatusCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/RelayStatusCard.kt @@ -18,7 +18,7 @@ * 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.ui.relay +package com.vitorpamplona.amethyst.desktop.ui.relay import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -41,7 +41,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.network.RelayStatus +import com.vitorpamplona.amethyst.desktop.network.RelayStatus /** * Card displaying the status of a Nostr relay connection. diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt index 3075e610a..d77211c0b 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt @@ -44,7 +44,7 @@ class DesktopRelayConnectionManagerTest { fun testRelayConnectionManagerInheritsFromBaseClass() { val manager = DesktopRelayConnectionManager() assertTrue( - manager is com.vitorpamplona.amethyst.commons.network.RelayConnectionManager, + manager is RelayConnectionManager, "DesktopRelayConnectionManager should extend RelayConnectionManager", ) } From 04ac40fa1ae0dfc9bb2323b0c8d85be8085efca8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 16:07:03 -0500 Subject: [PATCH 38/47] moves test as well --- .../amethyst/desktop}/filters/FilterBuildersTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename {commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop}/filters/FilterBuildersTest.kt (98%) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/filters/FilterBuildersTest.kt similarity index 98% rename from commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt rename to desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/filters/FilterBuildersTest.kt index 4afb6e0ea..8392599e2 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/filters/FilterBuildersTest.kt @@ -18,10 +18,10 @@ * 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.filters +package com.vitorpamplona.amethyst.desktop.filters -import com.vitorpamplona.amethyst.commons.subscriptions.FilterBuilders -import com.vitorpamplona.amethyst.commons.subscriptions.buildFilter +import com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders +import com.vitorpamplona.amethyst.desktop.subscriptions.buildFilter import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull From b0210e98f1ed6687ad7395628aec6646695ef23a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 13 Jan 2026 21:08:51 +0000 Subject: [PATCH 39/47] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-sv-rSE/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 8a89c5993..44571cb2a 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -175,6 +175,7 @@ Ingen Djup Hög + Neutral Anonymisera Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. Användaren har inte en Lightningadressinställning för att ta emot sats From be7865d458409541fc010f34cc70e13a46812792 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 17:13:03 -0500 Subject: [PATCH 40/47] updates keyring and vico --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f26e8b51..38e7607b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ firebaseBom = "34.7.0" fragmentKtx = "1.8.9" gms = "4.4.4" jacksonModuleKotlin = "2.20.1" -javaKeyring = "1.0.1" +javaKeyring = "1.0.4" jna = "5.18.1" jtorctl = "0.4.5.7" junit = "4.13.2" @@ -54,7 +54,7 @@ torAndroid = "0.4.8.21.1" translate = "17.0.3" unifiedpush = "3.0.10" urlDetector = "0.1.23" -vico-charts = "2.4.0" +vico-charts = "2.4.1" zelory = "3.0.1" zoomable = "2.9.0" zxing = "3.5.4" From 6097a0b5b7c2f5c8028d21ad6dace9aab51c16ab Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 17:32:38 -0500 Subject: [PATCH 41/47] Removes the need for an extra object for the tag array in the custom emoji nip --- .../amethyst/ui/components/ClickableRoute.kt | 4 ++-- .../amethyst/commons/richtext/RichTextParser.kt | 2 +- .../quartz/nip30CustomEmoji/CustomEmoji.kt | 13 ++++++------- .../quartz/nip30CustomEmoji/Nip30Test.kt | 3 +-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index f0c758bf3..1d4264585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -375,7 +375,7 @@ fun CustomEmojiChecker( onEmojiText: @Composable (ImmutableList) -> Unit, ) { val mayContainEmoji by remember(text, tags) { - mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags)) + mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags?.lists)) } if (mayContainEmoji) { @@ -385,7 +385,7 @@ fun CustomEmojiChecker( } LaunchedEffect(text, tags) { - val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags) + val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags?.lists) if (newEmojiList != null) { emojiList = newEmojiList } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index f51662962..70d90061d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -140,7 +140,7 @@ class RichTextParser { val imageUrls = imagesForPager.filterValues { it is MediaUrlImage }.keys val videoUrls = imagesForPager.filterValues { it is MediaUrlVideo }.keys - val emojiMap = CustomEmoji.createEmojiMap(tags) + val emojiMap = CustomEmoji.createEmojiMap(tags.lists) val segments = findTextSegments(content, imageUrls, videoUrls, urlSet, emojiMap, tags) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt index b7d6a1b14..648e23519 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip30CustomEmoji import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -32,10 +31,10 @@ class CustomEmoji { fun fastMightContainEmoji( input: String, - allTags: ImmutableListOfLists?, + allTags: Array>?, ): Boolean { if (allTags == null) return false - if (allTags.lists.any { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }) return true + if (allTags.any { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }) return true return input.contains(":") } @@ -47,7 +46,7 @@ class CustomEmoji { return input.contains(":") } - fun createEmojiMap(tags: ImmutableListOfLists): Map = tags.lists.filter { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }.associate { ":${it[1]}:" to it[2] } + fun createEmojiMap(tags: Array>): Map = tags.filter { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }.associate { ":${it[1]}:" to it[2] } fun findAllEmojis(input: String): List { val matcher = customEmojiPattern.findAll(input) @@ -67,11 +66,11 @@ class CustomEmoji { fun assembleAnnotatedList( input: String, - allTags: ImmutableListOfLists?, + tags: Array>?, ): ImmutableList? { - if (allTags == null || allTags.lists.isEmpty()) return null + if (tags == null || tags.isEmpty()) return null - val emojiPairs = createEmojiMap(allTags) + val emojiPairs = createEmojiMap(tags) return assembleAnnotatedList(input, emojiPairs) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt index 6d443a2ea..41a8e2b07 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip30CustomEmoji -import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -150,7 +149,7 @@ class Nip30Test { "#ioメシヨソイゲーム\n" + "https://misskey.io/play/9g3qza4jow" - val result = CustomEmoji.assembleAnnotatedList(input, ImmutableListOfLists(tags)) + val result = CustomEmoji.assembleAnnotatedList(input, tags) assertEquals(9, result!!.size) From c41040848ca577308d48576c55328afc9bb4d820 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 09:30:53 -0500 Subject: [PATCH 42/47] Fixes NPE on null existing event. --- .../java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 67b8afb51..0af8d19bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -106,12 +106,11 @@ class AntiSpamFilter { recentAddressables.put(hash, address) } else { // normal event + val existingEvent = recentEventIds[hash] if ( - (recentEventIds[hash] != null && recentEventIds[hash] != event.id) || + (existingEvent != null && existingEvent != event.id) || (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id)) ) { - val existingEvent = recentEventIds[hash] - val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay)) val link2 = njumpLink(NEvent.create(event.id, null, null, relay)) From 54ab70beb77c9498975c61b783e89e79d2ba9d66 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 14:24:57 -0500 Subject: [PATCH 43/47] Fixes https://github.com/vitorpamplona/amethyst/issues/1670 --- fastlane/metadata/android/en-US/full_description.txt | 8 +++++++- fastlane/metadata/android/fr-FR/full_description.txt | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index ea06490eb..011c0e9fd 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1 +1,7 @@ -

Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.

Nostr is an open protocol that is able to create a censorship-resistant global "social" network, transferring notes and other stuff using relays. It doesn't rely on any trusted central server, hence it is resilient; it is based on cryptographic keys and signatures, so it is tamperproof; it does not rely on P2P techniques, therefore it works. \ No newline at end of file +

Amethyst brings the best social network to your Android phone. Just insert your Nostr +private key and start posting.

+

Nostr +is an open protocol that is able to create a censorship-resistant global "social" network, transferring +notes and other stuff using relays. It doesn't rely on any trusted central server, hence it is resilient; +it is based on cryptographic keys and signatures, so it is tamperproof; it does not rely on P2P techniques, +therefore it works.

diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index 1db1b2cf4..eafa7aa1d 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -1 +1,8 @@ -

Amethyst apporte le meilleur réseau social sur votre téléphone Android. Insérez simplement votre clé privée Nostr et commencez à poster.

Nostr est un protocole ouvert qui permet de créer un réseau "social" mondial résistant à la censure, en transférant des notes et d'autres éléments à l'aide de relais. Il ne repose pas sur un serveur central de confiance, ce qui le rend résistant; il est basé sur des clés et des signatures cryptographiques, ce qui le rend inviolable; il ne repose pas sur des techniques P2P, ce qui le rend efficace. \ No newline at end of file +

Amethyst apporte le meilleur réseau social sur votre téléphone Android. +Insérez simplement votre clé privée Nostr et commencez à poster.

+ +

Nostr est un protocole +ouvert qui permet de créer un réseau "social" mondial résistant à la censure, en transférant des +notes et d'autres éléments à l'aide de relais. Il ne repose pas sur un serveur central de confiance, +ce qui le rend résistant; il est basé sur des clés et des signatures cryptographiques, ce qui le rend +inviolable; il ne repose pas sur des techniques P2P, ce qui le rend efficace.

\ No newline at end of file From adb2f48fb54005d679eda96088cefdb20d195dfc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 15:29:13 -0500 Subject: [PATCH 44/47] Updates event store readme --- .../quartz/nip01Core/store/README.md | 62 +++++-------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md index c8084f0a2..b5c097136 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md @@ -5,10 +5,10 @@ This module implements an **Event Store** with nostr-native queries. The goal was not to make the fastest database, since there could be multiple optimizations made if consistency can be sacrificed, but a database that will never crash and never go corrupt. -## Responsibilities +## Features - **Storage & Retrieval**: - Stores Nostr events and enables retrieval using Nostr filters + Stores Nostr events and retrieves using Nostr filters - **Replaceable Events**: - Old versions are removed when newer versions arrive. @@ -18,35 +18,42 @@ consistency can be sacrificed, but a database that will never crash and never go - Ephemeral events never stored. - **NIP-40 Expirations** - - Manages expiration timestamps and prunes expired events. + - Prunes expired events. - Blocks expired events from being reinserted - **NIP-09 Deletion Events** - Deletes by event id - Deletes by address until the `created_at` - Blocks deleted events from being re-inserted. + - GiftWraps are deleted by p-tag - **NIP-62 Right to Vanish** - Supports deleting an entire user until the `created_at` for enhanced privacy + - GiftWraps are deleted by p-tag + +- **NIP-45 Counts**: + - Counts records matching Nostr filters - **NIP-50 Full Text Search**: - - Implements content indexing and full text search supporting rich queries over event content. + - Custom content/tag indexing + - Rich queries over event content and tags + - Indexes updated on replaceables, deletions, vanish and expirations. + +- **NIP-91: AND operator for tags**: + - Allows queries matching two or more tags at the same time - **Immutable Tables** - Triggers ensure event immutability. + - Tables cannot be updated, only inserted and deleted. ## Indexing Strategy The store indexes events using five dedicated tables: - `event_headers`: stores the canonical event fields. -- `event_tags`: indexes tag values for fast filtering on tag-based queries. +- `event_tags`: indexes tag values as a hash for fast filtering on tag-based queries. - `event_fts`: for the content of full text search - `event_expirations`: to control when expired events must be deleted. - `event_vanish`: to control up to when vanished accounts must be blocked. -SQL triggers ensure the **immutability of stored events**, preventing accidental or intentional -modifications. - ## Querying This module supports optimized query planning, producing efficient SQL for multi-filter evaluation @@ -63,43 +70,6 @@ store.query( ) ``` -Becomes - -```sql -SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers -INNER JOIN ( - SELECT event_headers.row_id AS row_id - FROM event_headers - ORDER BY created_at DESC,id ASC - LIMIT 10 - - UNION - - SELECT event_headers.row_id AS row_id - FROM event_headers - INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id - WHERE event_headers.kind IN (1, 1111) - AND event_headers.pubkey = hexkey - AND event_fts MATCH "keywords" - ORDER BY created_at DESC, id ASC - LIMIT 100 - - UNION - - SELECT event_headers.row_id AS row_id - FROM event_headers - INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id - WHERE event_headers.kind = 20 - AND event_fts MATCH "cats" - ORDER BY created_at DESC,id ASC - LIMIT 30 -) AS filtered ON event_headers.row_id = filtered.row_id -ORDER BY created_at DESC,id -``` - -The union operations support complex filter lists while avoiding redundant data fetching and -duplicated outstreams. - ## How to Use The `EventStore` class provides a high-level interface for interacting with the event database. From 3d0b0c01b9be2ab5081a13ecea42319f815918ae Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 15:36:32 -0500 Subject: [PATCH 45/47] Finishes editing quartz event store readme --- .../vitorpamplona/quartz/nip01Core/store/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md index b5c097136..b82888b11 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md @@ -11,14 +11,20 @@ consistency can be sacrificed, but a database that will never crash and never go Stores Nostr events and retrieves using Nostr filters - **Replaceable Events**: + - Forces unique constraint by kind, pubkey - Old versions are removed when newer versions arrive. - Old versions are blocked if newer versions exist. +- **Addressable Events**: + - Forces unique constraint by kind, pubkey, d-tag + - Old versions are removed when newer versions arrive. + - Old versions are blocked if newer versions exist. + - **Ephemeral Events** - Ephemeral events never stored. - **NIP-40 Expirations** - - Prunes expired events. + - Deletes expired events. - Blocks expired events from being reinserted - **NIP-09 Deletion Events** @@ -28,7 +34,8 @@ consistency can be sacrificed, but a database that will never crash and never go - GiftWraps are deleted by p-tag - **NIP-62 Right to Vanish** - - Supports deleting an entire user until the `created_at` for enhanced privacy + - Deletes all user events until the `created_at` + - Blocks vanished events from being re-inserted. - GiftWraps are deleted by p-tag - **NIP-45 Counts**: From 92d4654b209874869e939b87e1521f0bbee2d905 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 16:49:11 -0500 Subject: [PATCH 46/47] Removes the memory counter methods because they are not the best way to measure the size in memory of these objects, since we intern strings all the time. --- .../com/vitorpamplona/amethyst/DebugUtils.kt | 12 ++++++++++ .../speedLogger/RelaySpeedLogger.kt | 3 ++- .../quartz/nip01Core/core/Address.kt | 8 ------- .../interactiveStories/tags/RootSceneTag.kt | 9 -------- .../quartz/nip01Core/core/Address.kt | 2 -- .../quartz/nip01Core/core/Event.kt | 11 ---------- .../quartz/nip01Core/hints/EventHintBundle.kt | 7 ------ .../quartz/nip01Core/tags/aTag/ATag.kt | 9 -------- .../quartz/nip01Core/tags/dTag/DTag.kt | 5 ----- .../quartz/nip01Core/tags/events/ETag.kt | 8 ------- .../quartz/nip01Core/tags/people/PTag.kt | 7 ------ .../quartz/nip02FollowList/tags/ContactTag.kt | 8 ------- .../quartz/nip13Pow/tags/PoWTag.kt | 4 ---- .../nip18Reposts/quotes/QAddressableTag.kt | 7 ------ .../quartz/nip18Reposts/quotes/QEventTag.kt | 8 ------- .../nip28PublicChat/list/tags/ChannelTag.kt | 8 ------- .../quartz/nip46RemoteSigner/BunkerMessage.kt | 4 +--- .../quartz/nip46RemoteSigner/BunkerRequest.kt | 11 +--------- .../nip46RemoteSigner/BunkerResponse.kt | 11 +--------- .../quartz/nip47WalletConnect/Response.kt | 18 +++------------ .../quartz/nip48ProxyTags/ProxyTag.kt | 4 ---- .../bookmarkList/tags/AddressBookmark.kt | 4 ---- .../bookmarkList/tags/EventBookmark.kt | 8 ------- .../nip51Lists/muteList/tags/UserTag.kt | 7 ------ .../nip51Lists/muteList/tags/WordTag.kt | 6 ----- .../meetingSpaces/tags/MeetingSpaceTag.kt | 4 ---- .../presence/tags/MeetingRoomTag.kt | 4 ---- .../quartz/nip57Zaps/LnZapEvent.kt | 6 ----- .../nip57Zaps/zapraiser/ZapRaiserTag.kt | 5 ----- .../nip59Giftwrap/seals/SealedRumorEvent.kt | 6 ----- .../nip59Giftwrap/wraps/GiftWrapEvent.kt | 6 ----- .../quartz/nip71Video/tags/TextTrackTag.kt | 7 ------ .../approval/tags/ApprovedAddressTag.kt | 4 ---- .../follow/tags/CommunityTag.kt | 4 ---- .../definition/AppDefinitionEvent.kt | 2 -- .../definition/AppMetadata.kt | 22 ------------------- .../NIP90ContentDiscoveryResponseEvent.kt | 6 ----- .../quartz/nip01Core/core/Address.ios.kt | 9 -------- .../quartz/nip01Core/core/Address.jvm.kt | 9 -------- 39 files changed, 20 insertions(+), 263 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 178950005..84277731f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -26,8 +26,11 @@ import android.content.pm.ApplicationInfo import android.os.Debug import androidx.core.content.getSystemService import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlin.time.DurationUnit import kotlin.time.measureTimedValue @@ -228,3 +231,12 @@ inline fun debug( Log.d(tag, debugMessage()) } } + +fun Event.countMemory(): Int = + 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) + 12 + // createdAt + kind + id.bytesUsedInMemory() + + pubKey.bytesUsedInMemory() + + tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } + + content.bytesUsedInMemory() + + sig.bytesUsedInMemory() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt index 8883fe5d8..94a2c05bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.bytesUsedInMemory /** * Listens to NostrClient's onNotify messages from the relay @@ -47,7 +48,7 @@ class RelaySpeedLogger( msg: Message, ) { if (msg is EventMessage) { - current.increment(msg.event.kind, msg.subId, relay.url, msg.event.countMemory()) + current.increment(msg.event.kind, msg.subId, relay.url, msgStr.bytesUsedInMemory()) } } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt index 0152e00e6..1df2a6dbf 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt @@ -23,8 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.core import android.os.Parcel import android.os.Parcelable import androidx.compose.runtime.Stable -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Stable actual data class Address actual constructor( @@ -35,12 +33,6 @@ actual data class Address actual constructor( Parcelable { actual fun toValue() = assemble(kind, pubKeyHex, dTag) - actual fun countMemory(): Int = - 3 * pointerSizeInBytes + - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() - actual override fun compareTo(other: Address): Int { val kindComparison = kind.compareTo(other.kind) return if (kindComparison == 0) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt index 3e55232b9..42d8c35ef 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt @@ -27,9 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class RootSceneTag( @@ -48,13 +46,6 @@ data class RootSceneTag( this.relay = relayHint } - fun countMemory(): Int = - 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = assembleATagId(kind, pubKeyHex, dTag) fun toTagArray() = assemble(kind, pubKeyHex, dTag, relay) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt index dca489a28..555d4ce8a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt @@ -33,8 +33,6 @@ expect class Address( fun toValue(): String - fun countMemory(): Int - companion object { fun assemble( kind: Int, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt index e7a455689..4e1c373ea 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable open class Event( @@ -44,15 +42,6 @@ open class Event( */ open fun isContentEncoded() = false - open fun countMemory(): Int = - 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) - 12 + // createdAt + kind - id.bytesUsedInMemory() + - pubKey.bytesUsedInMemory() + - tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } + - content.bytesUsedInMemory() + - sig.bytesUsedInMemory() - fun toJson(): String = OptimizedJsonMapper.toJson(this) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt index 415d295b2..875a1395a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class EventHintBundle( @@ -39,10 +37,5 @@ data class EventHintBundle( this.authorHomeRelay = authorHomeRelay } - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - event.countMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(event.id, event.pubKey, event.kind, relay) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt index 54925b539..809fc8698 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt @@ -32,9 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ATag( @@ -45,13 +43,6 @@ data class ATag( ) { constructor(address: Address, relayHint: NormalizedRelayUrl? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint) - fun countMemory(): Int = - 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = AddressSerializer.assemble(kind, pubKeyHex, dTag) fun toATagArray() = assemble(toTag(), relay) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt index 2b4b3c16e..413b09005 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt @@ -20,14 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.dTag -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - class DTag( val dId: String, ) { - fun countMemory(): Int = 1 * pointerSizeInBytes + dId.bytesUsedInMemory() - fun toTagArray() = assemble(dId) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index a11296052..d286581bb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -30,9 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ETag( @@ -46,12 +44,6 @@ data class ETag( this.author = authorPubKeyHex } - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) override fun toTagArray() = assemble(eventId, relay, author) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt index c827ff84d..d47153de5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -33,20 +33,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class PTag( override val pubKey: HexKey, override val relayHint: NormalizedRelayUrl? = null, ) : PubKeyReferenceTag { - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - pubKey.bytesUsedInMemory() + - (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) fun toNPub(): String = pubKey.hexToByteArray().toNpub() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt index 8320fcfcf..107a96aa1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt @@ -31,9 +31,7 @@ import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ContactTag( @@ -51,12 +49,6 @@ data class ContactTag( this.petname = petname } - fun countMemory(): Int = - 3 * pointerSizeInBytes + - pubKey.bytesUsedInMemory() + - (relayUri?.url?.bytesUsedInMemory() ?: 0) + - (petname?.bytesUsedInMemory() ?: 0) - fun toTagArray() = assemble(pubKey, relayUri, petname) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt index b5a526efb..352386f02 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt @@ -22,16 +22,12 @@ package com.vitorpamplona.quartz.nip13Pow.tags import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class PoWTag( val nonce: String, val commitment: Int?, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + nonce.bytesUsedInMemory() + (commitment?.bytesUsedInMemory() ?: 0) - fun toTagArray() = assemble(nonce, commitment) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt index a84e27981..17ddadb8d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt @@ -27,9 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class QAddressableTag( @@ -53,11 +51,6 @@ data class QAddressableTag( this.relay = relayHint } - fun countMemory(): Int = - 2 * pointerSizeInBytes + - address.countMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - override fun toTagArray() = assemble(address, relay) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt index 3f088ec42..b50f440ab 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt @@ -28,9 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class QEventTag( @@ -44,12 +42,6 @@ data class QEventTag( this.author = authorPubKeyHex } - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - override fun toTagArray() = assemble(eventId, relay, author) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt index c2d386972..21c999bf5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt @@ -28,9 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class ChannelTag( @@ -38,12 +36,6 @@ class ChannelTag( val relay: NormalizedRelayUrl? = null, val author: HexKey? = null, ) { - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) fun toTagArray() = assemble(eventId, relay, author) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt index 0b1921e92..bb19fd777 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt @@ -22,6 +22,4 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable -abstract class BunkerMessage : OptimizedSerializable { - abstract fun countMemory(): Int -} +abstract class BunkerMessage : OptimizedSerializable diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt index acb102a1e..ee0f51405 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt @@ -20,17 +20,8 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - open class BunkerRequest( val id: String, val method: String, val params: Array = emptyArray(), -) : BunkerMessage() { - override fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - id.bytesUsedInMemory() + - method.bytesUsedInMemory() + - params.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } -} +) : BunkerMessage() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt index 61ab48443..5ff45c08c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt @@ -20,17 +20,8 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - open class BunkerResponse( val id: String, val result: String?, val error: String?, -) : BunkerMessage() { - override fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - id.bytesUsedInMemory() + - (result?.bytesUsedInMemory() ?: 0) + - (error?.bytesUsedInMemory() ?: 0) -} +) : BunkerMessage() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt index 4dc25157a..91ae3715f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt @@ -21,15 +21,11 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes // RESPONSE OBJECTS abstract class Response( val resultType: String, -) : OptimizedSerializable { - abstract fun countMemory(): Int -} +) : OptimizedSerializable // PayInvoice Call @@ -38,11 +34,7 @@ class PayInvoiceSuccessResponse( ) : Response("pay_invoice") { class PayInvoiceResultParams( val preimage: String? = null, - ) { - fun countMemory(): Int = pointerSizeInBytes + (preimage?.bytesUsedInMemory() ?: 0) - } - - override fun countMemory(): Int = pointerSizeInBytes + (result?.countMemory() ?: 0) + ) } class PayInvoiceErrorResponse( @@ -51,11 +43,7 @@ class PayInvoiceErrorResponse( class PayInvoiceErrorParams( val code: ErrorType? = null, val message: String? = null, - ) { - fun countMemory(): Int = pointerSizeInBytes + pointerSizeInBytes + (message?.bytesUsedInMemory() ?: 0) - } - - override fun countMemory(): Int = pointerSizeInBytes + (error?.countMemory() ?: 0) + ) enum class ErrorType { RATE_LIMITED, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt index 5d41c61e3..d18a4e11c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt @@ -22,17 +22,13 @@ package com.vitorpamplona.quartz.nip48ProxyTags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.has -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ProxyTag( val id: String, val protocol: String, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + id.bytesUsedInMemory() + protocol.bytesUsedInMemory() - fun toTagArray() = assemble(id, protocol) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt index 92d11a598..eae9cf1c8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt @@ -27,16 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class AddressBookmark( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) : BookmarkIdTag { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) override fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt index f81bcf2a3..e9d32a4ed 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt @@ -28,9 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class EventBookmark( @@ -38,12 +36,6 @@ class EventBookmark( val relay: NormalizedRelayUrl? = null, val author: HexKey? = null, ) : BookmarkIdTag { - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) override fun toTagArray() = assemble(eventId, relay, author) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt index c67a1fdbf..44423095b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt @@ -30,19 +30,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class UserTag( val pubKey: HexKey, val relayHint: NormalizedRelayUrl? = null, ) : MuteTag { - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - pubKey.bytesUsedInMemory() + - (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) fun toNPub(): String = pubKey.hexToByteArray().toNpub() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt index cb87f97fa..1bf539536 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt @@ -23,18 +23,12 @@ package com.vitorpamplona.quartz.nip51Lists.muteList.tags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class WordTag( val word: String, ) : MuteTag { - fun countMemory(): Int = - 1 * pointerSizeInBytes + // 1 fields, 4 bytes each reference (32bit) - word.bytesUsedInMemory() - override fun toTagArray() = assemble(word) override fun toTagIdOnly() = assemble(word) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt index c43eb30d8..889ec5fd6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt @@ -27,16 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class MeetingSpaceTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt index 1c487501b..5fd7cc690 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt @@ -27,16 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class MeetingRoomTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index d6962b031..24159c810 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class LnZapEvent( @@ -74,11 +73,6 @@ class LnZapEvent( } } - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (zapRequest?.countMemory() ?: 0) + // rough calculation - pointerSizeInBytes + 36 // bigdecimal size - override fun containedPost(): LnZapRequestEvent? = try { description()?.ifBlank { null }?.let { fromJson(it) } as? LnZapRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt index a666a41e4..40e5cffb7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt @@ -20,14 +20,9 @@ */ package com.vitorpamplona.quartz.nip57Zaps.zapraiser -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - class ZapRaiserTag( val amountInSats: Long, ) { - fun countMemory(): Int = 1 * pointerSizeInBytes + amountInSats.bytesUsedInMemory() - fun toTagArray() = assemble(amountInSats) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt index c8a06bde8..ba354583b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class SealedRumorEvent( @@ -46,10 +44,6 @@ class SealedRumorEvent( @kotlin.jvm.Transient var innerEventId: HexKey? = null - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0) - fun copyNoContent(): SealedRumorEvent { val copy = SealedRumorEvent( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index de404e7a8..5b2a22e74 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -34,8 +34,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.HostStub import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class GiftWrapEvent( @@ -50,10 +48,6 @@ class GiftWrapEvent( @kotlin.jvm.Transient var innerEventId: HexKey? = null - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0) - fun copyNoContent(): GiftWrapEvent { val copy = GiftWrapEvent( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt index 5de8e7452..5600e4fe0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt @@ -23,19 +23,12 @@ package com.vitorpamplona.quartz.nip71Video.tags import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class TextTrackTag( val eventId: HexKey, var relay: String? = null, ) { - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) - fun toTagArray() = arrayOfNotNull(TAG_NAME, eventId, relay) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt index f0b805183..9fb768adc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt @@ -28,16 +28,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class ApprovedAddressTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt index fcdaba6ec..ee67f0b9e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt @@ -28,16 +28,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class CommunityTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index a7706ff6e..0b5d2536e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -51,8 +51,6 @@ class AppDefinitionEvent( sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), PublishedAtProvider { - override fun countMemory(): Int = super.countMemory() + (cachedMetadata?.countMemory() ?: 8) - @kotlinx.serialization.Transient @kotlin.jvm.Transient private var cachedMetadata: AppMetadata? = null diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt index 44b6d8bb8..8053473fd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt @@ -22,8 +22,6 @@ package com.vitorpamplona.quartz.nip89AppHandlers.definition import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.JsonMapper -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -52,26 +50,6 @@ class AppMetadata { var lud06: String? = null var lud16: String? = null - fun countMemory(): Int = - 20 * pointerSizeInBytes + // 20 fields, 4 bytes for each reference - (name?.bytesUsedInMemory() ?: 0) + - (username?.bytesUsedInMemory() ?: 0) + - (displayName?.bytesUsedInMemory() ?: 0) + - (picture?.bytesUsedInMemory() ?: 0) + - (banner?.bytesUsedInMemory() ?: 0) + - (image?.bytesUsedInMemory() ?: 0) + - (website?.bytesUsedInMemory() ?: 0) + - (about?.bytesUsedInMemory() ?: 0) + - (subscription?.bytesUsedInMemory() ?: 0) + - (acceptsNutZaps?.bytesUsedInMemory() ?: 0) + - (supportsEncryption?.bytesUsedInMemory() ?: 0) + - (personalized?.bytesUsedInMemory() ?: 0) + // A Boolean has 8 bytes of header, plus 1 byte of payload, for a total of 9 bytes of information. The JVM then rounds it up to the next multiple of 8. so the one instance of java.lang.Boolean takes up 16 bytes of memory. - (amount?.bytesUsedInMemory() ?: 0) + - (nip05?.bytesUsedInMemory() ?: 0) + - (domain?.bytesUsedInMemory() ?: 0) + - (lud06?.bytesUsedInMemory() ?: 0) + - (lud16?.bytesUsedInMemory() ?: 0) - fun anyName(): String? = displayName ?: name ?: username fun anyNameStartsWith(prefix: String): Boolean = diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt index c4e1500b3..4dfad6cd8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class NIP90ContentDiscoveryResponseEvent( @@ -44,10 +42,6 @@ class NIP90ContentDiscoveryResponseEvent( @kotlin.jvm.Transient var events: List? = null - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (events?.sumOf { it.bytesUsedInMemory() } ?: 0) - fun innerTags(): List { if (content.isEmpty()) { return listOf() diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt index 521c0dadd..d91d93bc9 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.core -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - actual data class Address actual constructor( actual val kind: Kind, actual val pubKeyHex: HexKey, @@ -30,12 +27,6 @@ actual data class Address actual constructor( ) : Comparable
{ actual fun toValue() = assemble(kind, pubKeyHex, dTag) - actual fun countMemory(): Int = - 3 * pointerSizeInBytes + - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() - actual override fun compareTo(other: Address): Int { val result = kind.compareTo(other.kind) return if (result == 0) { diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt index 521c0dadd..d91d93bc9 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.core -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - actual data class Address actual constructor( actual val kind: Kind, actual val pubKeyHex: HexKey, @@ -30,12 +27,6 @@ actual data class Address actual constructor( ) : Comparable
{ actual fun toValue() = assemble(kind, pubKeyHex, dTag) - actual fun countMemory(): Int = - 3 * pointerSizeInBytes + - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() - actual override fun compareTo(other: Address): Int { val result = kind.compareTo(other.kind) return if (result == 0) { From 208f57830fb6a591e89035ec7e9d770b886fd847 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 14 Jan 2026 22:56:07 +0000 Subject: [PATCH 47/47] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 6 ++ .../src/main/res/values-sl-rSI/strings.xml | 60 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 8d5434880..88b468e25 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -172,6 +172,12 @@ Nieoczekiwany status przesyłania NIP-95 nie jest jeszcze dostępny dla wiadomości głosowych Nie udało się przesłać głosu: %1$s + Brak + Niska + Wysoka + Neutralna + Anonimowy + Zmienia barwę głosu. Uwaga: podstawowe zmiany barwy głosu mogą zostać wykryte przez uważnych słuchaczy. Użytkownik nie ma skonfigurowanego adresu LN, aby odbierać satsy "odpowiedz tutaj.. " Kopiuje ID wpisu do schowka w celu udostępnienia w Nostr diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index 86b75dcd2..d72e872f7 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -15,6 +15,8 @@ Dešifriranje sporočila ni uspelo Slika skupine Eksplicitna vsebina + Obvestilo releja + Podvojena objava Nezaželena vsebina Iz tega releja izvira številna nezaželena vsebina Oponašanje @@ -132,6 +134,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Naslov releja Pošlji Bajti + Napaka Napake Število napak pri povezovanju v tej seji Domače vsebine @@ -143,6 +146,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Dodaj Uporabnika Dodaj rele Ime + Ime (za @označbe) + Moje ime @oznake Prikazano ime Moje prikazno ime Janez Slovenc @@ -169,7 +174,21 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Posnemi sporočilo Posnemi sporočilo Klikni in drži za snemanje sporočila + Ponovno posnemi + Snemam + Snemam %1$s Nalaganje … + Naloži napako + Neuspešno nalaganje zvočnega sporočila + Nepričakovan status nalaganja + NIP-95 še ne podpira zvočnih posnetkov + Neuspešno nalaganje zvočnega posnetka: %1$s + Nič + Globok + Visok + Nevtralen + Anonimiziraj + Prilagodi višino tona svojega glasu: Opomba: osnovne spremembe višine tona lahko poslušalci potencialno razveljavijo. Uporabnik nima nastavljenega \"lightning\" naslova za sprejem satoshi-jev "odgovori tukaj.. " Kopira ID zapiska v odložišče za deljenje v Nostr @@ -363,6 +382,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Blokiraj Ročno razdeli zape Zaznamki + Privzeti zaznamki + Tvoje privzeti zaznamki, ki jih podpira veliko Nostr odjemalcev. Osnutki Privatni zaznamki Javni zaznamki @@ -646,28 +667,64 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Količina bajtov, ki je bila prejeta od tega releja, vključno s filtri in dogodki Prišlo je do napake pri pridobivanju informacij o releju iz %1$s Lastnik + Servisni ključ + V teku %1$s + V teku %1$s (%2$s) Različica Program Kontakt Podprti NIP-i + Podprti upravljalniki vsebine Vstopnina + Vstopnina + Naročnina + Publikacija + Plačila %1$s URL za plačila + Ciljna skupina + Pravilnik & Povezave + Provizije & Plačila Omejitve Države Jeziki Oznake + Teme + Vse države + Vsi jeziki Pravila objavljanja + Izjava o zasebnosti + Določila & Pogoji + N/A Napake in obvestila s tega releja Dolžina sporočila Naročnine Filtri Dolžina id naročnine Minimalna predpona - Maksimum oznak v dogodku + Največ oznak v dogodku Dolžina vsebine + Največja dolžina vsebine + Zavrži starejše od + Sprejme do + %1$s v prihodnost + Pred %1$s + %1$s ničel + %1$s bitov + Zadržanje dogodka + Velikost vsebine + Povezljivost + Nadzor dostopa Minimalen PoW Auth + Potrebna avtentikacija Plačilo + Potrebno je plačilo + Največja dolžina sporočila + Največ naročnin + Največ filtrov na naročnino + Največja omejitev (vrnjenih dogodkov) + Privzeta omejitev (vrnjenih dogodkov) + Največja dolžina SubID Cashu žeton Unovči Pošlji v Zap denarnico @@ -1193,4 +1250,5 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Oddajaj paket Izbriši seznam Izbriši paket + Tipi