mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 03:18:23 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user