From ba0fc67a943a352ebafb32d583bff2bf6e2e1efd Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 25 Sep 2025 18:24:31 +0100 Subject: [PATCH 01/24] First hack for bolting follow sets onto follows, for determining if a user follows another. --- .../vitorpamplona/amethyst/model/Account.kt | 20 +++++++++++++++++++ .../reqCommand/user/UserObservers.kt | 7 +++++-- 2 files changed, 25 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 3191aae0a..05abfd8e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable +import androidx.compose.ui.util.fastAny import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -119,6 +120,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.mimeType import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.value import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle @@ -215,6 +217,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.math.BigDecimal import java.util.Locale @@ -838,6 +841,23 @@ class Account( return@withContext followSetNotes } + fun isUserInFollowSets(user: User): Boolean = + runBlocking(scope.coroutineContext) { + LocalCache.getFollowSetNotesFor(userProfile()).fastAny { it -> + val listEvent = it.event as PeopleListEvent + val isInPublicSets = + listEvent + .publicPeople() + .fastAny { it.toTagArray().value() == user.pubkeyHex } + val isInPrivateSets = + listEvent + .privatePeople(signer) + ?.fastAny { it.toTagArray().value() == user.pubkeyHex } ?: false + + isInPublicSets || isInPrivateSets + } + } + fun mapNoteToFollowSet(note: Note): FollowSet = FollowSet .mapEventToSet( 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 7430f3489..b2ece59d3 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 @@ -420,12 +420,15 @@ fun observeUserIsFollowing( .follows.stateFlow .sample(1000) .mapLatest { userState -> - userState.user.isFollowing(user2) + userState.user.isFollowing(user2) || + accountViewModel.account.isUserInFollowSets(user2) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } - return flow.collectAsStateWithLifecycle(user1.isFollowing(user2)) + return flow.collectAsStateWithLifecycle( + user1.isFollowing(user2) || accountViewModel.account.isUserInFollowSets(user2), + ) } @SuppressLint("StateFlowValueCalledInComposition") From 4c3eaf972cbed592aad12942f82af4230aaeb302 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 25 Sep 2025 10:53:33 +0200 Subject: [PATCH 02/24] move to LightCompressor-enhanced fork corrected image vs video compressor library names --- amethyst/build.gradle | 4 ++-- .../amethyst/service/uploads/VideoCompressionHelper.kt | 2 ++ gradle/libs.versions.toml | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 34e20e556..e700680f7 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -332,9 +332,9 @@ dependencies { implementation libs.audiowaveform // Video compression lib - implementation libs.abedElazizShe.image.compressor + implementation libs.abedElazizShe.video.compressor.fork // Image compression lib - implementation libs.zelory.video.compressor + implementation libs.zelory.image.compressor // Cbor for cashuB format implementation libs.kotlinx.serialization.cbor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index 894b40708..a64e102f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -40,6 +40,8 @@ import java.util.UUID import kotlin.coroutines.resume import kotlin.math.roundToInt +// TODO: add Auto setting. Focus on small fast streams. 4->1080p, 1080p->720p, 720p and below stay the same resolution. Use existing matrix to determine bitrate. +// TODO: use bps api and don't floor at 1Mbps data class VideoInfo( val resolution: VideoResolution, val framerate: Float, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 896582390..691f31f5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.9.4" -lightcompressor = "1.3.3" +lightcompressor = "1.4.0" markdown = "e1151c8" media3 = "1.8.0" mockk = "1.14.5" @@ -63,7 +63,7 @@ core = "1.7.0" mavenPublish = "0.34.0" [libraries] -abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" } +abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor" } accompanist-adaptive = { group = "com.google.accompanist", name = "accompanist-adaptive", version.ref = "accompanistAdaptive" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistAdaptive" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -145,7 +145,7 @@ vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", v vico-charts-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico-charts" } vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts" } vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico-charts" } -zelory-video-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" } +zelory-image-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" } zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" } zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } From 6e4e432adf75f868b0e2f5e4d615139c0de9e97c Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 25 Sep 2025 21:41:28 +0200 Subject: [PATCH 03/24] Use new bps api of lightcompressorlibrary fork --- .../service/uploads/VideoCompressionHelper.kt | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index a64e102f9..f2eeaab2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -38,10 +38,8 @@ import kotlinx.coroutines.withTimeoutOrNull import java.io.File import java.util.UUID import kotlin.coroutines.resume -import kotlin.math.roundToInt // TODO: add Auto setting. Focus on small fast streams. 4->1080p, 1080p->720p, 720p and below stay the same resolution. Use existing matrix to determine bitrate. -// TODO: use bps api and don't floor at 1Mbps data class VideoInfo( val resolution: VideoResolution, val framerate: Float, @@ -82,18 +80,19 @@ enum class VideoStandard( override fun toString(): String = label } +private const val MBPS_TO_BPS_MULTIPLIER = 1_000_000 + data class CompressionRule( val width: Int, val height: Int, val bitrateMbps: Float, val description: String, ) { - fun getBitrateMbpsInt(framerate: Float): Int { + fun getBitrateBps(framerate: Float): Int { // Apply 1.5x multiplier for 60fps+ videos val multiplier = if (framerate >= 60f) 1.5f else 1.0f - // Library doesn't support float so we have to convert it to int and use 1 as minimum - return (bitrateMbps * multiplier).roundToInt().coerceAtLeast(1) + return (bitrateMbps * multiplier).toInt() * MBPS_TO_BPS_MULTIPLIER } } @@ -146,22 +145,19 @@ object VideoCompressionHelper { ): MediaCompressorResult { val videoInfo = getVideoInfo(uri, applicationContext) - val videoBitrateInMbps = - if (videoInfo != null) { - val bitrateMbpsInt = + val videoBitrateInBps = + videoInfo?.let { info -> + val bitrateBps = compressionRules .getValue(mediaQuality) - .getValue(videoInfo.resolution.getStandard()) - .getBitrateMbpsInt(videoInfo.framerate) + .getValue(info.resolution.getStandard()) + .getBitrateBps(info.framerate) - Log.d( - LOG_TAG, - "Bitrate: ${bitrateMbpsInt}Mbps for ${videoInfo.resolution.getStandard()} " + - "quality=$mediaQuality framerate=${videoInfo.framerate}fps.", - ) - } else { + Log.d(LOG_TAG, "Bitrate: ${bitrateBps}bps for ${info.resolution.getStandard()} quality=$mediaQuality framerate=${info.framerate}fps.") + bitrateBps + } ?: run { Log.w(LOG_TAG, "Video bitrate fallback: 2Mbps (videoInfo unavailable)") - 2 + 2 * MBPS_TO_BPS_MULTIPLIER } val resizer = @@ -194,7 +190,7 @@ object VideoCompressionHelper { storageConfiguration = AppSpecificStorageConfiguration(), configureWith = Configuration( - videoBitrateInMbps = videoBitrateInMbps, + videoBitrateInBps = videoBitrateInBps.toLong(), resizer = resizer, videoNames = listOf(UUID.randomUUID().toString()), isMinBitrateCheckEnabled = false, From 4b60c3e20b5daa7a71bcef29c15cdeaf9caf5779 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 25 Sep 2025 21:36:34 +0100 Subject: [PATCH 04/24] Refactor: From List -> Set, and Viewmodel rename as well. Move FollowSet and related classes to model package. --- .../nip51Lists/followSets}/FollowSet.kt | 20 +++---- .../model/nip51Lists/followSets/NostrSet.kt | 52 ++++++++++++++++++ .../nip51Lists/followSets/SetVisibility.kt | 47 ++++++++++++++++ .../loggedIn/lists/CustomListsScreen.kt | 26 ++++----- .../ui/screen/loggedIn/lists/CustomSetItem.kt | 16 +++--- ...ollowSetState.kt => FollowSetFeedState.kt} | 12 +++-- ...ViewModel.kt => FollowSetFeedViewModel.kt} | 28 +++++----- .../screen/loggedIn/lists/ListVisibility.kt | 27 ---------- .../ui/screen/loggedIn/lists/NostrList.kt | 32 ----------- .../lists/followsets/FollowSetScreen.kt | 24 ++++----- .../followsets/FollowSetsManagementDialog.kt | 54 +++++++++---------- 11 files changed, 192 insertions(+), 146 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/{ui/screen/loggedIn/lists => model/nip51Lists/followSets}/FollowSet.kt (84%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/{FollowSetState.kt => FollowSetFeedState.kt} (83%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/{NostrUserListFeedViewModel.kt => FollowSetFeedViewModel.kt} (90%) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/ListVisibility.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrList.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSet.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.kt index 58982ebc3..59f5ed34c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSet.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.screen.loggedIn.lists +package com.vitorpamplona.amethyst.model.nip51Lists.followSets import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.value @@ -31,9 +31,9 @@ data class FollowSet( val identifierTag: String, val title: String, val description: String?, - val visibility: ListVisibility, - val profileList: Set, -) : NostrList(listVisibility = visibility, content = profileList) { + val visibility: SetVisibility, + val profiles: Set, +) : NostrSet(setVisibility = visibility, content = profiles) { companion object { fun mapEventToSet( event: PeopleListEvent, @@ -53,16 +53,16 @@ data class FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, - visibility = ListVisibility.Private, - profileList = privateFollows.toSet(), + visibility = SetVisibility.Private, + profiles = privateFollows.toSet(), ) } else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) { FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, - visibility = ListVisibility.Public, - profileList = publicFollows.toSet(), + visibility = SetVisibility.Public, + profiles = publicFollows.toSet(), ) } else { // Follow set is empty, so assume public. Why? Nostr limitation. @@ -71,8 +71,8 @@ data class FollowSet( identifierTag = dTag, title = listTitle, description = listDescription, - visibility = ListVisibility.Public, - profileList = publicFollows.toSet(), + visibility = SetVisibility.Public, + profiles = publicFollows.toSet(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt new file mode 100644 index 000000000..a32258cd7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt @@ -0,0 +1,52 @@ +/** + * 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.model.nip51Lists.followSets + +/** + * 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. + */ +sealed class NostrSet( + val setVisibility: SetVisibility, + val content: Collection, +) + +class CuratedBookmarkSet( + val name: String, + val visibility: SetVisibility, + val setItems: List, +) : NostrSet(visibility, setItems) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt new file mode 100644 index 000000000..ce418b099 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt @@ -0,0 +1,47 @@ +/** + * 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.model.nip51Lists.followSets + +/** + * 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. + */ +enum class SetVisibility { + Public, + Private, + Mixed, +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index ca09f2cbb..9a600f9e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -59,6 +59,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -76,10 +78,10 @@ fun ListsAndSetsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val followSetsViewModel: NostrUserListFeedViewModel = + val followSetsViewModel: FollowSetFeedViewModel = viewModel( - key = "NostrUserListFeedViewModel", - factory = NostrUserListFeedViewModel.Factory(accountViewModel.account), + key = "FollowSetFeedViewModel", + factory = FollowSetFeedViewModel.Factory(accountViewModel.account), ) ListsAndSetsScreen( @@ -91,7 +93,7 @@ fun ListsAndSetsScreen( @Composable fun ListsAndSetsScreen( - followSetsViewModel: NostrUserListFeedViewModel, + followSetsViewModel: FollowSetFeedViewModel, accountViewModel: AccountViewModel, nav: INav, ) { @@ -117,8 +119,8 @@ fun ListsAndSetsScreen( refresh = { followSetsViewModel.invalidateData() }, - addItem = { title: String, description: String?, listType: ListVisibility -> - val isSetPrivate = listType == ListVisibility.Private + addItem = { title: String, description: String?, listType: SetVisibility -> + val isSetPrivate = listType == SetVisibility.Private followSetsViewModel.addFollowSet( setName = title, setDescription = description, @@ -149,9 +151,9 @@ fun ListsAndSetsScreen( @Composable fun CustomListsScreen( - followSetState: FollowSetState, + followSetFeedState: FollowSetFeedState, refresh: () -> Unit, - addItem: (title: String, description: String?, listType: ListVisibility) -> Unit, + addItem: (title: String, description: String?, listType: SetVisibility) -> Unit, openItem: (identifier: String) -> Unit, renameItem: (followSet: FollowSet, newName: String) -> Unit, deleteItem: (followSet: FollowSet) -> Unit, @@ -195,10 +197,10 @@ fun CustomListsScreen( // TODO: Show components based on current tab FollowSetFabsAndMenu( onAddPrivateSet = { name: String, description: String? -> - addItem(name, description, ListVisibility.Private) + addItem(name, description, SetVisibility.Private) }, onAddPublicSet = { name: String, description: String? -> - addItem(name, description, ListVisibility.Public) + addItem(name, description, SetVisibility.Public) }, ) }, @@ -216,7 +218,7 @@ fun CustomListsScreen( when (page) { 0 -> FollowSetFeedView( - followSetState = followSetState, + followSetFeedState = followSetFeedState, onRefresh = refresh, onOpenItem = openItem, onRenameItem = renameItem, @@ -410,7 +412,7 @@ private fun SetItemPreview() { identifierTag = "00001-2222", title = "Sample List Title", description = "Sample List Description", - visibility = ListVisibility.Mixed, + visibility = SetVisibility.Mixed, emptySet(), ) ThemeComparisonColumn { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index 26cdf279e..4d75c61db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -55,6 +55,8 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.stringRes @@ -98,7 +100,7 @@ fun CustomSetItem( selected = true, onClick = {}, label = { - Text(text = "${followSet.profileList.size}") + Text(text = "${followSet.profiles.size}") }, leadingIcon = { Icon( @@ -121,9 +123,9 @@ fun CustomSetItem( followSet.visibility.let { val text by derivedStateOf { when (it) { - ListVisibility.Public -> stringRes(context, R.string.follow_set_type_public) - ListVisibility.Private -> stringRes(context, R.string.follow_set_type_private) - ListVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) + SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public) + SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private) + SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) } } Column( @@ -135,9 +137,9 @@ fun CustomSetItem( painter = painterResource( when (it) { - ListVisibility.Public -> R.drawable.ic_public - ListVisibility.Private -> R.drawable.lock - ListVisibility.Mixed -> R.drawable.format_list_bulleted_type + SetVisibility.Public -> R.drawable.ic_public + SetVisibility.Private -> R.drawable.lock + SetVisibility.Mixed -> R.drawable.format_list_bulleted_type }, ), contentDescription = stringRes(R.string.follow_set_type_description, text), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedState.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedState.kt index d39a66110..59109582b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedState.kt @@ -20,16 +20,18 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists -sealed class FollowSetState { - data object Loading : FollowSetState() +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet + +sealed class FollowSetFeedState { + data object Loading : FollowSetFeedState() data class Loaded( val feed: List, - ) : FollowSetState() + ) : FollowSetFeedState() - data object Empty : FollowSetState() + data object Empty : FollowSetFeedState() data class FeedError( val errorMessage: String, - ) : FollowSetState() + ) : FollowSetFeedState() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrUserListFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrUserListFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt index 735025564..c921816cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrUserListFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedViewModel.kt @@ -30,6 +30,8 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter @@ -49,12 +51,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.UUID -// TODO Update: Rename this to be used only for follow sets, and create separate VMs for bookmark sets, etc. -class NostrUserListFeedViewModel( +class FollowSetFeedViewModel( val dataSource: FeedFilter, ) : ViewModel(), InvalidatableContent { - private val _feedContent = MutableStateFlow(FollowSetState.Loading) + private val _feedContent = MutableStateFlow(FollowSetFeedState.Loading) val feedContent = _feedContent.asStateFlow() fun refresh() { @@ -67,9 +68,8 @@ class NostrUserListFeedViewModel( noteIdentifier: String, account: Account, ): AddressableNote? { -// checkNotInMainThread() val potentialNote = - runBlocking(Dispatchers.IO) { account.getFollowSetNotes() } + runBlocking(Dispatchers.IO) { account.followSetsState.getFollowSetNotes() } .find { it.dTag() == noteIdentifier } return potentialNote } @@ -79,7 +79,7 @@ class NostrUserListFeedViewModel( account: Account, ): Boolean { val potentialNote = - runBlocking(viewModelScope.coroutineContext) { account.getFollowSetNotes() } + runBlocking(viewModelScope.coroutineContext) { account.followSetsState.getFollowSetNotes() } .find { (it.event as PeopleListEvent).nameOrTitle() == setName } return potentialNote != null } @@ -94,7 +94,7 @@ class NostrUserListFeedViewModel( val newSets = dataSource.loadTop().toImmutableList() - if (oldFeedState is FollowSetState.Loaded) { + if (oldFeedState is FollowSetFeedState.Loaded) { val oldFeedList = oldFeedState.feed.toImmutableList() // Using size as a proxy for has changed. if (!equalImmutableLists(newSets, oldFeedList)) { @@ -108,7 +108,7 @@ class NostrUserListFeedViewModel( this.javaClass.simpleName, "refreshSuspended: Error loading or refreshing feed -> ${e.message}", ) - _feedContent.update { FollowSetState.FeedError(e.message.toString()) } + _feedContent.update { FollowSetFeedState.FeedError(e.message.toString()) } } finally { isRefreshing.value = false } @@ -190,7 +190,7 @@ class NostrUserListFeedViewModel( PeopleListEvent.addUser( earlierVersion = followSetEvent, pubKeyHex = userProfileHex, - isPrivate = followSet.visibility == ListVisibility.Private, + isPrivate = followSet.visibility == SetVisibility.Private, signer = account.signer, ) { account.sendMyPublicAndPrivateOutbox(it) @@ -223,9 +223,9 @@ class NostrUserListFeedViewModel( private fun updateFeed(sets: ImmutableList) { if (sets.isNotEmpty()) { - _feedContent.update { FollowSetState.Loaded(sets) } + _feedContent.update { FollowSetFeedState.Loaded(sets) } } else { - _feedContent.update { FollowSetState.Empty } + _feedContent.update { FollowSetFeedState.Empty } } } @@ -244,7 +244,7 @@ class NostrUserListFeedViewModel( init { Log.d("Init", this.javaClass.simpleName) - Log.d(this.javaClass.simpleName, " FollowSetState : ${_feedContent.value}") + Log.d(this.javaClass.simpleName, " FollowSetFeedState : ${_feedContent.value}") collectorJob = viewModelScope.launch(Dispatchers.IO) { LocalCache.live.newEventBundles.collect { newNotes -> @@ -266,8 +266,8 @@ class NostrUserListFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T = - NostrUserListFeedViewModel( - FollowSetFeedFilter(account), + FollowSetFeedViewModel( + FollowSetFeedFilter(account.followSetsState), ) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/ListVisibility.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/ListVisibility.kt deleted file mode 100644 index 8948cd93b..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/ListVisibility.kt +++ /dev/null @@ -1,27 +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.ui.screen.loggedIn.lists - -enum class ListVisibility { - Public, - Private, - Mixed, -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrList.kt deleted file mode 100644 index e51dff97a..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/NostrList.kt +++ /dev/null @@ -1,32 +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.ui.screen.loggedIn.lists - -sealed class NostrList( - val listVisibility: ListVisibility, - val content: Collection, -) - -class CuratedBookmarkList( - val name: String, - val visibility: ListVisibility, - val listItems: List, -) : NostrList(visibility, listItems) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt index df54be0f1..d0b930e80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt @@ -67,14 +67,14 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -92,10 +92,10 @@ fun FollowSetScreen( accountViewModel: AccountViewModel, navigator: INav, ) { - val followSetViewModel: NostrUserListFeedViewModel = + val followSetViewModel: FollowSetFeedViewModel = viewModel( - key = "NostrUserListFeedViewModel", - factory = NostrUserListFeedViewModel.Factory(accountViewModel.account), + key = "FollowSetFeedViewModel", + factory = FollowSetFeedViewModel.Factory(accountViewModel.account), ) FollowSetScreen(selectedSetIdentifier, followSetViewModel, accountViewModel, navigator) @@ -105,7 +105,7 @@ fun FollowSetScreen( @Composable fun FollowSetScreen( selectedSetIdentifier: String, - followSetViewModel: NostrUserListFeedViewModel, + followSetViewModel: FollowSetFeedViewModel, accountViewModel: AccountViewModel, navigator: INav, ) { @@ -144,7 +144,7 @@ fun FollowSetScreen( when { selectedSetState.value != null -> { val selectedSet = selectedSetState.value - val users = selectedSet!!.profileList.mapToUsers(accountViewModel).filterNotNull() + val users = selectedSet!!.profiles.mapToUsers(accountViewModel).filterNotNull() Scaffold( topBar = { TopAppBar( @@ -235,10 +235,10 @@ fun TitleAndDescription( Icon( painter = painterResource( - when (followSet.listVisibility) { - ListVisibility.Public -> R.drawable.ic_public - ListVisibility.Private -> R.drawable.lock - ListVisibility.Mixed -> R.drawable.format_list_bulleted_type + when (followSet.setVisibility) { + SetVisibility.Public -> R.drawable.ic_public + SetVisibility.Private -> R.drawable.lock + SetVisibility.Mixed -> R.drawable.format_list_bulleted_type }, ), contentDescription = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt index b0629981c..2cc2c35f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetsManagementDialog.kt @@ -78,13 +78,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NewSetCreationDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer @@ -97,10 +97,10 @@ fun FollowSetsManagementDialog( accountViewModel: AccountViewModel, navigator: INav, ) { - val followSetViewModel: NostrUserListFeedViewModel = + val followSetViewModel: FollowSetFeedViewModel = viewModel( - key = "NostrUserListFeedViewModel", - factory = NostrUserListFeedViewModel.Factory(accountViewModel.account), + key = "FollowSetFeedViewModel", + factory = FollowSetFeedViewModel.Factory(accountViewModel.account), ) FollowSetsManagementDialog(userHex, followSetViewModel, accountViewModel.account, navigator) @@ -110,7 +110,7 @@ fun FollowSetsManagementDialog( @Composable fun FollowSetsManagementDialog( userHex: String, - followSetsViewModel: NostrUserListFeedViewModel, + followSetsViewModel: FollowSetFeedViewModel, account: Account, navigator: INav, ) { @@ -164,17 +164,17 @@ fun FollowSetsManagementDialog( .imePadding(), ) { when (followSetsState) { - is FollowSetState.Loaded -> { - val lists = (followSetsState as FollowSetState.Loaded).feed + is FollowSetFeedState.Loaded -> { + val lists = (followSetsState as FollowSetFeedState.Loaded).feed lists.forEachIndexed { index, list -> Spacer(StdVertSpacer) FollowSetItem( modifier = Modifier.fillMaxWidth(), listHeader = list.title, - listVisibility = list.visibility, + setVisibility = list.visibility, userName = userInfo.toBestDisplayName(), - isUserInList = list.profileList.contains(userHex), + isUserInList = list.profiles.contains(userHex), onRemoveUser = { Log.d( "Amethyst", @@ -187,7 +187,7 @@ fun FollowSetsManagementDialog( ) Log.d( "Amethyst", - "Updated List. New size: ${list.profileList.size}", + "Updated List. New size: ${list.profiles.size}", ) }, onAddUser = { @@ -198,28 +198,28 @@ fun FollowSetsManagementDialog( followSetsViewModel.addUserToSet(userHex, list, account) Log.d( "Amethyst", - "Updated List. New size: ${list.profileList.size}", + "Updated List. New size: ${list.profiles.size}", ) }, ) } } - FollowSetState.Empty -> { + FollowSetFeedState.Empty -> { EmptyOrNoneFound { followSetsViewModel.refresh() } } - is FollowSetState.FeedError -> { - val errorMsg = (followSetsState as FollowSetState.FeedError).errorMessage + is FollowSetFeedState.FeedError -> { + val errorMsg = (followSetsState as FollowSetFeedState.FeedError).errorMessage ErrorMessage(errorMsg) { followSetsViewModel.refresh() } } - FollowSetState.Loading -> { + FollowSetFeedState.Loading -> { Loading() } } - if (followSetsState != FollowSetState.Loading) { + if (followSetsState != FollowSetFeedState.Loading) { FollowSetsCreationMenu( userName = userInfo.toBestDisplayName(), onSetCreate = { setName, setIsPrivate, description -> @@ -304,7 +304,7 @@ private fun ErrorMessage( fun FollowSetItem( modifier: Modifier = Modifier, listHeader: String, - listVisibility: ListVisibility, + setVisibility: SetVisibility, userName: String, isUserInList: Boolean, onAddUser: () -> Unit, @@ -330,21 +330,21 @@ fun FollowSetItem( ) { Text(listHeader, fontWeight = FontWeight.Bold) Spacer(modifier = StdHorzSpacer) - listVisibility.let { + setVisibility.let { val text by derivedStateOf { when (it) { - ListVisibility.Public -> stringRes(context, R.string.follow_set_type_public) - ListVisibility.Private -> stringRes(context, R.string.follow_set_type_private) - ListVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) + SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public) + SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private) + SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) } } Icon( painter = painterResource( - when (listVisibility) { - ListVisibility.Public -> R.drawable.ic_public - ListVisibility.Private -> R.drawable.lock - ListVisibility.Mixed -> R.drawable.format_list_bulleted_type + when (setVisibility) { + SetVisibility.Public -> R.drawable.ic_public + SetVisibility.Private -> R.drawable.lock + SetVisibility.Mixed -> R.drawable.format_list_bulleted_type }, ), contentDescription = stringRes(R.string.follow_set_type_description, text), From b694e143d0005cdacdc49ec70df3ab5614bdc62e Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 25 Sep 2025 21:37:33 +0100 Subject: [PATCH 05/24] Finish renaming refactor. --- .../screen/loggedIn/lists/FollowSetFeedView.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt index 4e524a0d8..1ecaca898 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -46,17 +47,17 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @Composable fun FollowSetFeedView( modifier: Modifier = Modifier, - followSetState: FollowSetState, + followSetFeedState: FollowSetFeedState, onRefresh: () -> Unit = {}, onOpenItem: (String) -> Unit = {}, onRenameItem: (targetSet: FollowSet, newName: String) -> Unit, onDeleteItem: (followSet: FollowSet) -> Unit, ) { - when (followSetState) { - FollowSetState.Loading -> LoadingFeed() + when (followSetFeedState) { + FollowSetFeedState.Loading -> LoadingFeed() - is FollowSetState.Loaded -> { - val followSetFeed = followSetState.feed + is FollowSetFeedState.Loaded -> { + val followSetFeed = followSetFeedState.feed FollowSetLoaded( loadedFeedState = followSetFeed, onRefresh = onRefresh, @@ -66,7 +67,7 @@ fun FollowSetFeedView( ) } - is FollowSetState.Empty -> { + is FollowSetFeedState.Empty -> { FollowSetFeedEmpty( message = stringRes(R.string.follow_set_empty_feed_msg), ) { @@ -74,9 +75,9 @@ fun FollowSetFeedView( } } - is FollowSetState.FeedError -> + is FollowSetFeedState.FeedError -> FeedError( - followSetState.errorMessage, + followSetFeedState.errorMessage, ) { onRefresh() } From 6368d17d09a5017a2c8ad7aa06ae52c8c5320c4a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 25 Sep 2025 21:39:02 +0100 Subject: [PATCH 06/24] Refactor: Create FollowSetState and move functionality from other places to it. Modify usages accordingly. --- .../vitorpamplona/amethyst/model/Account.kt | 39 +------- .../nip51Lists/followSets/FollowSetState.kt | 91 +++++++++++++++++++ .../amethyst/ui/dal/FollowSetFeedFilter.kt | 14 +-- 3 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt 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 05abfd8e3..fbeddd2fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable -import androidx.compose.ui.util.fastAny import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -58,6 +57,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayLis import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache @@ -94,7 +94,6 @@ import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent @@ -217,8 +216,6 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import java.math.BigDecimal import java.util.Locale import kotlin.coroutines.cancellation.CancellationException @@ -270,6 +267,7 @@ class Account( val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) val kind3FollowList = FollowListState(signer, cache, scope, settings) + val followSetsState = FollowSetState(signer, cache, scope) val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer) val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings) @@ -322,7 +320,7 @@ class Account( val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow // Merges all follow lists to create a single All Follows feed. - val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) + val allFollows = MergedFollowListsState(kind3FollowList, followSetsState, hashtagList, geohashList, communityList, scope) val privateDMDecryptionCache = PrivateDMCache(signer) val privateZapsDecryptionCache = PrivateZapCache(signer) @@ -834,37 +832,6 @@ class Account( fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic) - suspend fun getFollowSetNotes() = - withContext(Dispatchers.Default) { - val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile()) - Log.d(this@Account.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") - return@withContext followSetNotes - } - - fun isUserInFollowSets(user: User): Boolean = - runBlocking(scope.coroutineContext) { - LocalCache.getFollowSetNotesFor(userProfile()).fastAny { it -> - val listEvent = it.event as PeopleListEvent - val isInPublicSets = - listEvent - .publicPeople() - .fastAny { it.toTagArray().value() == user.pubkeyHex } - val isInPrivateSets = - listEvent - .privatePeople(signer) - ?.fastAny { it.toTagArray().value() == user.pubkeyHex } ?: false - - isInPublicSets || isInPrivateSets - } - } - - fun mapNoteToFollowSet(note: Note): FollowSet = - FollowSet - .mapEventToSet( - event = note.event as PeopleListEvent, - signer, - ) - suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user)) suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt new file mode 100644 index 000000000..85977a43c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt @@ -0,0 +1,91 @@ +/** + * 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.model.nip51Lists.followSets + +import androidx.compose.ui.util.fastAny +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.core.value +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +class FollowSetState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, +) { + val user = cache.getOrCreateUser(signer.pubKey) + + suspend fun getFollowSetNotes() = + withContext(Dispatchers.Default) { + val followSetNotes = LocalCache.getFollowSetNotesFor(user) + Log.d(this.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") + return@withContext followSetNotes + } + + private fun getFollowSetNotesFlow() = + flow { + val followSetNotes = getFollowSetNotes() + val followSets = followSetNotes.map { mapNoteToFollowSet(it) } + emit(followSets) + }.flowOn(Dispatchers.IO) + + val profilesFlow = + getFollowSetNotesFlow() + .map { it -> + it.flatMapTo(mutableSetOf()) { it.profiles }.toSet() + }.stateIn(scope, SharingStarted.Eagerly, emptySet()) + + fun isUserInFollowSets(user: User): Boolean = + runBlocking(scope.coroutineContext) { + LocalCache.getFollowSetNotesFor(user).fastAny { it -> + val listEvent = it.event as PeopleListEvent + val isInPublicSets = + listEvent + .publicPeople() + .fastAny { it.toTagArray().value() == user.pubkeyHex } + val isInPrivateSets = + listEvent + .privatePeople(signer) + ?.fastAny { it.toTagArray().value() == user.pubkeyHex } ?: false + + isInPublicSets || isInPrivateSets + } + } + + fun mapNoteToFollowSet(note: Note): FollowSet = + FollowSet + .mapEventToSet( + event = note.event as PeopleListEvent, + signer, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt index 04eea40ce..7ca7271e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt @@ -21,20 +21,20 @@ package com.vitorpamplona.amethyst.ui.dal import android.util.Log -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState import kotlinx.coroutines.runBlocking class FollowSetFeedFilter( - val account: Account, + val followSetState: FollowSetState, ) : FeedFilter() { - override fun feedKey(): String = account.userProfile().pubkeyHex + "-followsets" + override fun feedKey(): String = followSetState.user.pubkeyHex + "-followsets" override fun feed(): List = - runBlocking(account.scope.coroutineContext) { + runBlocking(followSetState.scope.coroutineContext) { try { - val fetchedSets = account.getFollowSetNotes() - val followSets = fetchedSets.map { account.mapNoteToFollowSet(it) } + val fetchedSets = followSetState.getFollowSetNotes() + val followSets = fetchedSets.map { followSetState.mapNoteToFollowSet(it) } println("Updated follow set size for feed filter: ${followSets.size}") followSets } catch (e: Exception) { From 4fc93863935f2660a56003f45270a19210f7a8cc Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 25 Sep 2025 21:58:16 +0200 Subject: [PATCH 07/24] Combine calculation of videoBitrateInBps, resizer to use compressionRules only once --- .../service/uploads/VideoCompressionHelper.kt | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index f2eeaab2e..6f6b929df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -92,7 +92,7 @@ data class CompressionRule( // Apply 1.5x multiplier for 60fps+ videos val multiplier = if (framerate >= 60f) 1.5f else 1.0f - return (bitrateMbps * multiplier).toInt() * MBPS_TO_BPS_MULTIPLIER + return (bitrateMbps * multiplier * MBPS_TO_BPS_MULTIPLIER).toInt() } } @@ -145,36 +145,28 @@ object VideoCompressionHelper { ): MediaCompressorResult { val videoInfo = getVideoInfo(uri, applicationContext) - val videoBitrateInBps = + val (videoBitrateInBps, resizer) = videoInfo?.let { info -> - val bitrateBps = + val rule = compressionRules .getValue(mediaQuality) .getValue(info.resolution.getStandard()) - .getBitrateBps(info.framerate) + val bitrateBps = rule.getBitrateBps(info.framerate) Log.d(LOG_TAG, "Bitrate: ${bitrateBps}bps for ${info.resolution.getStandard()} quality=$mediaQuality framerate=${info.framerate}fps.") - bitrateBps - } ?: run { - Log.w(LOG_TAG, "Video bitrate fallback: 2Mbps (videoInfo unavailable)") - 2 * MBPS_TO_BPS_MULTIPLIER - } - val resizer = - if (videoInfo != null) { - val rules = - compressionRules - .getValue(mediaQuality) - .getValue(videoInfo.resolution.getStandard()) Log.d( LOG_TAG, - "Resizer: ${videoInfo.resolution.width}x${videoInfo.resolution.height} -> " + - "${rules.width}x${rules.height} (${rules.description})", + "Resizer: ${info.resolution.width}x${info.resolution.height} -> " + + "${rule.width}x${rule.height} (${rule.description})", ) - VideoResizer.limitSize(rules.width.toDouble(), rules.height.toDouble()) - } else { + val resizer = VideoResizer.limitSize(rule.width.toDouble(), rule.height.toDouble()) + + Pair(bitrateBps, resizer) + } ?: run { + Log.w(LOG_TAG, "Video bitrate fallback: 2Mbps (videoInfo unavailable)") Log.d(LOG_TAG, "Resizer: null (original resolution preserved)") - null + Pair(2 * MBPS_TO_BPS_MULTIPLIER, null) } // Get original file size safely From 9c024e334be4f89fe930686adcd9ac142f297853 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 26 Sep 2025 12:24:42 +0100 Subject: [PATCH 08/24] Replace first hack with a better solution for determining a follow. Add FollowSetState to MergedFollowListsState and modify it to take into account users from follow sets when displaying a user's follows feed. --- .../nip51Lists/followSets/FollowSetState.kt | 55 ++++++++++--------- .../serverList/MergedFollowListsState.kt | 11 +++- .../reqCommand/user/UserObservers.kt | 7 ++- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt index 85977a43c..f2d096405 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt @@ -20,22 +20,25 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.followSets -import androidx.compose.ui.util.fastAny import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.quartz.nip01Core.core.value import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class FollowSetState( @@ -44,20 +47,24 @@ class FollowSetState( val scope: CoroutineScope, ) { val user = cache.getOrCreateUser(signer.pubKey) + private val isActive = MutableStateFlow(false) suspend fun getFollowSetNotes() = withContext(Dispatchers.Default) { val followSetNotes = LocalCache.getFollowSetNotesFor(user) - Log.d(this.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") + Log.d(this@FollowSetState.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") return@withContext followSetNotes } private fun getFollowSetNotesFlow() = flow { - val followSetNotes = getFollowSetNotes() - val followSets = followSetNotes.map { mapNoteToFollowSet(it) } - emit(followSets) - }.flowOn(Dispatchers.IO) + while (isActive.value) { + val followSetNotes = getFollowSetNotes() + val followSets = followSetNotes.map { mapNoteToFollowSet(it) } + emit(followSets) + delay(1000) + } + }.flowOn(Dispatchers.Default) val profilesFlow = getFollowSetNotesFlow() @@ -65,27 +72,25 @@ class FollowSetState( it.flatMapTo(mutableSetOf()) { it.profiles }.toSet() }.stateIn(scope, SharingStarted.Eagerly, emptySet()) - fun isUserInFollowSets(user: User): Boolean = - runBlocking(scope.coroutineContext) { - LocalCache.getFollowSetNotesFor(user).fastAny { it -> - val listEvent = it.event as PeopleListEvent - val isInPublicSets = - listEvent - .publicPeople() - .fastAny { it.toTagArray().value() == user.pubkeyHex } - val isInPrivateSets = - listEvent - .privatePeople(signer) - ?.fastAny { it.toTagArray().value() == user.pubkeyHex } ?: false - - isInPublicSets || isInPrivateSets - } - } - fun mapNoteToFollowSet(note: Note): FollowSet = FollowSet .mapEventToSet( event = note.event as PeopleListEvent, signer, ) + + fun isUserInFollowSets(user: User): Boolean = profilesFlow.value.contains(user.pubkeyHex) + + init { + isActive.update { true } + scope.launch(Dispatchers.Default) { + getFollowSetNotesFlow() + .onCompletion { + isActive.update { false } + }.catch { + Log.e(this@FollowSetState.javaClass.simpleName, "Error on flow collection: ${it.message}") + isActive.update { false } + }.collect {} + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index 68b7956ba..f155da664 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.stateIn class MergedFollowListsState( val kind3List: FollowListState, + val followSetList: FollowSetState, val hashtagList: HashtagListState, val geohashList: GeohashListState, val communityList: CommunityListState, @@ -44,12 +46,13 @@ class MergedFollowListsState( ) { fun mergeLists( kind3: FollowListState.Kind3Follows, + followSetProfiles: Set, hashtags: Set, geohashes: Set, community: Set, ): FollowListState.Kind3Follows = FollowListState.Kind3Follows( - kind3.authors, + kind3.authors + followSetProfiles, kind3.authorsPlusMe, kind3.hashtags + hashtags, kind3.geotags + geohashes, @@ -59,15 +62,17 @@ class MergedFollowListsState( val flow: StateFlow = combine( kind3List.flow, + followSetList.profilesFlow, hashtagList.flow, geohashList.flow, communityList.flow, - ) { kind3, hashtag, geohash, community -> - mergeLists(kind3, hashtag, geohash, community) + ) { kind3, followSet, hashtag, geohash, community -> + mergeLists(kind3, followSet, hashtag, geohash, community) }.onStart { emit( mergeLists( kind3List.flow.value, + followSetList.profilesFlow.value, hashtagList.flow.value, geohashList.flow.value, communityList.flow.value, 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 b2ece59d3..99045a72e 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 @@ -411,6 +411,8 @@ fun observeUserIsFollowing( ): State { // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user1, accountViewModel) + val isUserInFollowSets = accountViewModel.account.followSetsState.isUserInFollowSets(user2) + println("Is ${user2.toBestDisplayName()} in a Follow set? $isUserInFollowSets") // Subscribe in the LocalCache for changes that arrive in the device val flow = @@ -420,14 +422,13 @@ fun observeUserIsFollowing( .follows.stateFlow .sample(1000) .mapLatest { userState -> - userState.user.isFollowing(user2) || - accountViewModel.account.isUserInFollowSets(user2) + userState.user.isFollowing(user2) || isUserInFollowSets }.distinctUntilChanged() .flowOn(Dispatchers.Default) } return flow.collectAsStateWithLifecycle( - user1.isFollowing(user2) || accountViewModel.account.isUserInFollowSets(user2), + user1.isFollowing(user2) || isUserInFollowSets, ) } From 45192d1cf75d8f8682f2fc7b23c760d139285c22 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 26 Sep 2025 12:32:44 +0100 Subject: [PATCH 09/24] Remove statements to avoid polluting the logs. Modify the timing on the flow producer. --- .../amethyst/model/nip51Lists/followSets/FollowSetState.kt | 3 +-- .../service/relayClient/reqCommand/user/UserObservers.kt | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt index f2d096405..120c651a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt @@ -52,7 +52,6 @@ class FollowSetState( suspend fun getFollowSetNotes() = withContext(Dispatchers.Default) { val followSetNotes = LocalCache.getFollowSetNotesFor(user) - Log.d(this@FollowSetState.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") return@withContext followSetNotes } @@ -62,7 +61,7 @@ class FollowSetState( val followSetNotes = getFollowSetNotes() val followSets = followSetNotes.map { mapNoteToFollowSet(it) } emit(followSets) - delay(1000) + delay(2000) } }.flowOn(Dispatchers.Default) 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 99045a72e..7ac6f5b0f 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 @@ -412,7 +412,6 @@ fun observeUserIsFollowing( // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user1, accountViewModel) val isUserInFollowSets = accountViewModel.account.followSetsState.isUserInFollowSets(user2) - println("Is ${user2.toBestDisplayName()} in a Follow set? $isUserInFollowSets") // Subscribe in the LocalCache for changes that arrive in the device val flow = From 7ddce17bdde54421a45fd6e3be7a59b7ca78b573 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 26 Sep 2025 12:07:45 +0000 Subject: [PATCH 10/24] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 02f2316e4..f0944f92e 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -1065,4 +1065,5 @@ 要用私信将最近的崩溃报告发送给 Amethyst 吗?不会分享个人信息 发送它 此消息将在 %1$d 天内消失 + 选择签名者 From 61459187d7b852a68c66ae70c9117d78f93f8542 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 26 Sep 2025 15:46:29 +0100 Subject: [PATCH 11/24] Fix duplicate copyright and use derivedStateOf for followSet presence checking variable. --- .../model/nip51Lists/followSets/NostrSet.kt | 20 ------------------- .../nip51Lists/followSets/SetVisibility.kt | 20 ------------------- .../reqCommand/user/UserObservers.kt | 6 +++++- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt index a32258cd7..aadae3380 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/NostrSet.kt @@ -20,26 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.followSets -/** - * 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. - */ sealed class NostrSet( val setVisibility: SetVisibility, val content: Collection, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt index ce418b099..fc8da1658 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/SetVisibility.kt @@ -20,26 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.followSets -/** - * 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. - */ enum class SetVisibility { Public, Private, 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 7ac6f5b0f..68e493060 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 @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Account @@ -411,7 +413,9 @@ fun observeUserIsFollowing( ): State { // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user1, accountViewModel) - val isUserInFollowSets = accountViewModel.account.followSetsState.isUserInFollowSets(user2) + val isUserInFollowSets by remember(accountViewModel.account.followSetsState) { + derivedStateOf { accountViewModel.account.followSetsState.isUserInFollowSets(user2) } + } // Subscribe in the LocalCache for changes that arrive in the device val flow = From d2d811f6703be598f573aa14e0ebcc95677c4578 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 26 Sep 2025 16:21:13 +0100 Subject: [PATCH 12/24] Just use remember{} instead of adding derivedStateOf{} to avoid weird state issues. --- .../service/relayClient/reqCommand/user/UserObservers.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 68e493060..2da58f601 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 @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -413,9 +412,10 @@ fun observeUserIsFollowing( ): State { // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user1, accountViewModel) - val isUserInFollowSets by remember(accountViewModel.account.followSetsState) { - derivedStateOf { accountViewModel.account.followSetsState.isUserInFollowSets(user2) } - } + val isUserInFollowSets = + remember(accountViewModel.account.followSetsState) { + accountViewModel.account.followSetsState.isUserInFollowSets(user2) + } // Subscribe in the LocalCache for changes that arrive in the device val flow = From 26005a062d600b02fa45c8f288f5f9bc6a54ac90 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Sep 2025 13:23:57 -0400 Subject: [PATCH 13/24] Updates Quartz instructions to Maven --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f8cbbdf46..1f476dc63 100644 --- a/README.md +++ b/README.md @@ -242,16 +242,16 @@ openssl base64 < | tr -d '\n' | tee some_signing_key.j Add the following line to your `commonMain` dependencies: ```gradle -implementation('com.github.vitorpamplona.amethyst:quartz:') +implementation('com.vitorpamplona.quartz:quartz:') ``` Variations to each platform are also available: ```gradle -implementation('com.github.vitorpamplona.amethyst:quartz-android:') -implementation('com.github.vitorpamplona.amethyst:quartz-jvm:') -implementation('com.github.vitorpamplona.amethyst:quartz-iosarm64:') -implementation('com.github.vitorpamplona.amethyst:quartz-iossimulatorarm64:') +implementation('com.vitorpamplona.quartz:quartz-android:') +implementation('com.vitorpamplona.quartz:quartz-jvm:') +implementation('com.vitorpamplona.quartz:quartz-iosarm64:') +implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:') ``` ### How to use From 4cffd6d8689fbaa4922fd94f96dafe59934d8cd4 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 29 Sep 2025 17:25:18 +0000 Subject: [PATCH 14/24] New Crowdin translations by GitHub Action --- .../src/main/res/values-hu-rHU/strings.xml | 37 +++++++++++++++++-- .../src/main/res/values-pl-rPL/strings.xml | 32 ++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index a242f754a..836aa6bba 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -112,6 +112,7 @@ Közzététel Mentés Létrehozás + Átnevezés Mégse Nem sikerült feltölteni a képet Atjátszó címe @@ -140,12 +141,12 @@ LN-cím LN-webcím (elavult) Mentés a galériába - Kép elmentve a képgalériába + Kép mentve a képgalériába A videó letöltése megkezdődött… A média letöltése megkezdődött… Nem sikerült menteni a képet - Videó elmentve a videógalériába - Nem sikerült elmenteni a videót + Videó mentve a videógalériába + Nem sikerült menteni a videót Kép feltöltése Kép készítése Videó rögzítése @@ -445,6 +446,33 @@ Közelben lévők bejegyzései Globális Némítottak bejegyzései + Követési gyüjtemények + Címkézett könyvjelzők + Általános könyvjelzők + Nyílvános + Privát + Kevert + Úgy tűnik, hogy még egyetlen gyüjteményt sem követ. + \nKoppintson a frissítéshez vagy érintse meg a hozzáadás gombot a gyüjtemény létrehozásához. + + Szerző hozzáadása a követési gyűjteményhez + Felhasználó hozzáadása vagy eltávolítása a listákból, vagy új lista létrehozása ezzel a felhasználóval. + Ikon a(z) %1$s nevű listához + "A(z) %1$s már a létezik a listában" + "A(z) %1$s nincs a listában" + Saját követési gyüjtemények + Nem találhatók követési gyüjtemények, vagy nincs követési gyüjteménye. Érintse meg az alábbi gombot a frissítéshez vagy használja a menüt egy gyüjtemény létrehozásához. + Probléma történt a következő lekérdezésekor: %1$s + Új lista létrehozása + Új %1$s lista létrehozása felhasználóval + %1$s követési gyüjtemény létrehozása, és hozzáadás a következhöz: %2$s. + Új %1$s lista + Név megadása + Leírás megadása (nem kötelező) + Gyüjtemény létrehozása + Gyüjtemény átnevezése + Ön átnevezi a követési gyüjteményt erről: + erre: Alapértelmezett port: 9050 ## Kapcsolódás a TORon keresztül az Orbot segítségével \n\n1. Telepítse az [Orbotot](https://play.google.com/store/apps/details?id=org.torproject.android) @@ -1014,6 +1042,8 @@ Letöltés Nem sikerült megnyitni a fájlt A fájl megnyitásához és letöltéséhez nincsenek torrent-alkalmazások telepítve. + Az esemény nem tartalmaz elegendő információt a mágneshivatkozás létrehozásához + Saját lista/gyüjtemény Lista kiválasztása a hírfolyam szűréséhez Kijelentkeztetés az eszköz zárolása esetén Privát üzenet @@ -1035,4 +1065,5 @@ Szeretné elküldeni a legutóbbi összeomlási jelentést az Amethystnek egy közvetlen üzenetben? A személyes adatait nem osztja meg Küldés Ez az üzenet %1$d nap múlva eltűnik + Aláíró kiválasztása diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 82fe2b56d..9e145e1a3 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -112,6 +112,7 @@ Wyślij Zapisz Utwórz + Zmień nazwę Anuluj Nie udało się przesłać obrazu Adres transmitera @@ -442,6 +443,33 @@ W pobliżu Wszystkie Zablokowane + Zbiory obserwowanych + Oznaczone zakładki + Ogólne zakładki + Publiczna + Prywatna + Mieszana + Wygląda na to, że nie masz jeszcze żadnych zbiorów obserwowanych. + \nDotknij poniżej, aby odświeżyć, lub naciśnij przycisk Dodaj, aby utworzyć nowy. + + Dodaj autora do zbioru obserwowanych + Dodaj lub usuń użytkownika z list, lub utwórz nową listę z tym użytkownikiem. + Ikona dla listy %1$s + "%1$s jest obecny na liście" + "%1$s nie jest na liście" + Twój zbiór obserwowanych + Nie znaleziono zbiorów obserwowanych lub nie masz żadnych zbiorów obserwowanych. Dotknij poniżej, aby odświeżyć lub użyj menu, aby go utworzyć. + Podczas pobierania wystąpił błąd: %1$s + Utwórz nową listę + \"Utwórz nową listę %1$s z użytkownikiem + Tworzy zbiór obserwowanych %1$s i dodaje do niego %2$s. + Nowa lista %1$s + Nazwa zbioru + Opis zbioru (opcjonalnie) + Utwórz zbiór + Zmień nazwę zbioru + Zmieniasz nazwę z + do.. Domyślny port to 9050 ## Połącz przez Tor z Orbotem \n\n1. Zainstaluj [Orbota](https://play.google. om/store/apps/details?id= org.torproject.android) @@ -1011,6 +1039,8 @@ Pobierz Nie udało się otworzyć pliku Brak zainstalowanych aplikacji torrent do otwarcia i pobrania pliku. + Zdarzenie nie ma wystarczającej ilości informacji, aby zbudować link magnetyczny + Moje Listy/Zbiory Wybierz listę, aby filtrować aktualności Wyloguj się przy blokowaniu urządzenia Wiadomość prywatna @@ -1018,6 +1048,7 @@ Transmiter Czatu Transmiter, z którym łączą się wszyscy użytkownicy tego czatu Udostępnij zdjęcie… + Nie można udostępnić obrazu, spróbuj ponownie później… Szukaj tagu: #%1$s Nie tłumacz z Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych. @@ -1031,4 +1062,5 @@ Czy chcesz wysłać ostatni raport o awarii do Amethyst w DM? Żadne dane osobowe nie będą udostępnione Prześlij Ta wiadomość zniknie za %1$d dni + Wybierz Sygnatariusza From a6e306f3deb3ceb9b997a872ea19ffa0ea286614 Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 30 Sep 2025 22:02:18 +0200 Subject: [PATCH 15/24] Update to use lightcompressor library version that supports h265 Add h265 toggle to NewMediaView.kt and ImageVideoDescription.kt Touch many files to add optional h265 boolean (default is false) --- amethyst/build.gradle | 6 ++ .../service/uploads/MediaCompressor.kt | 3 +- .../service/uploads/MultiOrchestrator.kt | 6 ++ .../service/uploads/UploadOrchestrator.kt | 9 ++- .../service/uploads/VideoCompressionHelper.kt | 3 + .../amethyst/ui/actions/EditPostView.kt | 2 +- .../amethyst/ui/actions/NewMediaModel.kt | 4 ++ .../amethyst/ui/actions/NewMediaView.kt | 14 ++++ .../creators/uploads/ImageVideoDescription.kt | 65 +++++++++++-------- .../nip22Comments/GenericCommentPostScreen.kt | 2 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 2 +- .../nip99Classifieds/NewProductScreen.kt | 2 +- .../loggedIn/home/ShortNotePostScreen.kt | 2 +- .../publicMessages/NewPublicMessageScreen.kt | 2 +- amethyst/src/main/res/values/strings.xml | 2 + gradle/libs.versions.toml | 2 +- 16 files changed, 88 insertions(+), 38 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index e700680f7..2845df088 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -151,6 +151,12 @@ android { signingConfig = signingConfigs.debug } } + // TODO: remove this when lightcompressor uses one MP4 parser only + packaging { + resources { + resources.pickFirsts.add('builddef.lst') + } + } flavorDimensions = ["channel"] diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt index 0b506c4dc..ee7f51d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt @@ -45,6 +45,7 @@ class MediaCompressor { contentType: String?, mediaQuality: CompressorQuality, applicationContext: Context, + useH265: Boolean = false, ): MediaCompressorResult { // Skip compression if user selected uncompressed if (mediaQuality == CompressorQuality.UNCOMPRESSED) { @@ -57,7 +58,7 @@ class MediaCompressor { // branch into compression based on content type return when { contentType?.startsWith("video", ignoreCase = true) == true -> { - VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality) + VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality, useH265) } contentType?.startsWith("image", ignoreCase = true) == true && !contentType.contains("gif") && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt index 45d8269e2..207810b1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt @@ -46,6 +46,8 @@ class MultiOrchestrator( fun first() = list.first() + fun hasVideo() = list.any { it.media.mimeType?.startsWith("video", ignoreCase = true) == true } + suspend fun upload( alt: String?, contentWarningReason: String?, @@ -53,6 +55,7 @@ class MultiOrchestrator( server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): Result { coroutineScope { val jobs = @@ -67,6 +70,7 @@ class MultiOrchestrator( server, account, context, + useH265, ) } } @@ -85,6 +89,7 @@ class MultiOrchestrator( server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): Result { coroutineScope { val jobs = @@ -100,6 +105,7 @@ class MultiOrchestrator( server, account, context, + useH265, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index dd011f74f..42a763dee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -288,9 +288,10 @@ class UploadOrchestrator { mimeType: String?, compressionQuality: CompressorQuality, context: Context, + useH265: Boolean = false, ) = if (compressionQuality != CompressorQuality.UNCOMPRESSED) { updateState(0.02, UploadingState.Compressing) - MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext) + MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265) } else { MediaCompressorResult(uri, mimeType, null) } @@ -304,8 +305,9 @@ class UploadOrchestrator { server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context) + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265) return when (server.type) { ServerType.NIP95 -> uploadNIP95(compressed.uri, compressed.contentType, null, null, context) @@ -324,8 +326,9 @@ class UploadOrchestrator { server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context) + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265) val encrypted = EncryptFiles().encryptFile(context, compressed.uri, encrypt) return when (server.type) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index 6f6b929df..adab194f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -29,6 +29,7 @@ import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import com.abedelazizshe.lightcompressorlibrary.CompressionListener +import com.abedelazizshe.lightcompressorlibrary.VideoCodec import com.abedelazizshe.lightcompressorlibrary.VideoCompressor import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration import com.abedelazizshe.lightcompressorlibrary.config.Configuration @@ -141,6 +142,7 @@ object VideoCompressionHelper { contentType: String?, applicationContext: Context, mediaQuality: CompressorQuality, + useH265: Boolean = false, timeoutMs: Long = 60_000L, // configurable, default 60s ): MediaCompressorResult { val videoInfo = getVideoInfo(uri, applicationContext) @@ -186,6 +188,7 @@ object VideoCompressionHelper { resizer = resizer, videoNames = listOf(UUID.randomUUID().toString()), isMinBitrateCheckEnabled = false, + videoCodec = if (useH265) VideoCodec.H265 else VideoCodec.H264, ), listener = object : CompressionListener { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 66f922c90..ef1f45c27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -261,7 +261,7 @@ fun EditPostView( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 630f52573..b384c5503 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -62,6 +62,9 @@ open class NewMediaModel : ViewModel() { // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED var mediaQualitySlider by mutableIntStateOf(1) + // Codec selection: false = H264, true = H265 + var useH265Codec by mutableStateOf(false) + open fun load( account: Account, uris: ImmutableList, @@ -111,6 +114,7 @@ open class NewMediaModel : ViewModel() { serverToUse, myAccount, context, + useH265Codec, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 899c3c59c..1af6109fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -261,4 +261,18 @@ fun ImageVideoPost( steps = 2, ) } + + // Only show H.265 codec option if there are videos in the upload + if (postViewModel.multiOrchestrator?.hasVideo() == true) { + SettingSwitchItem( + title = R.string.video_codec_h265_label, + description = R.string.video_codec_h265_description, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + checked = postViewModel.useH265Codec, + onCheckedChange = { postViewModel.useH265Codec = it }, + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index 0d3b1052a..84e445182 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -78,7 +78,7 @@ import kotlinx.collections.immutable.toImmutableList fun ImageVideoDescription( uris: MultiOrchestrator, defaultServer: ServerName, - onAdd: (String, ServerName, Boolean, Int) -> Unit, + onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit, onDelete: (SelectedMediaProcessing) -> Unit, onCancel: () -> Unit, accountViewModel: AccountViewModel, @@ -91,7 +91,7 @@ fun ImageVideoDescription( uris: MultiOrchestrator, defaultServer: ServerName, includeNIP95: Boolean, - onAdd: (String, ServerName, Boolean, Int) -> Unit, + onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit, onDelete: (SelectedMediaProcessing) -> Unit, onCancel: () -> Unit, accountViewModel: AccountViewModel, @@ -128,6 +128,9 @@ fun ImageVideoDescription( // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED var mediaQualitySlider by remember { mutableIntStateOf(1) } + // Codec selection: false = H264, true = H265 + var useH265Codec by remember { mutableStateOf(false) } + Column( modifier = Modifier @@ -294,32 +297,40 @@ fun ImageVideoDescription( } } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box(modifier = Modifier.fillMaxWidth()) { - Text( - text = - when (mediaQualitySlider) { - 0 -> stringRes(R.string.media_compression_quality_low) - 1 -> stringRes(R.string.media_compression_quality_medium) - 2 -> stringRes(R.string.media_compression_quality_high) - 3 -> stringRes(R.string.media_compression_quality_uncompressed) - else -> stringRes(R.string.media_compression_quality_medium) - }, - modifier = Modifier.align(Alignment.Center), - ) - } - - Slider( - value = mediaQualitySlider.toFloat(), - onValueChange = { mediaQualitySlider = it.toInt() }, - valueRange = 0f..3f, - steps = 2, + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = + when (mediaQualitySlider) { + 0 -> stringRes(R.string.media_compression_quality_low) + 1 -> stringRes(R.string.media_compression_quality_medium) + 2 -> stringRes(R.string.media_compression_quality_high) + 3 -> stringRes(R.string.media_compression_quality_uncompressed) + else -> stringRes(R.string.media_compression_quality_medium) + }, + modifier = Modifier.align(Alignment.Center), ) } + + Slider( + value = mediaQualitySlider.toFloat(), + onValueChange = { mediaQualitySlider = it.toInt() }, + valueRange = 0f..3f, + steps = 2, + ) + } + + if (uris.first().media.isVideo() == true) { + SettingSwitchItem( + title = R.string.video_codec_h265_label, + description = R.string.video_codec_h265_description, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + checked = useH265Codec, + onCheckedChange = { useH265Codec = it }, + ) } Button( @@ -327,7 +338,7 @@ fun ImageVideoDescription( Modifier .fillMaxWidth() .padding(vertical = 10.dp), - onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider) }, + onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) }, shape = QuoteBorder, colors = ButtonDefaults.buttonColors( 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 c692fc600..e53d66b46 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 @@ -285,7 +285,7 @@ private fun GenericCommentPostBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) 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 f339704eb..cb2c7965e 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 @@ -278,7 +278,7 @@ fun GroupDMScreenContent( ImageVideoDescription( selectedFiles, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.uploadAndHold( accountViewModel.toastManager::toast, context, 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 f12efbad8..b0c6b8823 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 @@ -266,7 +266,7 @@ private fun NewProductBody( uris = it, defaultServer = accountViewModel.account.settings.defaultFileServer, includeNIP95 = false, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) 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 1929fc017..4a205f158 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 @@ -326,7 +326,7 @@ private fun NewPostScreenBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) 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 09d47edbb..a4d3100cd 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 @@ -231,7 +231,7 @@ fun PublicMessageScreenContent( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9c6f4283c..438dab8e3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1039,6 +1039,8 @@ Medium High Uncompressed + Use H.265/HEVC Codec + Better quality at smaller file sizes but not all devices support H.265 playback. Edit draft diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 691f31f5f..ca144d44a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.9.4" -lightcompressor = "1.4.0" +lightcompressor = "51defaaa8d" markdown = "e1151c8" media3 = "1.8.0" mockk = "1.14.5" From 7d69122409dd46e94ee2f37baae5ef286162e95a Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 30 Sep 2025 22:07:15 +0200 Subject: [PATCH 16/24] remove unused imports --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 1 - .../service/relayClient/reqCommand/user/UserObservers.kt | 1 - .../com/vitorpamplona/amethyst/ui/actions/EditPostView.kt | 5 ++--- 3 files changed, 2 insertions(+), 5 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 96b3adfb6..f888022cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -118,7 +118,6 @@ import com.vitorpamplona.quartz.experimental.profileGallery.mimeType import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.value import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle 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 2da58f601..77bcab70f 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 @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index ef1f45c27..430737ac9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -54,7 +54,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -255,7 +254,7 @@ fun EditPostView( postViewModel.multiOrchestrator?.let { Row( - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), ) { ImageVideoDescription( @@ -279,7 +278,7 @@ fun EditPostView( if (lud16 != null && postViewModel.wantsInvoice) { Row( - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), ) { InvoiceRequest( From 4554813d724de7b3ba0d92833023587ad1299e77 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 1 Oct 2025 11:28:31 +0200 Subject: [PATCH 17/24] Add codec based multiplier (h265 = 0.7x) --- .../service/uploads/VideoCompressionHelper.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index adab194f9..a581adacc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -89,11 +89,18 @@ data class CompressionRule( val bitrateMbps: Float, val description: String, ) { - fun getBitrateBps(framerate: Float): Int { - // Apply 1.5x multiplier for 60fps+ videos - val multiplier = if (framerate >= 60f) 1.5f else 1.0f + fun getBitrateBps( + framerate: Float, + useH265: Boolean, + ): Int { + // Apply 1.3x multiplier for 60fps+ videos, 0.7x multiplier for H265 + val framerateMultiplier = if (framerate >= 60f) 1.3f else 1.0f + val codecMultiplier = if (useH265) 0.7f else 1.0f + val finalMultiplier = framerateMultiplier * codecMultiplier - return (bitrateMbps * multiplier * MBPS_TO_BPS_MULTIPLIER).toInt() + Log.d("VideoCompressionHelper", "framerate: $framerate, useH265: $useH265, Bitrate multiplier: $finalMultiplier") + + return (bitrateMbps * finalMultiplier * MBPS_TO_BPS_MULTIPLIER).toInt() } } @@ -154,8 +161,8 @@ object VideoCompressionHelper { .getValue(mediaQuality) .getValue(info.resolution.getStandard()) - val bitrateBps = rule.getBitrateBps(info.framerate) - Log.d(LOG_TAG, "Bitrate: ${bitrateBps}bps for ${info.resolution.getStandard()} quality=$mediaQuality framerate=${info.framerate}fps.") + val bitrateBps = rule.getBitrateBps(info.framerate, useH265) + Log.d(LOG_TAG, "Bitrate: ${bitrateBps}bps for ${info.resolution.getStandard()} quality=$mediaQuality framerate=${info.framerate}fps useH265=$useH265.") Log.d( LOG_TAG, From 59be0986ebf7627b9096949f8a2284162518b761 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 1 Oct 2025 15:44:29 +0200 Subject: [PATCH 18/24] Fix bug: useH265 parameter wasn't passed to compressor --- .../amethyst/service/uploads/VideoCompressionHelper.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt | 4 ++++ .../ui/note/creators/contentWarning/SettingSwitchItem.kt | 2 +- .../amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt | 4 ++-- .../ui/screen/loggedIn/home/ShortNotePostViewModel.kt | 5 ++++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index a581adacc..2369431f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -149,7 +149,7 @@ object VideoCompressionHelper { contentType: String?, applicationContext: Context, mediaQuality: CompressorQuality, - useH265: Boolean = false, + useH265: Boolean, timeoutMs: Long = 60_000L, // configurable, default 60s ): MediaCompressorResult { val videoInfo = getVideoInfo(uri, applicationContext) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 9f261fe9e..da02defbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -87,6 +87,9 @@ open class EditPostViewModel : ViewModel() { // Images and Videos var multiOrchestrator by mutableStateOf(null) + // Codec selection: false = H264, true = H265 + var useH265Codec by mutableStateOf(false) + // Invoices var canAddInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false) @@ -201,6 +204,7 @@ open class EditPostViewModel : ViewModel() { server, myAccount, context, + useH265Codec, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/contentWarning/SettingSwitchItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/contentWarning/SettingSwitchItem.kt index c0b6232d7..feaf98419 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/contentWarning/SettingSwitchItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/contentWarning/SettingSwitchItem.kt @@ -75,7 +75,7 @@ fun SettingSwitchItem( text = stringRes(id = description), style = MaterialTheme.typography.bodySmall, color = Color.Gray, - maxLines = 2, + maxLines = 3, overflow = TextOverflow.Ellipsis, ) } 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 4a205f158..7d5df8868 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 @@ -326,8 +326,8 @@ private fun NewPostScreenBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) + onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 -> + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) } 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 d9d05bb3a..847214bca 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 @@ -624,8 +624,9 @@ open class ShortNotePostViewModel : server: ServerName, onError: (title: String, message: String) -> Unit, context: Context, + useH265: Boolean, ) = try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, useH265) } catch (_: SignerExceptions.ReadOnlyException) { onError( stringRes(context, R.string.read_only_user), @@ -640,6 +641,7 @@ open class ShortNotePostViewModel : server: ServerName, onError: (title: String, message: String) -> Unit, context: Context, + useH265: Boolean, ) { viewModelScope.launch(Dispatchers.Default) { val myMultiOrchestrator = multiOrchestrator ?: return@launch @@ -654,6 +656,7 @@ open class ShortNotePostViewModel : server, account, context, + useH265, ) if (results.allGood) { From 7c41da0c4c50cec05a102db886fa54e64be14829 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 1 Oct 2025 16:57:21 +0200 Subject: [PATCH 19/24] Update lightcompressor to release 1.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca144d44a..f835e77be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.9.4" -lightcompressor = "51defaaa8d" +lightcompressor = "1.5.0" markdown = "e1151c8" media3 = "1.8.0" mockk = "1.14.5" From ec82f89225cfd636d1733b8e9d36b92534298149 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 1 Oct 2025 15:30:00 +0000 Subject: [PATCH 20/24] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hi-rIN/strings.xml | 1 + amethyst/src/main/res/values-sl-rSI/strings.xml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 8e6ffcb6a..e1db6d5b0 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1065,4 +1065,5 @@ क्या आप निकटकालिक क्रमदोष सूचनापत्र एक सीधे सन्देश में अमेथिस्ट को भेजना चाहते हैं। कोई व्यक्तिगत जानकारी बाँटी नहीं जाएगी भेजें यह सन्देश %1$d दिनों में अदृश्य हो जाएगा + हस्ताक्षरकर्ता चुनें diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index 7ed17546e..a5ad70e03 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -456,6 +456,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem V moji okolici Globalno Spisek utišanih + Zasebno + Mešano Prevzeta vrata so 9050 ## Poveži se preko Tor omrežja z Orbot aplikacijo \n\n1. Namesti [Orbot aplikacijo](https://play.google.com/store/apps/details?id=org.torproject.android) From e72a588dab7f0ddbd36e9252bbfa90f78e376e50 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 2 Oct 2025 19:11:53 +0200 Subject: [PATCH 21/24] update CS, DE, SE, PT --- amethyst/src/main/res/values-cs/strings.xml | 31 +++++++++++++++++++ amethyst/src/main/res/values-de/strings.xml | 31 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 31 +++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 31 +++++++++++++++++++ 4 files changed, 124 insertions(+) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 7d48a433e..1d68921db 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -952,4 +952,35 @@ Pokyny Moderátoři Otevřít rozbalovací nabídku + Sady sledování + Označené záložky + Obecné záložky + Veřejné + Soukromé + Smíšené + Zdá se, že zatím nemáte žádné sady sledování.\nKlepněte níže pro obnovení nebo použijte tlačítko přidat k vytvoření nové. + Přidat autora do sady sledování + Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem. + Ikona pro seznam %1$s + %1$s je v tomto seznamu + %1$s není v tomto seznamu + Vaše sady sledování + Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové. + Došlo k problému při načítání: %1$s + Vytvořit nový seznam + Vytvořit nový seznam %1$s s uživatelem + Vytvoří %1$s sadu sledování a přidá do ní %2$s. + Nový seznam %1$s + Název sady + Popis sady (volitelné) + Vytvořit sadu + Přejmenovat sadu + Přejmenováváte z + na.. + Přejmenovat + Událost nemá dostatek informací pro vytvoření magnet odkazu + Moje seznamy/sady + Vybrat podepisovatele + Použít kodek H.265/HEVC + Lepší kvalita při menší velikosti souboru, ale ne všechna zařízení podporují přehrávání H.265. diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index 6476be0e7..4d90c98c9 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -992,4 +992,35 @@ anz der Bedingungen ist erforderlich Richtlinien Moderatoren Dropdown-Menü öffnen + Folge-Sets + Markierte Lesezeichen + Allgemeine Lesezeichen + Öffentlich + Privat + Gemischt + Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen. + Autor zum Folge-Set hinzufügen + Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen. + Symbol für %1$s-Liste + %1$s ist in dieser Liste + %1$s ist nicht in dieser Liste + Deine Folge-Sets + Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen. + Beim Abrufen ist ein Problem aufgetreten: %1$s + Neue Liste erstellen + Neue %1$s-Liste mit Benutzer erstellen + Erstellt ein %1$s-Folge-Set und fügt %2$s hinzu. + Neue %1$s-Liste + Set-Name + Set-Beschreibung (optional) + Set erstellen + Set umbenennen + Du benennst um von + zu.. + Umbenennen + Das Ereignis enthält nicht genügend Informationen, um einen Magnetlink zu erstellen + Meine Listen/Sets + Signierer auswählen + H.265/HEVC-Codec verwenden + Bessere Qualität bei kleinerer Dateigröße, aber nicht alle Geräte unterstützen die H.265-Wiedergabe. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 3cc81998a..4c5f6cd8f 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1033,4 +1033,35 @@ Gostaria de enviar o relatório de falha recente para o Amethyst em uma DM? Nenhuma informação pessoal será compartilhada Enviar Esta mensagem desaparecerá em %1$d dias + Conjuntos de Seguimento + Favoritos com etiqueta + Favoritos gerais + Público + Privado + Misto + Parece que você ainda não tem conjuntos de seguimento.\nToque abaixo para atualizar ou use o botão de adicionar para criar um novo. + Adicionar autor ao conjunto de seguimento + Adicionar ou remover usuário de listas, ou criar uma nova lista com este usuário. + Ícone da lista %1$s + %1$s está presente nesta lista + %1$s não está nesta lista + Seus conjuntos de seguimento + Nenhum conjunto de seguimento foi encontrado ou você não possui nenhum. Toque abaixo para atualizar ou use o menu para criar um. + Houve um problema ao buscar: %1$s + Criar nova lista + Criar nova lista %1$s com usuário + Cria um conjunto de seguimento %1$s e adiciona %2$s a ele. + Nova lista %1$s + Nome do conjunto + Descrição do conjunto (opcional) + Criar conjunto + Renomear conjunto + Você está renomeando de + para.. + Renomear + O evento não tem informações suficientes para criar um link magnético + Minhas listas/conjuntos + Selecionar assinador + Usar codec H.265/HEVC + Melhor qualidade em arquivos menores, mas nem todos os dispositivos suportam reprodução em H.265. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 3695dd940..ef8f428e4 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1032,4 +1032,35 @@ Vill du skicka den senaste kraschrapporten till Amethyst i ett DM? Ingen personlig information kommer att delas Skicka Detta meddelande försvinner om %1$d dagar + Följ-set + Märkta bokmärken + Allmänna bokmärken + Offentlig + Privat + Blandad + Det verkar som att du inte har några följ-set ännu.\nTryck nedan för att uppdatera eller använd plusknappen för att skapa ett nytt. + Lägg till författare i följ-set + Lägg till eller ta bort användare från listor, eller skapa en ny lista med denna användare. + Ikon för %1$s-lista + %1$s finns i denna lista + %1$s finns inte i denna lista + Dina följ-set + Inga följ-set hittades, eller så har du inga. Tryck nedan för att uppdatera eller använd menyn för att skapa ett. + Ett problem uppstod vid hämtning: %1$s + Skapa ny lista + Skapa ny %1$s-lista med användare + Skapar ett %1$s-följ-set och lägger till %2$s i det. + Ny %1$s-lista + Set-namn + Set-beskrivning (valfritt) + Skapa set + Byt namn på set + Du byter namn från + till.. + Byt namn + Händelsen har inte tillräcklig information för att skapa en magnetlänk + Mina listor/set + Välj signatör + Använd H.265/HEVC-codec + Bättre kvalitet med mindre filstorlek, men inte alla enheter stöder H.265-uppspelning. From 1c82417a0a4a186acdd0a85f6050f8047f4f0b4c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Oct 2025 15:53:10 -0400 Subject: [PATCH 22/24] Updating the documentation on the use of Quartz --- README.md | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1f476dc63..7d847181e 100644 --- a/README.md +++ b/README.md @@ -259,25 +259,25 @@ implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:")) +val keyPair = KeyPair() // creates a random key +val keyPair = KeyPair("hex...".hexToByteArray()) +val keyPair = KeyPair("nsec1...".bechToBytes()) +val keyPair = KeyPair(Nip06().privateKeyFromMnemonic("")) val readOnly = KeyPair(pubKey = "hex...".hexToByteArray()) val readOnly = KeyPair(pubKey = "npub1...".bechToBytes()) ``` -Create signers that can be internal, when you have the private key or when it is a read-only user -or external, when it is controlled by Amber in NIP-55 +Create signers that can be Internal, when you have the private key or a read-only public key, +or External, when it is controlled by Amber in NIP-55. -the `NostrSignerInternal` and `NostrSignerExternal` classes. +Use either the `NostrSignerInternal` or `NostrSignerExternal` class: ```kt val signer = NostrSignerInternal(keyPair) val amberSigner = NostrSignerExternal( pubKey = keyPair.pubKey.toHexKey(), - packageName = signerPackageName, - contentResolver = appContext.contentResolver, + packageName = signerPackageName, // Amber package name + contentResolver = appContext.contentResolver, ) ``` @@ -299,16 +299,19 @@ val client = NostrClient(socketBuilder, appScope) If you want to auth, given a logged-in `signer`: ```kt -val authCoordinator = RelayAuthenticator(client, applicationIOScope) { challenge, relay -> - val authedEvent = RelayAuthEvent.create(relayUrl, challenge, signer) +val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay -> + val authedEvent = RelayAuthEvent.create(relay.url, challenge, signer) client.sendIfExists(authedEvent, relay.url) } ``` -To manage subscriptions, the suggested approach is to use subscriptions in the Application class. +To manage subscriptions, the simplest approach is to build mutable subscriptions in +the Application class. To use the best of the outbox model, this class allows you to +build filters for as many relays as needed. The `NostrClient` will connect to the +complete set of relays for all subscriptions. ```kt -val metadataSub = RelayClientSubscription( +val metadataSub = NostrClientSubscription( client = client, filter = { val filters = listOf( @@ -330,6 +333,14 @@ val metadataSub = RelayClientSubscription( } ``` +In that way, you can simply call `metadataSub.updateFilter()` when you need to update +subscriptions to all relays. Or call `metadataSub.closeSubscription()` to stop the sub +without deleting it. + +When your app goes to the background, you can use NostrClient's `connect` and `disconnect` +methods to stop all communication to relays. Add the `connect` to your `onResume` and `disconnect` +to `onPause` methods. + ## Contributing Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst) From 34cd26b0042c248075684960610ada37f77b4876 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 2 Oct 2025 19:54:33 +0000 Subject: [PATCH 23/24] New Crowdin translations by GitHub Action --- .../src/main/res/values-pt-rBR/strings.xml | 60 +++++++++---------- .../src/main/res/values-sv-rSE/strings.xml | 60 +++++++++---------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 4c5f6cd8f..439c76e55 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -112,6 +112,7 @@ Salvar Salvar Criar + Renomear Cancelar Falha ao enviar imagem Endereço do Relay @@ -443,6 +444,31 @@ Perto de mim Global Lista Silenciada + Conjuntos de Seguimento + Favoritos com etiqueta + Favoritos gerais + Público + Privado + Misto + Parece que você ainda não tem conjuntos de seguimento.\nToque abaixo para atualizar ou use o botão de adicionar para criar um novo. + Adicionar autor ao conjunto de seguimento + Adicionar ou remover usuário de listas, ou criar uma nova lista com este usuário. + Ícone da lista %1$s + "%1$s está presente nesta lista" + "%1$s não está nesta lista" + Seus conjuntos de seguimento + Nenhum conjunto de seguimento foi encontrado ou você não possui nenhum. Toque abaixo para atualizar ou use o menu para criar um. + Houve um problema ao buscar: %1$s + Criar nova lista + Criar nova lista %1$s com usuário + Cria um conjunto de seguimento %1$s e adiciona %2$s a ele. + Nova lista %1$s + Nome do conjunto + Descrição do conjunto (opcional) + Criar conjunto + Renomear conjunto + Você está renomeando de + para.. Porta padrão é 9050 ## Conecte-se através do Tor com o Orbot \n\n1. Instale o [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) @@ -830,6 +856,8 @@ Média Alta Sem compressão + Usar codec H.265/HEVC + Melhor qualidade em arquivos menores, mas nem todos os dispositivos suportam reprodução em H.265. Editar rascunho Entrar com Código QR Rota @@ -1012,6 +1040,8 @@ Baixar Falha ao abrir o arquivo Nenhum aplicativo torrent instalado para abrir e baixar o arquivo. + O evento não tem informações suficientes para criar um link magnético + Minhas listas/conjuntos Selecione uma lista para filtrar o feed Terminar sessão no bloqueio do dispositivo Mensagem Privada @@ -1033,35 +1063,5 @@ Gostaria de enviar o relatório de falha recente para o Amethyst em uma DM? Nenhuma informação pessoal será compartilhada Enviar Esta mensagem desaparecerá em %1$d dias - Conjuntos de Seguimento - Favoritos com etiqueta - Favoritos gerais - Público - Privado - Misto - Parece que você ainda não tem conjuntos de seguimento.\nToque abaixo para atualizar ou use o botão de adicionar para criar um novo. - Adicionar autor ao conjunto de seguimento - Adicionar ou remover usuário de listas, ou criar uma nova lista com este usuário. - Ícone da lista %1$s - %1$s está presente nesta lista - %1$s não está nesta lista - Seus conjuntos de seguimento - Nenhum conjunto de seguimento foi encontrado ou você não possui nenhum. Toque abaixo para atualizar ou use o menu para criar um. - Houve um problema ao buscar: %1$s - Criar nova lista - Criar nova lista %1$s com usuário - Cria um conjunto de seguimento %1$s e adiciona %2$s a ele. - Nova lista %1$s - Nome do conjunto - Descrição do conjunto (opcional) - Criar conjunto - Renomear conjunto - Você está renomeando de - para.. - Renomear - O evento não tem informações suficientes para criar um link magnético - Minhas listas/conjuntos Selecionar assinador - Usar codec H.265/HEVC - Melhor qualidade em arquivos menores, mas nem todos os dispositivos suportam reprodução em H.265. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index ef8f428e4..4eb00d879 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -112,6 +112,7 @@ Dela Spara Skapa + Byt namn Avbryt Det gick inte att ladda upp bilden Relä Adress @@ -443,6 +444,31 @@ Runt mig Global Tyst listan + Följ-set + Märkta bokmärken + Allmänna bokmärken + Offentlig + Privat + Blandad + Det verkar som att du inte har några följ-set ännu.\nTryck nedan för att uppdatera eller använd plusknappen för att skapa ett nytt. + Lägg till författare i följ-set + Lägg till eller ta bort användare från listor, eller skapa en ny lista med denna användare. + Ikon för %1$s-lista + "%1$s finns i denna lista" + "%1$s finns inte i denna lista" + Dina följ-set + Inga följ-set hittades, eller så har du inga. Tryck nedan för att uppdatera eller använd menyn för att skapa ett. + Ett problem uppstod vid hämtning: %1$s + Skapa ny lista + Skapa ny %1$s-lista med användare + Skapar ett %1$s-följ-set och lägger till %2$s i det. + Ny %1$s-lista + Set-namn + Set-beskrivning (valfritt) + Skapa set + Byt namn på set + Du byter namn från + till.. Standardporten är 9050 ## Anslut genom Tor med Orbot \n\n1. Installera [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) @@ -829,6 +855,8 @@ Medel Hög Okomprimerad + Använd H.265/HEVC-codec + Bättre kvalitet med mindre filstorlek, men inte alla enheter stöder H.265-uppspelning. Redigera utkast Logga in med QR-kod Rutt @@ -1011,6 +1039,8 @@ Ladda ner Det gick inte att öppna filen Inga torrent-appar installerade för att öppna och ladda ner filen. + Händelsen har inte tillräcklig information för att skapa en magnetlänk + Mina listor/set Välj en lista för att filtrera flödet Logga ut när enheten låses Privat meddelande @@ -1032,35 +1062,5 @@ Vill du skicka den senaste kraschrapporten till Amethyst i ett DM? Ingen personlig information kommer att delas Skicka Detta meddelande försvinner om %1$d dagar - Följ-set - Märkta bokmärken - Allmänna bokmärken - Offentlig - Privat - Blandad - Det verkar som att du inte har några följ-set ännu.\nTryck nedan för att uppdatera eller använd plusknappen för att skapa ett nytt. - Lägg till författare i följ-set - Lägg till eller ta bort användare från listor, eller skapa en ny lista med denna användare. - Ikon för %1$s-lista - %1$s finns i denna lista - %1$s finns inte i denna lista - Dina följ-set - Inga följ-set hittades, eller så har du inga. Tryck nedan för att uppdatera eller använd menyn för att skapa ett. - Ett problem uppstod vid hämtning: %1$s - Skapa ny lista - Skapa ny %1$s-lista med användare - Skapar ett %1$s-följ-set och lägger till %2$s i det. - Ny %1$s-lista - Set-namn - Set-beskrivning (valfritt) - Skapa set - Byt namn på set - Du byter namn från - till.. - Byt namn - Händelsen har inte tillräcklig information för att skapa en magnetlänk - Mina listor/set Välj signatör - Använd H.265/HEVC-codec - Bättre kvalitet med mindre filstorlek, men inte alla enheter stöder H.265-uppspelning. From beff7e60b7565d517ba0251afbd9e03eab455277 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 3 Oct 2025 13:20:56 -0300 Subject: [PATCH 24/24] Fix sendAndWaitForResponse never receiving a response --- .../relay/client/accessories/NostrClientSendAndWaitExt.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt index 0fed3c8d1..248a358fe 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt @@ -95,6 +95,7 @@ suspend fun INostrClient.sendAndWaitForResponse( val receivedResults = mutableMapOf() // The withTimeout block will cancel the coroutine if the loop takes too long withTimeoutOrNull(timeoutInSeconds * 1000) { + send(event, relayList) while (receivedResults.size < relayList.size) { val result = resultChannel.receive() @@ -109,8 +110,6 @@ suspend fun INostrClient.sendAndWaitForResponse( } } - send(event, relayList) - val receivedResults = resultSubscription.await() unsubscribe(subscription)