From 8df02cf8f835a34b1fbbdd0a17a6e4bfbf310d37 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:21:12 +0000 Subject: [PATCH 01/10] fix: pad Hidden Words screen for bottom nav and gesture bars The bottom add-word text field sat behind the phone's navigation and gesture bars. Apply the union of navigation bar and IME insets to the bottom bar so it clears the system bars and rises with the keyboard without a double-counted gap. https://claude.ai/code/session_01KPkQbS9SYTigkkzYGU97Sz --- .../ui/screen/loggedIn/settings/HiddenWordsScreen.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt index 2500e8078d..ad047e37d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt @@ -26,10 +26,14 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -83,7 +87,6 @@ fun HiddenWordsScreen( var selected by remember { mutableStateOf(setOf()) } Scaffold( - modifier = Modifier.imePadding(), topBar = { BlockListTopBar( title = R.string.hidden_words, @@ -104,7 +107,10 @@ fun HiddenWordsScreen( ) }, bottomBar = { - Surface(tonalElevation = 3.dp) { + Surface( + tonalElevation = 3.dp, + modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars.union(WindowInsets.ime)), + ) { AddMuteWordTextField(accountViewModel) } }, From 3ff721b31aca31933a1be8d57079510bc644b793 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:21:28 +0000 Subject: [PATCH 02/10] refactor: move packaging/appimage into desktopApp module The AppImage build inputs (AppRun, .desktop entry, icon, and the CI-fetched linuxdeploy binary) are consumed only by desktopApp's createReleaseAppImage task. Co-locating them under desktopApp/packaging/appimage/ removes the `../` path escape from the build script and keeps all desktop packaging assets inside the module. https://claude.ai/code/session_0137ULcfJkASmfmffFBdW8ac --- .github/workflows/create-release.yml | 6 +++--- .gitignore | 6 +++--- BUILDING.md | 4 ++-- desktopApp/build.gradle.kts | 9 +++++---- {packaging => desktopApp/packaging}/appimage/AppRun | 0 .../packaging}/appimage/amethyst.desktop | 0 .../packaging}/appimage/amethyst.png | Bin 7 files changed, 13 insertions(+), 12 deletions(-) rename {packaging => desktopApp/packaging}/appimage/AppRun (100%) rename {packaging => desktopApp/packaging}/appimage/amethyst.desktop (100%) rename {packaging => desktopApp/packaging}/appimage/amethyst.png (100%) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index e5ce7edcdf..843aba8f34 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -88,13 +88,13 @@ jobs: if: matrix.family == 'linux-portable' run: | set -euo pipefail - curl -fsSL --retry 3 "$LINUXDEPLOY_URL" -o packaging/appimage/linuxdeploy-x86_64.AppImage - actual=$(sha256sum packaging/appimage/linuxdeploy-x86_64.AppImage | awk '{print $1}') + curl -fsSL --retry 3 "$LINUXDEPLOY_URL" -o desktopApp/packaging/appimage/linuxdeploy-x86_64.AppImage + actual=$(sha256sum desktopApp/packaging/appimage/linuxdeploy-x86_64.AppImage | awk '{print $1}') if [[ "$actual" != "$LINUXDEPLOY_SHA256" ]]; then echo "::error::linuxdeploy SHA256 mismatch. Expected $LINUXDEPLOY_SHA256, got $actual" exit 1 fi - chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage + chmod +x desktopApp/packaging/appimage/linuxdeploy-x86_64.AppImage - name: Build desktop artifacts uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 diff --git a/.gitignore b/.gitignore index 5e5bcfd89f..63cf8a9eec 100644 --- a/.gitignore +++ b/.gitignore @@ -167,9 +167,9 @@ desktopApp/src/jvmMain/appResources/macos/ desktopApp/src/jvmMain/appResources/windows/ # CI-fetched AppImage tooling (downloaded by create-release workflow; not committed) -packaging/appimage/linuxdeploy-x86_64.AppImage -packaging/appimage/linuxdeploy-extracted/ -packaging/appimage/squashfs-root/ +desktopApp/packaging/appimage/linuxdeploy-x86_64.AppImage +desktopApp/packaging/appimage/linuxdeploy-extracted/ +desktopApp/packaging/appimage/squashfs-root/ # Git worktrees .worktrees/ diff --git a/BUILDING.md b/BUILDING.md index 611db4f12c..f4b6ed6c29 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -42,9 +42,9 @@ sudo dnf install -y rpm-build Install linuxdeploy locally (CI fetches its own — SHA-verified): ```bash -curl -fsSL -o packaging/appimage/linuxdeploy-x86_64.AppImage \ +curl -fsSL -o desktopApp/packaging/appimage/linuxdeploy-x86_64.AppImage \ https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage -chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage +chmod +x desktopApp/packaging/appimage/linuxdeploy-x86_64.AppImage ``` --- diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index c9797b90d8..786f5a0fcd 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -173,13 +173,14 @@ tasks.withType().configureEach { // Instead: wrap `createReleaseDistributable` output with `linuxdeploy` (which // auto-bundles libraries, handles rpath, and calls appimagetool internally). // -// Build inputs live in packaging/appimage/: +// Build inputs live in desktopApp/packaging/appimage/: // - AppRun shell launcher (sets LD_LIBRARY_PATH including bundled VLC) // - amethyst.desktop XDG desktop entry // - amethyst.png 512x512 icon // -// linuxdeploy binary is fetched by CI (SHA-verified) into packaging/appimage/ -// as linuxdeploy-x86_64.AppImage. BUILDING.md documents local-dev fetch. +// linuxdeploy binary is fetched by CI (SHA-verified) into +// desktopApp/packaging/appimage/ as linuxdeploy-x86_64.AppImage. +// BUILDING.md documents local-dev fetch. val createReleaseAppImage by tasks.registering(Exec::class) { group = "compose desktop" description = "Bundle createReleaseDistributable output into a Linux AppImage via linuxdeploy." @@ -188,7 +189,7 @@ val createReleaseAppImage by tasks.registering(Exec::class) { val distDir = layout.buildDirectory.dir("compose/binaries/main-release/app/Amethyst") val appDir = layout.buildDirectory.dir("appimage/Amethyst.AppDir") val outFile = layout.buildDirectory.file("appimage/Amethyst-$appVersion-x86_64.AppImage") - val toolRoot = layout.projectDirectory.dir("../packaging/appimage") + val toolRoot = layout.projectDirectory.dir("packaging/appimage") val linuxdeployTool = toolRoot.file("linuxdeploy-x86_64.AppImage") inputs.dir(distDir) diff --git a/packaging/appimage/AppRun b/desktopApp/packaging/appimage/AppRun similarity index 100% rename from packaging/appimage/AppRun rename to desktopApp/packaging/appimage/AppRun diff --git a/packaging/appimage/amethyst.desktop b/desktopApp/packaging/appimage/amethyst.desktop similarity index 100% rename from packaging/appimage/amethyst.desktop rename to desktopApp/packaging/appimage/amethyst.desktop diff --git a/packaging/appimage/amethyst.png b/desktopApp/packaging/appimage/amethyst.png similarity index 100% rename from packaging/appimage/amethyst.png rename to desktopApp/packaging/appimage/amethyst.png From 1a8e349e9ff887cf56e54a7a029da38f29519753 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:29:28 +0000 Subject: [PATCH 03/10] feat(privacy): enable NIP-89 client tag by default and move to Compose settings Convert the per-account `disableClientTag` toggle into a positive `addClientTag` setting that defaults to enabled, and relocate it from the Security Filters screen to the Compose Settings page. https://claude.ai/code/session_01TkmBzzEz3kKnrjpbrKCpsf --- .../vitorpamplona/amethyst/model/Account.kt | 4 +-- .../amethyst/model/AccountSettings.kt | 4 +-- .../amethyst/model/AccountSyncedSettings.kt | 16 +++++----- .../model/AccountSyncedSettingsInternal.kt | 2 +- .../model/accountsCache/AccountCacheState.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../settings/ComposeSettingsScreen.kt | 29 +++++++++++++++++-- .../settings/SecurityFiltersScreen.kt | 17 ----------- .../src/main/res/values-cs-rCZ/strings.xml | 2 -- .../src/main/res/values-de-rDE/strings.xml | 2 -- .../src/main/res/values-hi-rIN/strings.xml | 2 -- .../src/main/res/values-hu-rHU/strings.xml | 2 -- .../src/main/res/values-nl-rNL/strings.xml | 2 -- .../src/main/res/values-pl-rPL/strings.xml | 2 -- .../src/main/res/values-pt-rBR/strings.xml | 2 -- .../src/main/res/values-sv-rSE/strings.xml | 2 -- .../src/main/res/values-zh-rCN/strings.xml | 2 -- amethyst/src/main/res/values/strings.xml | 4 +-- 18 files changed, 43 insertions(+), 55 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 396eee5ce0..251b0e3fe4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -545,8 +545,8 @@ class Account( return false } - suspend fun updateDisableClientTag(disable: Boolean): Boolean { - if (settings.updateDisableClientTag(disable)) { + suspend fun updateAddClientTag(add: Boolean): Boolean { + if (settings.updateAddClientTag(add)) { sendNewAppSpecificData() return true } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 4a5511df5a..94dc103b93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -440,8 +440,8 @@ class AccountSettings( } } - fun updateDisableClientTag(disable: Boolean): Boolean = - if (syncedSettings.security.updateDisableClientTag(disable)) { + fun updateAddClientTag(add: Boolean): Boolean = + if (syncedSettings.security.updateAddClientTag(add)) { saveAccountSettings() true } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index ed4d71e424..6d582ceaac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -56,7 +56,7 @@ class AccountSyncedSettings( MutableStateFlow(internalSettings.security.filterSpamFromStrangers), MutableStateFlow(internalSettings.security.maxHashtagLimit), MutableStateFlow(internalSettings.security.sendKind0EventsToLocalRelay), - MutableStateFlow(internalSettings.security.disableClientTag), + MutableStateFlow(internalSettings.security.addClientTag), ) val videoPlayer = AccountVideoPlayerPreferences( @@ -85,7 +85,7 @@ class AccountSyncedSettings( security.filterSpamFromStrangers.value, security.maxHashtagLimit.value, security.sendKind0EventsToLocalRelay.value, - security.disableClientTag.value, + security.addClientTag.value, ), videoPlayer = AccountVideoPlayerPreferencesInternal(videoPlayer.buttonItems.value), ) @@ -146,8 +146,8 @@ class AccountSyncedSettings( security.sendKind0EventsToLocalRelay.tryEmit(syncedSettingsInternal.security.sendKind0EventsToLocalRelay) } - if (security.disableClientTag.value != syncedSettingsInternal.security.disableClientTag) { - security.disableClientTag.tryEmit(syncedSettingsInternal.security.disableClientTag) + if (security.addClientTag.value != syncedSettingsInternal.security.addClientTag) { + security.addClientTag.tryEmit(syncedSettingsInternal.security.addClientTag) } val newVideoPlayerButtonItems = @@ -247,7 +247,7 @@ class AccountSecurityPreferences( var filterSpamFromStrangers: MutableStateFlow = MutableStateFlow(true), val maxHashtagLimit: MutableStateFlow = MutableStateFlow(5), var sendKind0EventsToLocalRelay: MutableStateFlow = MutableStateFlow(false), - val disableClientTag: MutableStateFlow = MutableStateFlow(false), + val addClientTag: MutableStateFlow = MutableStateFlow(true), ) { fun updateShowSensitiveContent(show: Boolean?): Boolean { if (showSensitiveContent.value != show) { @@ -297,9 +297,9 @@ class AccountSecurityPreferences( false } - fun updateDisableClientTag(disable: Boolean): Boolean = - if (disable != disableClientTag.value) { - disableClientTag.tryEmit(disable) + fun updateAddClientTag(add: Boolean): Boolean = + if (add != addClientTag.value) { + addClientTag.tryEmit(add) true } else { false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index 37c277a11c..6273c2a426 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -161,5 +161,5 @@ class AccountSecurityPreferencesInternal( var filterSpamFromStrangers: Boolean = true, val maxHashtagLimit: Int = 5, var sendKind0EventsToLocalRelay: Boolean = false, - var disableClientTag: Boolean = false, + var addClientTag: Boolean = true, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index 655af6970e..3233e9377a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -139,7 +139,7 @@ class AccountCacheState( NostrSignerWithClientTag( inner = signer, clientName = CLIENT_TAG_NAME, - disabled = { accountSettings.syncedSettings.security.disableClientTag.value }, + disabled = { !accountSettings.syncedSettings.security.addClientTag.value }, ) val accountDir = File(rootFilesDir(), "accounts/${signer.pubKey}").apply { mkdirs() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 5ad6de3d07..2241e3739f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1193,7 +1193,7 @@ class AccountViewModel( fun updateReportWarningThreshold(threshold: Int) = launchSigner { account.updateReportWarningThreshold(threshold) } - fun updateDisableClientTag(disable: Boolean) = launchSigner { account.updateDisableClientTag(disable) } + fun updateAddClientTag(add: Boolean) = launchSigner { account.updateAddClientTag(add) } fun updateFilterSpam(filterSpam: Boolean) = launchSigner { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 1c5baba82f..237bc42785 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -33,12 +33,14 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -57,7 +59,7 @@ fun ComposeSettingsScreen( }, ) { Column(Modifier.padding(it)) { - ComposeSettingsContent(accountViewModel.settings.uiSettingsFlow) + ComposeSettingsContent(accountViewModel.settings.uiSettingsFlow, accountViewModel) } } } @@ -66,12 +68,15 @@ fun ComposeSettingsScreen( @Composable fun ComposeSettingsScreenPreview() { ThemeComparisonRow { - ComposeSettingsContent(UiSettingsFlow()) + ComposeSettingsContent(UiSettingsFlow(), mockAccountViewModel()) } } @Composable -fun ComposeSettingsContent(sharedPrefs: UiSettingsFlow) { +fun ComposeSettingsContent( + sharedPrefs: UiSettingsFlow, + accountViewModel: AccountViewModel, +) { Column( Modifier .fillMaxSize() @@ -95,6 +100,24 @@ fun ComposeSettingsContent(sharedPrefs: UiSettingsFlow) { R.string.tracked_broadcasts_setting_title, R.string.tracked_broadcasts_setting_description, ) + AddClientTagRow(accountViewModel) + } +} + +@Composable +private fun AddClientTagRow(accountViewModel: AccountViewModel) { + val addClientTag by accountViewModel.account.settings.syncedSettings.security + .addClientTag + .collectAsStateWithLifecycle() + + SettingsRow( + R.string.add_client_tag_title, + R.string.add_client_tag_explainer, + ) { + Switch( + checked = addClientTag, + onCheckedChange = accountViewModel::updateAddClientTag, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index c040be314d..11faac4803 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -84,8 +84,6 @@ private fun SecurityPreferencesSection(accountViewModel: AccountViewModel) { SettingsDivider() HideCommunityViolationsTile(accountViewModel) SettingsDivider() - DisableClientTagTile(accountViewModel) - SettingsDivider() WarnReportsTile(accountViewModel) SettingsDivider() MaxHashtagsTile(accountViewModel) @@ -147,21 +145,6 @@ private fun HideCommunityViolationsTile(accountViewModel: AccountViewModel) { ) } -@Composable -private fun DisableClientTagTile(accountViewModel: AccountViewModel) { - val disableClientTag by accountViewModel.account.settings.syncedSettings.security - .disableClientTag - .collectAsStateWithLifecycle() - - SwitchTile( - icon = MaterialSymbols.Code, - title = R.string.disable_client_tag_title, - description = R.string.disable_client_tag_explainer, - checked = disableClientTag, - onCheckedChange = accountViewModel::updateDisableClientTag, - ) -} - @Composable private fun WarnReportsTile(accountViewModel: AccountViewModel) { val security = accountViewModel.account.settings.syncedSettings.security diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index acf9f6b8df..759897403e 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1122,8 +1122,6 @@ Varovat, když příspěvky obsahují hlášení od vašich sledovaných osob Filtrovat spam Skryje příspěvky od cizinců, které byly přesně stejné 5 a více krát - Nepřidávat klientský tag k mým událostem - Pokud je povoleno, Amethyst nebude k tvým publikovaným událostem přidávat klientský tag NIP-89. Upozornit na hlášení Zobrazí varovnou zprávu, když příspěvky mají 5 nebo více hlášení od vašich sledovaných Práh varování o nahlášeních diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index c38cd8f4ee..e98f445534 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1115,8 +1115,6 @@ anz der Bedingungen ist erforderlich Warnung bei Meldungen von deinen Abonnements Spam filtern Versteckt Beiträge von Fremden, die 5 oder mehr Male genau die gleichen waren - Kein Client-Tag zu meinen Ereignissen hinzufügen - Wenn aktiviert, fügt Amethyst deinen veröffentlichten Ereignissen kein NIP-89 Client-Tag hinzu. Bei Berichten warnen Zeigt eine Warnmeldung an, wenn die Beiträge 5 oder mehr Berichte von den folgenden Beiträgen enthalten Warnschwelle für Meldungen diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 70a80a3fdb..6610b655fc 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1100,8 +1100,6 @@ चेतावनी दें जब पत्र सूचित किये गये हों आपके द्वारा अनुचरित व्यक्तियों से कचरालेख छलनी अज्ञात लोगों से पत्र छिपाएँ जो निरन्तर 5 अथवा अधिक बार यथावत समान थे - मेरे घटनाओं में ग्राहक सूचक ना जोडें - जब सक्षम अमेथिस्ट निप॰८९ ग्राहक सूचक नहीं जोडेगा आपके द्वारा प्रकाशित घटनाओं में। सूचनाएँ प्राप्त होने पर चेतावनी दें चेतावनी सन्देश दिखाता है जब प्रकाशित पत्र 5 अथवा अधिक बार सूचित हो आपके द्वारा अनुचरित लेखाओं से चेतावनी सीमा सूचना diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index dcee038b4d..7d5138e28d 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1114,8 +1114,6 @@ Figyelmeztessen amikor a bejegyzések jelentve vannak azok által akiket követek Kéretlen tartalomszűrő Elrejti az ismeretlen felhasználók hozzászólásait, amelyek pontosan ugyanazok voltak 5 vagy több alkalommal - Ne legyenek megjelölve az eseményeim klienscímkével - Ha engedélyezve van, akkor az Amethyst nem fűz NIP-89 klienscímkét az Ön által közzétett eseményekhez. Figyelmeztetés a jelentésekre Figyelmeztető üzenetet jelenít meg, ha az Ön követői bejelentést tettek bejegyzésekről vagy profilokról Jelentési figyelmeztetés küszöbértéke diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml index a40aeaf9a9..7e2d48b7ed 100644 --- a/amethyst/src/main/res/values-nl-rNL/strings.xml +++ b/amethyst/src/main/res/values-nl-rNL/strings.xml @@ -1078,8 +1078,6 @@ Waarschuwen bij rapportages van volgers Spamfilter Verbergt identieke berichten van onbekenden die 5 keer of vaker voorkomen - Geen client-tag toevoegen aan mijn events - Wanneer ingeschakeld voegt Amethyst geen NIP-89 client-tag toe aan events die je publiceert. Waarschuwen bij rapportages Toont waarschuwing wanneer een bericht 5 of meer rapportages van je volgers heeft Gevoelige inhoud tonen diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 1d0e9e81f8..21ee2a8b78 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1124,8 +1124,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz Filtr spamu Ukrywa posty od nieznajomych, które były dokładnie takie same przez 5 lub więcej razy - Nie dodawaj tagu klienta do moich publikacji - Po włączeniu tej opcji Amethyst nie będzie dołączać tagu klienta NIP-89 do Twoich publikacji. Ostrzeżenie o zgłoszeniach Wyświetla komunikat ostrzegawczy, gdy posty zostały zgłoszone przez 5 lub więcej obserwowanych osób Zgłoś próg ostrzegawczy diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 5cba6e077d..fb3af86a80 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1116,8 +1116,6 @@ Avise quando as postagens tiverem denuncias de seus seguidores Filtrar spam Esconde publicações de desconhecidos que eram exatamente o mesmo por 5 ou mais vezes - Não adicionar tag de cliente aos meus eventos - Quando ativado, o Amethyst não anexará uma tag de cliente NIP-89 aos eventos que você publicar. Avisar em relatórios Mostra uma mensagem de aviso quando as postagens tiverem 5 ou mais relatórios de suas seguintes Limite de aviso de denúncias diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index a77c3b1fb0..f5949a68b0 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1109,8 +1109,6 @@ Varna när inlägg har rapporter från dina följare Filtrera spam Döljer inlägg från främlingar som var exakt samma för 5 eller fler gånger - Lägg inte till klienttagg i mina händelser - När aktiverat kommer Amethyst inte att lägga till en NIP-89 klienttagg till händelser du publicerar. Varna vid rapporter Visar ett varningsmeddelande när inlägg har 5 eller fler rapporter från följande Tröskel för rapportvarning diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index bd911d7087..7196354500 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -1107,8 +1107,6 @@ 当帖子有你的关注的报告时警告 过滤垃圾信息 隐藏陌生人的五次或五次以上完全相同的帖子 - 不要将客户标签添加到我的事件 - 启用时,Amethyst 将不会在您发布的事件中附加一个 NIP-89 客户端标签。 举报警示 当帖子有来自你关注的五次及以上举报时显示警告消息 举报警告阈值 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3f6fbf18ce..5e87fff51f 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1232,8 +1232,8 @@ Filter spam Hides posts from strangers that were exactly the same for 5 or more times - Don\'t add client tag to my events - When enabled, Amethyst will not append a NIP-89 client tag to events you publish. + Add client tag to my events + When enabled, Amethyst appends a NIP-89 client tag to events you publish. Warn on reports Shows a warning message when posts or profiles have reports from your follows Report warning threshold From 87b6bae6da4f14f7f5359a01bc979502a532331d Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 14 May 2026 18:34:30 +0000 Subject: [PATCH 04/10] New Crowdin translations by GitHub Action --- .../src/main/res/values-hu-rHU/strings.xml | 6 ++++-- .../src/main/res/values-pl-rPL/strings.xml | 19 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 12 ------------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index dcee038b4d..83a970ae68 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -545,7 +545,7 @@ Ön kapott egy kitűzőt Profilkitűzők Kitűzők · %1$d - Válassza ki, hogy a megszerzett jelvények közül melyek jelenjenek meg a profilban. + Válassza ki, hogy a megszerzett kitűzők közül melyek jelenjenek meg a profilban. Ön még nem kapott kitűzőt. Képek Rövidek @@ -657,6 +657,8 @@ Kérés %1$s kliense felé a mikrofon némítására. Néhány kliens figyelmen kívül hagyhatja ezt a parancsot. Kényszerített némítás Mégse + Sikertelen művelet + A műveletet nem sikerült előkészíteni. Szoba megosztása Minimalizálás Minimalizálás a további hallgatáshoz @@ -1756,7 +1758,7 @@ Átjátszók a kimenő üzenetkhez Állítsa be a nyilvános kimenő üzenetek átjátszóit a bejegyzéshez A tartalom fogadására kifejezetten kialakított átjátszólista létrehozása elengedhetetlen a Nostr élményhez, és ez az egyetlen módja annak, hogy a követői megtalálják Önt. - Adjon meg 1–3 átjátszót, amelyek fogadják az Ön bejegyzéseit. Győződjön meg arról, hogy nem kérnek fizetést, ha Ön nem fizet a használatukért + Adjon meg 1-3 átjátszót, amelyek fogadják az Ön bejegyzéseit. Győződjön meg arról, hogy nem kérnek fizetést, ha Ön nem fizet a használatukért Jó választási lehetőségek:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social Átjátszók a bejövő üzenetkhez Állítsa be a nyilvános bejövő üzenetek átjátszóit az értesítések fogadásához diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 1d0e9e81f8..04ed42a9f8 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -667,6 +667,8 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Prosi klienta %1$so wyciszenie mikrofonu. Niektórzy klienci mogą zignorować to polecenie. Wymuś wyciszenie Anuluj + Akcja nieudana + Nie udało się przygotować tej akcji. Udostępnij pokój Zminimalizuj Minimalizuj aby słuchać @@ -1136,6 +1138,12 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Ukrywa posty zawierające więcej hashtagów niż ten limit. Ustaw wartość 0, aby wyłączyć tę funkcję Ukryj posty, które naruszają zasady społeczności Usuwa posty z kanałów społeczności, gdy społeczność opublikuje dokument zawierający zasady NIP-9B, a dane wydarzenie nie spełnia tych zasad. Nie ma to żadnego wpływu, jeśli społeczność nie posiada strukturyzowanych zasad. + Preferencje filtrowania + Zablokowane treści + + Nie zablokowałeś jeszcze żadnych użytkowników. + W tej sesji żadne konta nie zostały oznaczone jako spam. + Brak ukrytych słów. Wpisz poniżej słowo, aby ukryć posty, które je zawierają. Nowy Symbol Odzewu Brak wstępnie wybranych typów reakcji dla tego użytkownika. Przytrzymaj przycisk serce, aby zmienić Zapraiser @@ -1333,6 +1341,8 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Tylko obserwatorzy z twojej lokalizacji to zobaczą. Twoi ogólni obserwatorzy nie zobaczą tego. Wpis hashtagowy Tylko obserwujący ten hashtag to zobaczą. Twoi ogólni obserwujący nie zobaczą tego. + Skomentuj na stronie internetowej + Skomentuj na zewnętrznym serwisie Czas czytania: %1$d min. Pobieranie lokalizacji Brak dostępu do lokalizacji @@ -1671,6 +1681,12 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Karty główne Wybierz, które karty pojawiają się na ekranie głównym. Gdy tylko jedna karta jest aktywna, pasek karty jest ukryty. Wszystko + Interfejs profilu + Wybierz które sekcje i kanały pojawiają się na ekranach profilu użytkownika. Wszystkie opcje są domyślnie włączone. + Odznaki w profilu + Rekomendacje aplikacji + Kanał, który otrzymał Zapa + Kanał Obserwowanych Ustawienia reakcji Skonfiguruj które przyciski reakcji będą wyświetlane, ich kolejność i czy wyświetlić liczniki. Włączone @@ -2530,6 +2546,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Używa modelu sztucznej inteligencji wbudowanego w urządzenie, proponując poprawki tekstu i zmiany tonu wypowiedzi. Monitorowane transmisje Podczas wysyłania zdarzeń korzystaj z monitora transmisji. Pokazuje postęp na żywo i status transmisji podczas nadawania. + Skonfiguruj ustawienia + Automatycznie twórz wersje robocze + Automatycznie zapisuje wersję roboczą wiadomości podczas pisania lub po zamknięciu okna edycji z niewysłanym tekstem i umieszcza ją w prywatnej skrzynce wysyłkowej transmiterów. Użyj tego Ignoruj Popraw diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 5cba6e077d..46c018826a 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -428,7 +428,6 @@ Desconectado Desconectado · %d post agendado excluído - Desconectado · %d de posts agendados excluídos Desconectado · %d posts agendados excluídos Post agendado publicado @@ -446,22 +445,18 @@ Cancelado Você tem %d post agendado que ainda não foi publicado. Sair excluirá esse post permanentemente. - Você tem %d de posts agendados que ainda não foram publicados. Sair excluirá esses posts permanentemente. Você tem %d posts agendados que ainda não foram publicados. Sair excluirá esses posts permanentemente. %d na fila - %d na fila %d na fila · %d em 1h - · %d em 1h · %d em 1h para %d relay - para %d de relays para %d relays ID do post copiado @@ -621,7 +616,6 @@ Não foi possível marcar a sala como fechada. Saindo mesmo assim — a sala será fechada automaticamente. %1$d ouvinte - %1$d de ouvintes %1$d ouvintes AO VIVO @@ -2272,32 +2266,26 @@ Nunca visto %1$d minuto - %1$d de minutos %1$d minutos %1$d hora - %1$d de horas %1$d horas %1$d dia - %1$d de dias %1$d dias %1$d semana - %1$d de semanas %1$d semanas %1$d mês - %1$d de meses %1$d meses %1$d ano - %1$d de anos %1$d anos <%1$s From 2408c43918aed28ee8d868df8467fc7c88662504 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:35:40 +0000 Subject: [PATCH 05/10] fix: stop reordering Nest stage members by speaking state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Members were sorted to float the current speaker to the top, so a member jumped to the front when they started talking and slid back ~250ms after they stopped — a constant shuffle in any active room. The speaking state is already shown in-place via the mic badge and audio-level ring, so keep members in their stable arrival order. https://claude.ai/code/session_01GvvNS1FRn6gGvHRC7dgEtP --- .../nests/room/stage/ParticipantsGrid.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt index a6d824114b..0d340f1fc9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt @@ -162,15 +162,12 @@ internal fun StageGrid( listenerCount: Int = 0, bottomBar: (@Composable () -> Unit)? = null, ) { - // Float currently-speaking members to the top so the listener can - // see who they're hearing without scrolling. sortedBy is stable in - // Kotlin — speakers retain relative order among themselves, as do - // non-speakers. Memoized on (members, speakingNow) so a 250ms - // speaking flip doesn't reallocate when neither input changed. - val sortedMembers = - remember(members, speakingNow) { - members.sortedBy { if (it.pubkey in speakingNow) 0 else 1 } - } + // Keep members in their stable arrival order. Reordering on the + // speaking state made a member jump to the front when they started + // talking and slide back ~250ms after they stopped — a constant + // shuffle in any active room. The speaking state is already shown + // in-place via MemberCell's mic badge / audio-level ring, so no + // reordering is needed. // Wrap the strip in a tonal card so the active speakers visually // live in their own zone, separated from the chat / audience tab // below. surfaceContainerLow is a quiet step up from the screen @@ -221,7 +218,7 @@ internal fun StageGrid( horizontalArrangement = Arrangement.spacedBy(GRID_SPACING), verticalArrangement = Arrangement.spacedBy(GRID_SPACING), ) { - items(items = sortedMembers, key = { it.pubkey }) { member -> + items(items = members, key = { it.pubkey }) { member -> val isSelf = myPubkey != null && member.pubkey == myPubkey MemberCell( member = member, From 35dfa119c374477ba875f6b34af33ed2a1db0dcc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:41:18 +0000 Subject: [PATCH 06/10] fix: restrict video player button reordering drag to the drag handle Move the detectDragGestures pointerInput from the whole button card to the six-dot drag handle icon so drag-and-drop reordering activates only when the user grabs the handle. https://claude.ai/code/session_01Jyu8dLYN7MXVNaVZE3u2M9 --- .../settings/VideoPlayerSettingsScreen.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt index e6e6dada17..704575d6e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt @@ -261,18 +261,7 @@ private fun VideoPlayerButtonItemCard( scaleX = 1.02f scaleY = 1.02f } - }.padding(vertical = 8.dp, horizontal = Size20dp) - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { onDragStart() }, - onDrag = { change, dragAmount -> - change.consume() - onDrag(dragAmount.y) - }, - onDragEnd = { onDragEnd() }, - onDragCancel = { onDragCancel() }, - ) - }, + }.padding(vertical = 8.dp, horizontal = Size20dp), ) { Row( modifier = Modifier.fillMaxWidth(), @@ -296,7 +285,20 @@ private fun VideoPlayerButtonItemCard( } Box( - modifier = Modifier.size(32.dp), + modifier = + Modifier + .size(32.dp) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { onDragStart() }, + onDrag = { change, dragAmount -> + change.consume() + onDrag(dragAmount.y) + }, + onDragEnd = { onDragEnd() }, + onDragCancel = { onDragCancel() }, + ) + }, contentAlignment = Alignment.CenterEnd, ) { Icon( From 39f2b939c3ec6c8d1240c661f96dbf31ebd80743 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:43:53 +0000 Subject: [PATCH 07/10] fix: drop stale relay-replayed groups on Nests listener reconnect On every re-subscribe (listener reconnect or publisher-cycle), the moq-lite relay re-serves its cached latest group from the first frame. The wrapper forwarded it straight into the decoder, so in a fully-muted room the same old clip looped once per reconnect. Track the highest group sequence delivered by prior subscriptions and drop any group not strictly newer, mirroring kixelated/hang's Container.Consumer.#run. https://claude.ai/code/session_01G9h2dzkEj6Y2F1Yr2kCojp --- .../nestsclient/ReconnectingNestsListener.kt | 46 +++++++- .../ReconnectingNestsListenerTest.kt | 105 +++++++++++++++++- 2 files changed, 144 insertions(+), 7 deletions(-) diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListener.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListener.kt index 6ec082b75a..c4e6b3602e 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListener.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListener.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull +import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference /** @@ -334,6 +335,29 @@ private class ReconnectingHandle( ) val liveHandleRef = AtomicReference(null) + // Cross-subscription replay guard. moq-lite publishers carry + // their group lineage forward monotonically across their own + // session recycles (ReconnectingNestsSpeaker.runHotSwapIteration + // seeds each new publisher with the prior one's nextSequence), + // and on every re-subscribe — listener-side reconnect OR the + // inner publisher-cycle loop — the relay re-serves its cached + // latest group from the first frame. Without a drop filter that + // stale group is replayed straight into the decoder; in a + // fully-muted room, where no newer group is ever produced, it's + // the same ~1 s clip looping once per reconnect. Mirrors + // kixelated/hang's `Container.Consumer.#run`, which drops any + // group not strictly newer than what's already been consumed. + // + // Watermark = highest group sequence delivered by a PRIOR + // underlying subscription. It only advances when a subscription + // ends (the `finally` below), so frames of the *current* group + // are never dropped mid-stream — a live group is always newer + // than the frozen watermark. Cost: a reconnect mid-group drops + // the tail of that one in-progress group (≤ ~1 s of audio), + // which is the right trade for live audio and strictly better + // than replaying already-played speech. + val priorGroupWatermark = AtomicLong(-1L) + // Re-subscribe pump. Two re-issue triggers, layered: // // 1. Listener session swap (outer collectLatest) — fires @@ -432,15 +456,31 @@ private class ReconnectingHandle( liveHandleRef.set(handle) Log.d("NestRx") { "wrapper: handle attached id=${handle.subscribeId}, starting collect" } var emitted = 0L + var droppedStale = 0L + var handleHighestGroup = -1L try { - handle.objects.collect { + handle.objects.collect { obj -> + if (obj.groupId <= priorGroupWatermark.get()) { + // Stale group re-served from the relay + // cache after a re-subscribe — drop so + // it never reaches the decoder. + droppedStale += 1 + return@collect + } + if (obj.groupId > handleHighestGroup) handleHighestGroup = obj.groupId emitted += 1 - frames.emit(it) + frames.emit(obj) } } finally { if (liveHandleRef.get() === handle) liveHandleRef.set(null) + // Advance the watermark only now the subscription + // is done, so the next re-subscribe drops every + // group up to and including the last one we saw. + if (handleHighestGroup > priorGroupWatermark.get()) { + priorGroupWatermark.set(handleHighestGroup) + } } - Log.w("NestRx") { "wrapper: handle objects flow ENDED id=${handle.subscribeId} emitted=$emitted — re-issuing after ${RESUBSCRIBE_BACKOFF_MS}ms" } + Log.w("NestRx") { "wrapper: handle objects flow ENDED id=${handle.subscribeId} emitted=$emitted droppedStale=$droppedStale — re-issuing after ${RESUBSCRIBE_BACKOFF_MS}ms" } // Brief backoff so a permanently-gone // publisher doesn't tight-loop the relay // with re-subscribes. 100 ms stays well diff --git a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListenerTest.kt b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListenerTest.kt index 3d4249c161..0147f87357 100644 --- a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListenerTest.kt +++ b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsListenerTest.kt @@ -137,10 +137,13 @@ class ReconnectingNestsListenerTest { } } - private fun frame(payload: ByteArray): MoqObject = + private fun frame( + payload: ByteArray, + groupId: Long = 0L, + ): MoqObject = MoqObject( trackAlias = 1L, - groupId = 0L, + groupId = groupId, objectId = 0L, publisherPriority = 0, payload = payload, @@ -232,7 +235,7 @@ class ReconnectingNestsListenerTest { first.frames.subscriptionCount.first { it > 0 } } - first.frames.emit(frame(byteArrayOf(0x01))) + first.frames.emit(frame(byteArrayOf(0x01), groupId = 0L)) // Wait for FRAME1 to traverse pump → wrapper.frames → // consumer collector. Without this sync the next @@ -261,7 +264,11 @@ class ReconnectingNestsListenerTest { second.frames.subscriptionCount.first { it > 0 } } - second.frames.emit(frame(byteArrayOf(0x02))) + // Post-reconnect audio lands in a newer group — moq-lite + // publishers carry their group lineage forward + // monotonically across recycles, so the second session's + // first real frame is group 1, not a replay of group 0. + second.frames.emit(frame(byteArrayOf(0x02), groupId = 1L)) val result = collected.await() assertEquals(2, result.size) @@ -283,6 +290,96 @@ class ReconnectingNestsListenerTest { } } + /** + * Regression: after a reconnect the relay re-serves its cached + * latest group from the first frame. moq-lite publishers carry + * group sequences forward monotonically across their own recycles, + * so a re-served group has a sequence we've already consumed — the + * wrapper must drop it instead of replaying already-played audio + * into the decoder. In a fully-muted room (no newer group ever + * produced) the un-dropped replay loops the same clip once per + * reconnect. + */ + @Test + fun reconnect_does_not_replay_already_consumed_group() = + runBlocking { + val scope = CoroutineScope(SupervisorJob()) + try { + val first = ScriptedListener(room) + val second = ScriptedListener(room) + val listenersInOrder = mutableListOf(first, second) + + val reconnecting = + connectReconnectingNestsListener( + httpClient = httpClient, + transport = transport, + scope = scope, + room = room, + signer = signer, + policy = NestsReconnectPolicy(initialDelayMs = 1L), + connector = { listenersInOrder.removeAt(0) }, + ) + + withTimeout(5_000L) { + reconnecting.state.first { it is NestsListenerState.Connected } + } + + val handle = reconnecting.subscribeSpeaker("speaker-pubkey") + + val consumerSubscribed = CompletableDeferred() + val consumerProgress = MutableStateFlow(0) + + @Suppress("UNCHECKED_CAST") + val objectsAsShared = handle.objects as SharedFlow + // Expect exactly two frames: the original group-0 frame + // and the genuinely-new group-1 frame. The group-0 frame + // re-served by the second session must NOT appear. + val collected = + scope.async { + withTimeout(5_000L) { + objectsAsShared + .onSubscription { consumerSubscribed.complete(Unit) } + .take(2) + .onEach { consumerProgress.value += 1 } + .toList() + } + } + + withTimeout(5_000L) { consumerSubscribed.await() } + withTimeout(5_000L) { + first.frames.subscriptionCount.first { it > 0 } + } + + first.frames.emit(frame(byteArrayOf(0x01), groupId = 0L)) + withTimeout(5_000L) { + consumerProgress.first { it >= 1 } + } + + first.fail("scripted-disconnect") + withTimeout(5_000L) { + second.frames.subscriptionCount.first { it > 0 } + } + + // The relay re-serves the cached latest group (0) from + // the start — this is the stale replay that must be + // dropped. + second.frames.emit(frame(byteArrayOf(0x02), groupId = 0L)) + // ...followed by genuinely new audio in the next group. + second.frames.emit(frame(byteArrayOf(0x03), groupId = 1L)) + + val result = collected.await() + assertEquals(2, result.size) + assertEquals(0x01.toByte(), result[0].payload[0]) + // 0x02 (re-served group 0) dropped; 0x03 (group 1) kept. + assertEquals(0x03.toByte(), result[1].payload[0]) + + handle.unsubscribe() + reconnecting.close() + } finally { + scope.cancel() + } + } + @Test fun unsubscribe_before_session_swap_releases_handle() = runBlocking { From 7dbb78644a2d1085faa913510f51dd0e8a5a844a Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 14 May 2026 20:47:16 +0200 Subject: [PATCH 08/10] i18n: translate compose/profile-UI/security/nest strings into cs/de/pt-BR/sv Adds Czech, German, Brazilian Portuguese, and Swedish translations for 19 new keys covering auto-create-drafts compose settings, profile-UI section toggles, security-screen empty states and section headers, external-resource comment scopes, and nest host-action failure toasts. --- .../src/main/res/values-cs-rCZ/strings.xml | 19 +++++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 19 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 19 +++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 19 +++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 759897403e..8209ad9e0b 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -2582,4 +2582,23 @@ Dnes jsi již zveřejnil %1$d událostí tohoto typu (limit %2$d). Nesplňuješ požadavky sítě důvěry této komunity. Nejnovější dokument s pravidly komunity byl odmítnut jako zastaralý. + Automaticky uloží koncept události, když píšeš nebo opouštíš editor s neodeslaným textem, a odešle ho na tvé soukromé odchozí relaye. + Automaticky vytvářet koncepty + Nastavení editoru + Komentář k externímu zdroji + Komentář k webové stránce + Akci nelze připravit. + Akce selhala + Doporučení aplikací + Profilové odznaky + Kanál sledujících + Kanál přijatých zapů + Vzhled profilu + Vyberte, které sekce a kanály se zobrazí na profilových obrazovkách uživatelů. Všechny možnosti jsou ve výchozím nastavení povoleny. + Zatím jsi nezablokoval/a žádné uživatele. + Žádná skrytá slova. Přidej slovo níže pro skrytí příspěvků, které ho obsahují. + Blokovaný obsah + Předvolby filtrování + V této relaci nebyly žádné účty označeny jako spam. + diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index e98f445534..59a160a894 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2563,4 +2563,23 @@ anz der Bedingungen ist erforderlich Du hast heute bereits %1$d Ereignisse dieses Typs veröffentlicht (Limit %2$d). Du erfüllst die Web-of-Trust-Anforderungen dieser Community nicht. Das aktuelle Community-Regeldokument wurde als veraltet abgelehnt. + Speichert automatisch ein Entwurfsereignis, wenn du tippst oder den Editor mit ungesendetem Text verlässt, und sendet es an deine privaten ausgehenden Relays. + Entwürfe automatisch erstellen + Editor-Einstellungen + Eine externe Ressource kommentieren + Eine Webseite kommentieren + Die Aktion konnte nicht vorbereitet werden. + Aktion fehlgeschlagen + App-Empfehlungen + Profilabzeichen + Follower-Feed + Erhaltene-Zaps-Feed + Profil-Oberfläche + Wähle, welche Abschnitte und Feeds auf den Profilseiten erscheinen. Alle Optionen sind standardmäßig aktiviert. + Du hast noch keine Benutzer blockiert. + Keine versteckten Wörter. Füge unten ein Wort hinzu, um Beiträge auszublenden, die es enthalten. + Blockierte Inhalte + Filtereinstellungen + In dieser Sitzung wurden keine Konten als Spam markiert. + diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 1ed62a9c3a..52a8e2ba34 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2558,4 +2558,23 @@ Você já publicou %1$d eventos deste tipo hoje (limite %2$d). Você não atende aos requisitos da rede de confiança desta comunidade. O documento mais recente de regras da comunidade foi rejeitado por estar desatualizado. + Salva um evento de rascunho automaticamente quando você digita ou sai do compositor com texto não enviado e envia para seus relays de saída privados. + Criar rascunhos automaticamente + Configurações de composição + Comentar em um recurso externo + Comentar em um site + A ação não pôde ser preparada. + Falha na ação + Recomendações de Apps + Selos de perfil + Feed de Seguidores + Feed de Zaps Recebidos + Interface do perfil + Escolha quais seções e feeds aparecem nas telas de perfil dos usuários. Todas as opções estão habilitadas por padrão. + Você ainda não bloqueou nenhum usuário. + Nenhuma palavra oculta. Adicione uma palavra abaixo para ocultar publicações que a contenham. + Conteúdo bloqueado + Preferências de filtragem + Nenhuma conta foi marcada como spam nesta sessão. + diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index f5949a68b0..5eaee56103 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2557,4 +2557,23 @@ Du har redan publicerat %1$d händelser av den här typen idag (gräns %2$d). Du uppfyller inte den här gemenskapens krav på förtroendenät. Det senaste dokumentet med gemenskapsregler avvisades som föråldrat. + Sparar ett utkast automatiskt när du skriver eller lämnar redigeraren med osänd text och skickar det till dina privata utgående reläer. + Skapa utkast automatiskt + Skrivinställningar + Kommentera en extern resurs + Kommentera en webbplats + Åtgärden kunde inte förberedas. + Åtgärden misslyckades + App-rekommendationer + Profilmärken + Följarflöde + Mottagna zaps-flöde + Profilgränssnitt + Välj vilka sektioner och flöden som visas på användarprofiler. Alla alternativ är aktiverade som standard. + Du har inte blockerat några användare än. + Inga dolda ord. Lägg till ett ord nedan för att dölja inlägg som innehåller det. + Blockerat innehåll + Filterinställningar + Inga konton har flaggats som spam under denna session. + From a1aa9ebb022ff335a576149da46720bcc0360686 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 14 May 2026 20:20:56 +0200 Subject: [PATCH 09/10] fix(account): break a-tag cycle in computeRelayListToBroadcast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An addressable event whose `a` tags reach itself — directly (self-mention) or via any cycle of other in-cache events — would recurse unbounded through `AddressHintProvider` / `EventHintProvider` branches and crash with StackOverflowError on publish (`signAndComputeBroadcast` → `computeRelayListToBroadcast`). The crash was reproducible on the device when publishing a kind-1 note that referenced an article whose own event carries a self `a` tag (e.g. YakiHonne-authored long-form posts include their own naddr in their `a` tags). --- .../com/vitorpamplona/amethyst/model/Account.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 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 251b0e3fe4..971da63e26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -965,7 +965,15 @@ class Account( return true } - fun computeRelayListToBroadcast(event: Event): Set { + fun computeRelayListToBroadcast(event: Event): Set = computeRelayListToBroadcast(event, mutableSetOf()) + + private fun computeRelayListToBroadcast( + event: Event, + visited: MutableSet, + ): Set { + // a-tagged events can form cycles; without this the two recursive descents stack-overflow. + if (!visited.add(event.id)) return emptySet() + if (event is GiftWrapEvent) { val receiver = event.recipientPubKey() return if (receiver != null) { @@ -1044,7 +1052,7 @@ class Account( } linkedNote.event?.let { linkedEvent -> - relayList.addAll(computeRelayListToBroadcast(linkedEvent)) + relayList.addAll(computeRelayListToBroadcast(linkedEvent, visited)) } } } @@ -1065,7 +1073,7 @@ class Account( } linkedNote.event?.let { linkedEvent -> - relayList.addAll(computeRelayListToBroadcast(linkedEvent)) + relayList.addAll(computeRelayListToBroadcast(linkedEvent, visited)) } } } From 961cce7f18f89cd801f897b8573875da756174b0 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 14 May 2026 18:54:14 +0000 Subject: [PATCH 10/10] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 19 ------------------- .../src/main/res/values-de-rDE/strings.xml | 19 ------------------- .../src/main/res/values-pt-rBR/strings.xml | 19 ------------------- .../src/main/res/values-sv-rSE/strings.xml | 19 ------------------- 4 files changed, 76 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 8209ad9e0b..759897403e 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -2582,23 +2582,4 @@ Dnes jsi již zveřejnil %1$d událostí tohoto typu (limit %2$d). Nesplňuješ požadavky sítě důvěry této komunity. Nejnovější dokument s pravidly komunity byl odmítnut jako zastaralý. - Automaticky uloží koncept události, když píšeš nebo opouštíš editor s neodeslaným textem, a odešle ho na tvé soukromé odchozí relaye. - Automaticky vytvářet koncepty - Nastavení editoru - Komentář k externímu zdroji - Komentář k webové stránce - Akci nelze připravit. - Akce selhala - Doporučení aplikací - Profilové odznaky - Kanál sledujících - Kanál přijatých zapů - Vzhled profilu - Vyberte, které sekce a kanály se zobrazí na profilových obrazovkách uživatelů. Všechny možnosti jsou ve výchozím nastavení povoleny. - Zatím jsi nezablokoval/a žádné uživatele. - Žádná skrytá slova. Přidej slovo níže pro skrytí příspěvků, které ho obsahují. - Blokovaný obsah - Předvolby filtrování - V této relaci nebyly žádné účty označeny jako spam. - diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 59a160a894..e98f445534 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2563,23 +2563,4 @@ anz der Bedingungen ist erforderlich Du hast heute bereits %1$d Ereignisse dieses Typs veröffentlicht (Limit %2$d). Du erfüllst die Web-of-Trust-Anforderungen dieser Community nicht. Das aktuelle Community-Regeldokument wurde als veraltet abgelehnt. - Speichert automatisch ein Entwurfsereignis, wenn du tippst oder den Editor mit ungesendetem Text verlässt, und sendet es an deine privaten ausgehenden Relays. - Entwürfe automatisch erstellen - Editor-Einstellungen - Eine externe Ressource kommentieren - Eine Webseite kommentieren - Die Aktion konnte nicht vorbereitet werden. - Aktion fehlgeschlagen - App-Empfehlungen - Profilabzeichen - Follower-Feed - Erhaltene-Zaps-Feed - Profil-Oberfläche - Wähle, welche Abschnitte und Feeds auf den Profilseiten erscheinen. Alle Optionen sind standardmäßig aktiviert. - Du hast noch keine Benutzer blockiert. - Keine versteckten Wörter. Füge unten ein Wort hinzu, um Beiträge auszublenden, die es enthalten. - Blockierte Inhalte - Filtereinstellungen - In dieser Sitzung wurden keine Konten als Spam markiert. - diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 52a8e2ba34..1ed62a9c3a 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2558,23 +2558,4 @@ Você já publicou %1$d eventos deste tipo hoje (limite %2$d). Você não atende aos requisitos da rede de confiança desta comunidade. O documento mais recente de regras da comunidade foi rejeitado por estar desatualizado. - Salva um evento de rascunho automaticamente quando você digita ou sai do compositor com texto não enviado e envia para seus relays de saída privados. - Criar rascunhos automaticamente - Configurações de composição - Comentar em um recurso externo - Comentar em um site - A ação não pôde ser preparada. - Falha na ação - Recomendações de Apps - Selos de perfil - Feed de Seguidores - Feed de Zaps Recebidos - Interface do perfil - Escolha quais seções e feeds aparecem nas telas de perfil dos usuários. Todas as opções estão habilitadas por padrão. - Você ainda não bloqueou nenhum usuário. - Nenhuma palavra oculta. Adicione uma palavra abaixo para ocultar publicações que a contenham. - Conteúdo bloqueado - Preferências de filtragem - Nenhuma conta foi marcada como spam nesta sessão. - diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 5eaee56103..f5949a68b0 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2557,23 +2557,4 @@ Du har redan publicerat %1$d händelser av den här typen idag (gräns %2$d). Du uppfyller inte den här gemenskapens krav på förtroendenät. Det senaste dokumentet med gemenskapsregler avvisades som föråldrat. - Sparar ett utkast automatiskt när du skriver eller lämnar redigeraren med osänd text och skickar det till dina privata utgående reläer. - Skapa utkast automatiskt - Skrivinställningar - Kommentera en extern resurs - Kommentera en webbplats - Åtgärden kunde inte förberedas. - Åtgärden misslyckades - App-rekommendationer - Profilmärken - Följarflöde - Mottagna zaps-flöde - Profilgränssnitt - Välj vilka sektioner och flöden som visas på användarprofiler. Alla alternativ är aktiverade som standard. - Du har inte blockerat några användare än. - Inga dolda ord. Lägg till ett ord nedan för att dölja inlägg som innehåller det. - Blockerat innehåll - Filterinställningar - Inga konton har flaggats som spam under denna session. -