This commit is contained in:
Vitor Pamplona
2026-05-14 15:20:26 -04:00
30 changed files with 265 additions and 117 deletions
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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/
+2 -2
View File
@@ -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
```
---
@@ -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<NormalizedRelayUrl> {
fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> = computeRelayListToBroadcast(event, mutableSetOf())
private fun computeRelayListToBroadcast(
event: Event,
visited: MutableSet<HexKey>,
): Set<NormalizedRelayUrl> {
// 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))
}
}
}
@@ -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 {
@@ -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<Boolean> = MutableStateFlow(true),
val maxHashtagLimit: MutableStateFlow<Int> = MutableStateFlow(5),
var sendKind0EventsToLocalRelay: MutableStateFlow<Boolean> = MutableStateFlow(false),
val disableClientTag: MutableStateFlow<Boolean> = MutableStateFlow(false),
val addClientTag: MutableStateFlow<Boolean> = 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
@@ -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,
)
@@ -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() }
@@ -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 {
@@ -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,
@@ -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,
)
}
}
@@ -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<String>()) }
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)
}
},
@@ -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
@@ -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(
@@ -1122,8 +1122,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">Varovat, když příspěvky obsahují hlášení od vašich sledovaných osob</string>
<string name="filter_spam_from_strangers_title">Filtrovat spam</string>
<string name="filter_spam_from_strangers_explainer">Skryje příspěvky od cizinců, které byly přesně stejné 5 a více krát</string>
<string name="disable_client_tag_title">Nepřidávat klientský tag k mým událostem</string>
<string name="disable_client_tag_explainer">Pokud je povoleno, Amethyst nebude k tvým publikovaným událostem přidávat klientský tag NIP-89.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Upozornit na hlášení</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Zobrazí varovnou zprávu, když příspěvky mají 5 nebo více hlášení od vašich sledovaných</string>
<string name="report_warning_threshold_title">Práh varování o nahlášeních</string>
@@ -1115,8 +1115,6 @@ anz der Bedingungen ist erforderlich</string>
<string name="warn_when_posts_have_reports_from_your_follows">Warnung bei Meldungen von deinen Abonnements</string>
<string name="filter_spam_from_strangers_title">Spam filtern</string>
<string name="filter_spam_from_strangers_explainer">Versteckt Beiträge von Fremden, die 5 oder mehr Male genau die gleichen waren</string>
<string name="disable_client_tag_title">Kein Client-Tag zu meinen Ereignissen hinzufügen</string>
<string name="disable_client_tag_explainer">Wenn aktiviert, fügt Amethyst deinen veröffentlichten Ereignissen kein NIP-89 Client-Tag hinzu.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Bei Berichten warnen</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Zeigt eine Warnmeldung an, wenn die Beiträge 5 oder mehr Berichte von den folgenden Beiträgen enthalten</string>
<string name="report_warning_threshold_title">Warnschwelle für Meldungen</string>
@@ -1100,8 +1100,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">चेतावनी दें जब पत्र सूचित किये गये हों आपके द्वारा अनुचरित व्यक्तियों से</string>
<string name="filter_spam_from_strangers_title">कचरालेख छलनी</string>
<string name="filter_spam_from_strangers_explainer">अज्ञात लोगों से पत्र छिपाएँ जो निरन्तर 5 अथवा अधिक बार यथावत समान थे</string>
<string name="disable_client_tag_title">मेरे घटनाओं में ग्राहक सूचक ना जोडें</string>
<string name="disable_client_tag_explainer">जब सक्षम अमेथिस्ट निप॰८९ ग्राहक सूचक नहीं जोडेगा आपके द्वारा प्रकाशित घटनाओं में।</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">सूचनाएँ प्राप्त होने पर चेतावनी दें</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">चेतावनी सन्देश दिखाता है जब प्रकाशित पत्र 5 अथवा अधिक बार सूचित हो आपके द्वारा अनुचरित लेखाओं से</string>
<string name="report_warning_threshold_title">चेतावनी सीमा सूचना</string>
@@ -545,7 +545,7 @@
<string name="badge_award_received_title">Ön kapott egy kitűzőt</string>
<string name="profile_badges_title">Profilkitűzők</string>
<string name="profile_badges_header">Kitűzők · %1$d </string>
<string name="profile_badges_description">Válassza ki, hogy a megszerzett jelvények közül melyek jelenjenek meg a profilban.</string>
<string name="profile_badges_description">Válassza ki, hogy a megszerzett kitűzők közül melyek jelenjenek meg a profilban.</string>
<string name="profile_badges_empty">Ön még nem kapott kitűzőt.</string>
<string name="pictures">Képek</string>
<string name="shorts">Rövidek</string>
@@ -657,6 +657,8 @@
<string name="nest_confirm_force_mute_body">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.</string>
<string name="nest_confirm_force_mute_confirm">Kényszerített némítás</string>
<string name="nest_confirm_cancel">Mégse</string>
<string name="nest_toast_host_action_failed_title">Sikertelen művelet</string>
<string name="nest_toast_host_action_failed_template_null">A műveletet nem sikerült előkészíteni.</string>
<string name="nest_share_action">Szoba megosztása</string>
<string name="nest_minimize">Minimalizálás</string>
<string name="nest_minimize_description">Minimalizálás a további hallgatáshoz</string>
@@ -1114,8 +1116,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">Figyelmeztessen amikor a bejegyzések jelentve vannak azok által akiket követek</string>
<string name="filter_spam_from_strangers_title">Kéretlen tartalomszűrő</string>
<string name="filter_spam_from_strangers_explainer">Elrejti az ismeretlen felhasználók hozzászólásait, amelyek pontosan ugyanazok voltak 5 vagy több alkalommal</string>
<string name="disable_client_tag_title">Ne legyenek megjelölve az eseményeim klienscímkével</string>
<string name="disable_client_tag_explainer">Ha engedélyezve van, akkor az Amethyst nem fűz NIP-89 klienscímkét az Ön által közzétett eseményekhez.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Figyelmeztetés a jelentésekre</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Figyelmeztető üzenetet jelenít meg, ha az Ön követői bejelentést tettek bejegyzésekről vagy profilokról</string>
<string name="report_warning_threshold_title">Jelentési figyelmeztetés küszöbértéke</string>
@@ -1756,7 +1756,7 @@
<string name="outbox_relays_title">Átjátszók a kimenő üzenetkhez</string>
<string name="outbox_relays_not_found">Állítsa be a nyilvános kimenő üzenetek átjátszóit a bejegyzéshez</string>
<string name="outbox_relays_not_found_description">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. </string>
<string name="outbox_relays_not_found_editing">Adjon meg 13 á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</string>
<string name="outbox_relays_not_found_editing">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</string>
<string name="outbox_relays_not_found_examples">Jó választási lehetőségek:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social</string>
<string name="inbox_relays_title">Átjátszók a bejövő üzenetkhez</string>
<string name="inbox_relays_not_found">Állítsa be a nyilvános bejövő üzenetek átjátszóit az értesítések fogadásához</string>
@@ -1078,8 +1078,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">Waarschuwen bij rapportages van volgers</string>
<string name="filter_spam_from_strangers_title">Spamfilter</string>
<string name="filter_spam_from_strangers_explainer">Verbergt identieke berichten van onbekenden die 5 keer of vaker voorkomen</string>
<string name="disable_client_tag_title">Geen client-tag toevoegen aan mijn events</string>
<string name="disable_client_tag_explainer">Wanneer ingeschakeld voegt Amethyst geen NIP-89 client-tag toe aan events die je publiceert.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Waarschuwen bij rapportages</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Toont waarschuwing wanneer een bericht 5 of meer rapportages van je volgers heeft</string>
<string name="show_sensitive_content_title">Gevoelige inhoud tonen</string>
@@ -667,6 +667,8 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="nest_confirm_force_mute_body">Prosi klienta %1$so wyciszenie mikrofonu. Niektórzy klienci mogą zignorować to polecenie.</string>
<string name="nest_confirm_force_mute_confirm">Wymuś wyciszenie</string>
<string name="nest_confirm_cancel">Anuluj</string>
<string name="nest_toast_host_action_failed_title">Akcja nieudana</string>
<string name="nest_toast_host_action_failed_template_null">Nie udało się przygotować tej akcji.</string>
<string name="nest_share_action">Udostępnij pokój</string>
<string name="nest_minimize">Zminimalizuj</string>
<string name="nest_minimize_description">Minimalizuj aby słuchać</string>
@@ -1124,8 +1126,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="warn_when_posts_have_reports_from_your_follows">Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz</string>
<string name="filter_spam_from_strangers_title">Filtr spamu</string>
<string name="filter_spam_from_strangers_explainer">Ukrywa posty od nieznajomych, które były dokładnie takie same przez 5 lub więcej razy</string>
<string name="disable_client_tag_title">Nie dodawaj tagu klienta do moich publikacji</string>
<string name="disable_client_tag_explainer">Po włączeniu tej opcji Amethyst nie będzie dołączać tagu klienta NIP-89 do Twoich publikacji.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Ostrzeżenie o zgłoszeniach</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Wyświetla komunikat ostrzegawczy, gdy posty zostały zgłoszone przez 5 lub więcej obserwowanych osób</string>
<string name="report_warning_threshold_title">Zgłoś próg ostrzegawczy</string>
@@ -1136,6 +1136,12 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="max_hashtag_limit_explainer">Ukrywa posty zawierające więcej hashtagów niż ten limit. Ustaw wartość 0, aby wyłączyć tę funkcję</string>
<string name="hide_community_rules_violations_title">Ukryj posty, które naruszają zasady społeczności</string>
<string name="hide_community_rules_violations_explainer">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.</string>
<string name="security_section_filtering_preferences">Preferencje filtrowania</string>
<string name="security_section_blocked_content">Zablokowane treści</string>
<string name="security_unlimited"></string>
<string name="security_blocked_users_empty">Nie zablokowałeś jeszcze żadnych użytkowników.</string>
<string name="security_spamming_users_empty">W tej sesji żadne konta nie zostały oznaczone jako spam.</string>
<string name="security_hidden_words_empty">Brak ukrytych słów. Wpisz poniżej słowo, aby ukryć posty, które je zawierają.</string>
<string name="new_reaction_symbol">Nowy Symbol Odzewu</string>
<string name="no_reaction_type_setup_long_press_to_change">Brak wstępnie wybranych typów reakcji dla tego użytkownika. Przytrzymaj przycisk serce, aby zmienić</string>
<string name="zapraiser">Zapraiser</string>
@@ -1333,6 +1339,8 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="geohash_exclusive_explainer">Tylko obserwatorzy z twojej lokalizacji to zobaczą. Twoi ogólni obserwatorzy nie zobaczą tego.</string>
<string name="hashtag_exclusive">Wpis hashtagowy</string>
<string name="hashtag_exclusive_explainer">Tylko obserwujący ten hashtag to zobaczą. Twoi ogólni obserwujący nie zobaczą tego.</string>
<string name="external_url_scope">Skomentuj na stronie internetowej</string>
<string name="external_id_scope">Skomentuj na zewnętrznym serwisie</string>
<string name="long_form_reading_minutes">Czas czytania: %1$d min.</string>
<string name="loading_location">Pobieranie lokalizacji</string>
<string name="lack_location_permissions">Brak dostępu do lokalizacji</string>
@@ -1671,6 +1679,12 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="home_tabs_settings">Karty główne</string>
<string name="home_tabs_settings_description">Wybierz, które karty pojawiają się na ekranie głównym. Gdy tylko jedna karta jest aktywna, pasek karty jest ukryty.</string>
<string name="home_tab_everything">Wszystko</string>
<string name="profile_ui_settings">Interfejs profilu</string>
<string name="profile_ui_settings_description">Wybierz które sekcje i kanały pojawiają się na ekranach profilu użytkownika. Wszystkie opcje są domyślnie włączone.</string>
<string name="profile_ui_setting_badges">Odznaki w profilu</string>
<string name="profile_ui_setting_app_recommendations">Rekomendacje aplikacji</string>
<string name="profile_ui_setting_zap_received_feed">Kanał, który otrzymał Zapa</string>
<string name="profile_ui_setting_followers_feed">Kanał Obserwowanych</string>
<string name="reactions_settings">Ustawienia reakcji</string>
<string name="reactions_settings_description">Skonfiguruj które przyciski reakcji będą wyświetlane, ich kolejność i czy wyświetlić liczniki.</string>
<string name="reactions_settings_enabled">Włączone</string>
@@ -2530,6 +2544,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="ai_writing_setting_description">Używa modelu sztucznej inteligencji wbudowanego w urządzenie, proponując poprawki tekstu i zmiany tonu wypowiedzi.</string>
<string name="tracked_broadcasts_setting_title">Monitorowane transmisje</string>
<string name="tracked_broadcasts_setting_description">Podczas wysyłania zdarzeń korzystaj z monitora transmisji. Pokazuje postęp na żywo i status transmisji podczas nadawania.</string>
<string name="compose_settings">Skonfiguruj ustawienia</string>
<string name="auto_create_drafts_setting_title">Automatycznie twórz wersje robocze</string>
<string name="auto_create_drafts_setting_description">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.</string>
<string name="ai_writing_use_this">Użyj tego</string>
<string name="ai_writing_dismiss">Ignoruj</string>
<string name="ai_tone_correct">Popraw</string>
@@ -428,7 +428,6 @@
<string name="scheduled_posts_logout_toast_zero">Desconectado</string>
<plurals name="scheduled_posts_logout_toast">
<item quantity="one">Desconectado · %d post agendado excluído</item>
<item quantity="many">Desconectado · %d de posts agendados excluídos</item>
<item quantity="other">Desconectado · %d posts agendados excluídos</item>
</plurals>
<string name="scheduled_posts_notification_sent_title">Post agendado publicado</string>
@@ -446,22 +445,18 @@
<string name="scheduled_posts_status_cancelled">Cancelado</string>
<plurals name="scheduled_posts_logout_warning">
<item quantity="one">Você tem %d post agendado que ainda não foi publicado. Sair excluirá esse post permanentemente.</item>
<item quantity="many">Você tem %d de posts agendados que ainda não foram publicados. Sair excluirá esses posts permanentemente.</item>
<item quantity="other">Você tem %d posts agendados que ainda não foram publicados. Sair excluirá esses posts permanentemente.</item>
</plurals>
<plurals name="scheduled_posts_subtitle_queued">
<item quantity="one">%d na fila</item>
<item quantity="many">%d na fila</item>
<item quantity="other">%d na fila</item>
</plurals>
<plurals name="scheduled_posts_subtitle_due_suffix">
<item quantity="one"> · %d em 1h</item>
<item quantity="many"> · %d em 1h</item>
<item quantity="other"> · %d em 1h</item>
</plurals>
<plurals name="scheduled_posts_relay_count">
<item quantity="one">para %d relay</item>
<item quantity="many">para %d de relays</item>
<item quantity="other">para %d relays</item>
</plurals>
<string name="scheduled_posts_event_id_copied">ID do post copiado</string>
@@ -621,7 +616,6 @@
<string name="nest_leave_host_close_failed">Não foi possível marcar a sala como fechada. Saindo mesmo assim — a sala será fechada automaticamente.</string>
<plurals name="nest_listener_count">
<item quantity="one">%1$d ouvinte</item>
<item quantity="many">%1$d de ouvintes</item>
<item quantity="other">%1$d ouvintes</item>
</plurals>
<string name="nest_live_chip">AO VIVO</string>
@@ -1116,8 +1110,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">Avise quando as postagens tiverem denuncias de seus seguidores</string>
<string name="filter_spam_from_strangers_title">Filtrar spam</string>
<string name="filter_spam_from_strangers_explainer">Esconde publicações de desconhecidos que eram exatamente o mesmo por 5 ou mais vezes</string>
<string name="disable_client_tag_title">Não adicionar tag de cliente aos meus eventos</string>
<string name="disable_client_tag_explainer">Quando ativado, o Amethyst não anexará uma tag de cliente NIP-89 aos eventos que você publicar.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Avisar em relatórios</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Mostra uma mensagem de aviso quando as postagens tiverem 5 ou mais relatórios de suas seguintes</string>
<string name="report_warning_threshold_title">Limite de aviso de denúncias</string>
@@ -2272,32 +2264,26 @@
<string name="last_seen_never">Nunca visto</string>
<plurals name="duration_minutes">
<item quantity="one">%1$d minuto</item>
<item quantity="many">%1$d de minutos</item>
<item quantity="other">%1$d minutos</item>
</plurals>
<plurals name="duration_hours">
<item quantity="one">%1$d hora</item>
<item quantity="many">%1$d de horas</item>
<item quantity="other">%1$d horas</item>
</plurals>
<plurals name="duration_days">
<item quantity="one">%1$d dia</item>
<item quantity="many">%1$d de dias</item>
<item quantity="other">%1$d dias</item>
</plurals>
<plurals name="duration_weeks">
<item quantity="one">%1$d semana</item>
<item quantity="many">%1$d de semanas</item>
<item quantity="other">%1$d semanas</item>
</plurals>
<plurals name="duration_months">
<item quantity="one">%1$d mês</item>
<item quantity="many">%1$d de meses</item>
<item quantity="other">%1$d meses</item>
</plurals>
<plurals name="duration_years">
<item quantity="one">%1$d ano</item>
<item quantity="many">%1$d de anos</item>
<item quantity="other">%1$d anos</item>
</plurals>
<string name="event_sync_less_than_until">&lt;%1$s</string>
@@ -1109,8 +1109,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">Varna när inlägg har rapporter från dina följare</string>
<string name="filter_spam_from_strangers_title">Filtrera spam</string>
<string name="filter_spam_from_strangers_explainer">Döljer inlägg från främlingar som var exakt samma för 5 eller fler gånger</string>
<string name="disable_client_tag_title">Lägg inte till klienttagg i mina händelser</string>
<string name="disable_client_tag_explainer">När aktiverat kommer Amethyst inte att lägga till en NIP-89 klienttagg till händelser du publicerar.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Varna vid rapporter</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Visar ett varningsmeddelande när inlägg har 5 eller fler rapporter från följande</string>
<string name="report_warning_threshold_title">Tröskel för rapportvarning</string>
@@ -1107,8 +1107,6 @@
<string name="warn_when_posts_have_reports_from_your_follows">当帖子有你的关注的报告时警告</string>
<string name="filter_spam_from_strangers_title">过滤垃圾信息</string>
<string name="filter_spam_from_strangers_explainer">隐藏陌生人的五次或五次以上完全相同的帖子</string>
<string name="disable_client_tag_title">不要将客户标签添加到我的事件</string>
<string name="disable_client_tag_explainer">启用时,Amethyst 将不会在您发布的事件中附加一个 NIP-89 客户端标签。</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">举报警示</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">当帖子有来自你关注的五次及以上举报时显示警告消息</string>
<string name="report_warning_threshold_title">举报警告阈值</string>
+2 -2
View File
@@ -1232,8 +1232,8 @@
<string name="filter_spam_from_strangers_title">Filter spam</string>
<string name="filter_spam_from_strangers_explainer">Hides posts from strangers that were exactly the same for 5 or more times</string>
<string name="disable_client_tag_title">Don\'t add client tag to my events</string>
<string name="disable_client_tag_explainer">When enabled, Amethyst will not append a NIP-89 client tag to events you publish.</string>
<string name="add_client_tag_title">Add client tag to my events</string>
<string name="add_client_tag_explainer">When enabled, Amethyst appends a NIP-89 client tag to events you publish.</string>
<string name="warn_when_posts_have_reports_from_your_follows_title">Warn on reports</string>
<string name="warn_when_posts_have_reports_from_your_follows_explainer">Shows a warning message when posts or profiles have reports from your follows</string>
<string name="report_warning_threshold_title">Report warning threshold</string>
+5 -4
View File
@@ -173,13 +173,14 @@ tasks.withType<Download>().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)

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

@@ -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<SubscribeHandle?>(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
@@ -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<Unit>()
val consumerProgress = MutableStateFlow(0)
@Suppress("UNCHECKED_CAST")
val objectsAsShared = handle.objects as SharedFlow<MoqObject>
// 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 {