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/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 396eee5ce0..971da63e26 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 } @@ -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)) } } } 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/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, 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/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) } }, 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/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( 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..481d0b8826 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 @@ -1114,8 +1116,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 @@ -1756,7 +1756,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-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..9336f06122 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ć @@ -1124,8 +1126,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 @@ -1136,6 +1136,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 +1339,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 +1679,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 +2544,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..1ed62a9c3a 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 @@ -1116,8 +1110,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 @@ -2272,32 +2264,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 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 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 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 {