Merge pull request #423 from greenart7c3/claude/add-beta-release-support-TFp6Y

Add update channel selection (stable/beta) for app updates
This commit is contained in:
greenart7c3
2026-05-06 10:22:08 -03:00
committed by GitHub
6 changed files with 76 additions and 6 deletions
@@ -11,6 +11,7 @@ import androidx.core.os.LocaleListCompat
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.AmberSettings
import com.greenart7c3.nostrsigner.models.TorMode
import com.greenart7c3.nostrsigner.models.UpdateChannel
import com.greenart7c3.nostrsigner.models.UpdateCheckFrequency
import com.greenart7c3.nostrsigner.models.defaultAppRelays
import com.greenart7c3.nostrsigner.models.defaultIndexerRelays
@@ -59,6 +60,7 @@ private enum class SettingsKeys(val key: String) {
AUTH_WHITELIST("auth_whitelist"),
AUTO_CHECK_UPDATES("auto_check_updates"),
UPDATE_CHECK_FREQUENCY("update_check_frequency"),
UPDATE_CHANNEL("update_channel"),
LAST_UPDATE_CHECK_TIME("last_update_check_time"),
START_SERVICE_ON_BOOT("start_service_on_boot"),
WEBDAV_URL("webdav_url"),
@@ -264,6 +266,13 @@ object LocalPreferences {
} catch (_: IllegalArgumentException) {
UpdateCheckFrequency.DAILY
},
updateChannel = try {
UpdateChannel.valueOf(
getString(SettingsKeys.UPDATE_CHANNEL.key, UpdateChannel.STABLE.name)!!,
)
} catch (_: IllegalArgumentException) {
UpdateChannel.STABLE
},
startServiceOnBoot = getBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, true),
rateLimitEnabled = getBoolean(SettingsKeys.RATE_LIMIT_ENABLED.key, true),
rateLimitMaxPerWindow = getInt(SettingsKeys.RATE_LIMIT_MAX_PER_WINDOW.key, 5),
@@ -535,6 +544,15 @@ object LocalPreferences {
Amber.instance.settings = loadSettingsFromEncryptedStorage()
}
fun updateUpdateChannel(context: Context, channel: UpdateChannel) {
sharedPrefs(context).edit {
apply {
putString(SettingsKeys.UPDATE_CHANNEL.key, channel.name)
}
}
Amber.instance.settings = loadSettingsFromEncryptedStorage()
}
fun getLastUpdateCheckTime(context: Context): Long = sharedPrefs(context).getLong(SettingsKeys.LAST_UPDATE_CHECK_TIME.key, 0L)
fun setLastUpdateCheckTime(context: Context, time: Long) {
@@ -32,6 +32,7 @@ data class AmberSettings(
val authWhitelist: List<String> = emptyList(),
val autoCheckUpdates: Boolean = true,
val updateCheckFrequency: UpdateCheckFrequency = UpdateCheckFrequency.DAILY,
val updateChannel: UpdateChannel = UpdateChannel.STABLE,
val startServiceOnBoot: Boolean = true,
val rateLimitEnabled: Boolean = true,
val rateLimitMaxPerWindow: Int = 5,
@@ -0,0 +1,8 @@
package com.greenart7c3.nostrsigner.models
import com.greenart7c3.nostrsigner.R
enum class UpdateChannel(val resourceId: Int) {
STABLE(R.string.update_channel_stable),
BETA(R.string.update_channel_beta),
}
@@ -8,6 +8,7 @@ import androidx.core.content.FileProvider
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import com.greenart7c3.nostrsigner.models.UpdateChannel
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.verify
@@ -34,7 +35,11 @@ import okhttp3.Request
private const val EOSE_TIMEOUT_MS = 15_000L
private const val RELEASE_KIND = 30063
private const val ZAPSTORE_RELAY = "wss://relay.zapstore.dev"
private val UPDATE_RELAY_URLS = listOf(
"wss://relay.zapstore.dev",
"wss://relay.damus.io",
"wss://nos.lol",
)
private const val APP_IDENTIFIER = "com.greenart7c3.nostrsigner"
enum class DownloadState {
@@ -57,7 +62,8 @@ class ZapstoreUpdater(
private val fileSubId = UUID.randomUUID().toString()
private val pendingFileEventIds = mutableListOf<String>()
private val versionByEventId = mutableMapOf<String, Pair<String, Long>>()
private val zapstoreRelay: NormalizedRelayUrl? = RelayUrlNormalizer.normalizeOrNull(ZAPSTORE_RELAY)
private val updateRelays: List<NormalizedRelayUrl> =
UPDATE_RELAY_URLS.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
private var timeoutJob: Job? = null
init {
@@ -76,7 +82,7 @@ class ZapstoreUpdater(
fun checkForUpdates() {
if (BuildFlavorChecker.isOfflineFlavor()) return
val relay = zapstoreRelay ?: return
if (updateRelays.isEmpty()) return
if (isChecking.value) return
isChecking.value = true
@@ -91,7 +97,7 @@ class ZapstoreUpdater(
limit = 10,
)
client.subscribe(releaseSubId, mapOf(relay to listOf(releaseFilter)))
client.subscribe(releaseSubId, updateRelays.associateWith { listOf(releaseFilter) })
timeoutJob = scope.launch {
delay(EOSE_TIMEOUT_MS)
@@ -114,6 +120,12 @@ class ZapstoreUpdater(
val isForThisApp = identifier.startsWith(APP_IDENTIFIER) || appId == APP_IDENTIFIER
if (!isForThisApp) return
// Channel filter: stable users skip events tagged c=beta. Events with no `c`
// tag (legacy releases) are treated as stable and shown on both channels.
val channelTag = tags.firstOrNull { it.size > 1 && it[0] == "c" }?.getOrNull(1)
val isBeta = channelTag.equals("beta", ignoreCase = true)
if (isBeta && Amber.instance.settings.updateChannel == UpdateChannel.STABLE) return
val version = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.getOrNull(1) ?: return
if (!isNewerVersion(version)) return
@@ -144,14 +156,14 @@ class ZapstoreUpdater(
}
private fun fetchFileEvents() {
val relay = zapstoreRelay ?: run {
if (updateRelays.isEmpty()) {
finishCheck()
return
}
val fileFilter = Filter(
ids = pendingFileEventIds.toList(),
)
client.subscribe(fileSubId, mapOf(relay to listOf(fileFilter)))
client.subscribe(fileSubId, updateRelays.associateWith { listOf(fileFilter) })
}
private fun processFileEvent(event: Event) {
@@ -30,6 +30,7 @@ import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.UpdateChannel
import com.greenart7c3.nostrsigner.models.UpdateCheckFrequency
import com.greenart7c3.nostrsigner.service.DownloadState
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
@@ -68,6 +69,7 @@ fun UpdateSettingsScreen(modifier: Modifier = Modifier) {
val dlProgress by updater.downloadProgress.collectAsStateWithLifecycle()
var autoCheck by remember { mutableStateOf(Amber.instance.settings.autoCheckUpdates) }
var frequency by remember { mutableStateOf(Amber.instance.settings.updateCheckFrequency) }
var channel by remember { mutableStateOf(Amber.instance.settings.updateChannel) }
// Status row
val statusText = when {
@@ -168,5 +170,30 @@ fun UpdateSettingsScreen(modifier: Modifier = Modifier) {
},
)
}
Spacer(modifier = Modifier.height(8.dp))
val channelOptions = UpdateChannel.entries.map {
TitleExplainer(stringResource(it.resourceId))
}.toImmutableList()
SettingsRow(
name = R.string.update_channel,
description = R.string.update_channel_description,
selectedItems = channelOptions,
selectedIndex = UpdateChannel.entries.indexOf(channel),
onSelect = { index ->
val selected = UpdateChannel.entries[index]
if (selected != channel) {
channel = selected
scope.launch(Dispatchers.IO) {
LocalPreferences.updateUpdateChannel(context, selected)
// Reset last-check timestamp so the next check actually runs
// and surfaces a release on the newly selected channel.
LocalPreferences.setLastUpdateCheckTime(context, 0L)
}
}
},
)
}
}
+4
View File
@@ -628,6 +628,10 @@
<string name="update_frequency_on_startup">Every app start</string>
<string name="update_frequency_daily">Daily</string>
<string name="update_frequency_weekly">Weekly</string>
<string name="update_channel">Update channel</string>
<string name="update_channel_description">Choose between stable and beta (pre-release) builds</string>
<string name="update_channel_stable">Stable</string>
<string name="update_channel_beta">Beta (pre-release)</string>
<string name="current_version">Current version: %1$s</string>
<string name="updates_not_available">Updates are not available for this build.</string>
<string name="discover_more">Discover more Nostr apps for android at</string>