Merge pull request #3444 from vitorpamplona/claude/bottom-nav-reset-defaults-jxg5z0

Persist bottom bar defaults as blank sentinel for auto-migration
This commit is contained in:
Vitor Pamplona
2026-07-01 16:59:01 -04:00
committed by GitHub
2 changed files with 75 additions and 2 deletions
@@ -205,7 +205,7 @@ class UiSharedPreferences(
preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name
preferences[UI_USE_TRACKED_BROADCASTS] = sharedSettings.useTrackedBroadcasts.name
preferences[UI_AUTOMATICALLY_CREATE_DRAFTS] = sharedSettings.automaticallyCreateDrafts.name
preferences[UI_BOTTOM_BAR_ITEMS] = JsonMapper.toJson(sharedSettings.bottomBarItems)
preferences[UI_BOTTOM_BAR_ITEMS] = encodeBottomBarItems(sharedSettings.bottomBarItems)
preferences[UI_SHOW_HOME_NEW_THREADS_TAB] = sharedSettings.showHomeNewThreadsTab
preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] = sharedSettings.showHomeConversationsTab
preferences[UI_SHOW_HOME_EVERYTHING_TAB] = sharedSettings.showHomeEverythingTab
@@ -226,7 +226,19 @@ class UiSharedPreferences(
}
}
private fun decodeBottomBarItems(raw: String): List<BottomBarEntry>? {
/**
* Persists "follow the defaults" as a blank sentinel instead of the concrete default list.
*
* A user who resets the bottom bar (or who never customized it) should track whatever
* [DefaultBottomBarEntries] is in the *installed* app version. Storing the concrete list would
* pin them to today's default, so a future version that changes the default would never reach
* them. Storing a blank value instead makes [decodeBottomBarItems] resolve it back to the
* current [DefaultBottomBarEntries] on every load — i.e. the user is automatically migrated to
* the new default. Any genuinely customized bar is still stored as JSON.
*/
internal fun encodeBottomBarItems(items: List<BottomBarEntry>): String = if (items == DefaultBottomBarEntries) "" else JsonMapper.toJson(items)
internal fun decodeBottomBarItems(raw: String): List<BottomBarEntry>? {
if (raw.isBlank()) return DefaultBottomBarEntries
// Current format: a JSON list of BottomBarEntry (built-ins + favorites).
runCatching { return JsonMapper.fromJson<List<BottomBarEntry>>(raw) }
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.preferences
import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarEntry
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarEntries
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Locks the "reset to defaults migrates automatically" behavior.
*
* Resetting the bottom bar (or never customizing it) is persisted as a blank sentinel rather than the
* concrete default list, so that a user on the defaults tracks whatever [DefaultBottomBarEntries] the
* *installed* app version ships. If a future version changes the default, that blank value resolves to
* the new default on load — the user is migrated instead of being pinned to the old default.
*/
class BottomBarPersistenceTest {
@Test
fun defaultsAreStoredAsBlankSentinel() {
assertEquals("", UiSharedPreferences.encodeBottomBarItems(DefaultBottomBarEntries))
}
@Test
fun blankSentinelDecodesToCurrentDefaults() {
// The blank sentinel resolves to whatever DefaultBottomBarEntries this build ships. Because it
// returns the current constant (not a value frozen at reset time), a future version that changes
// the default automatically migrates every user who is on the defaults.
assertEquals(DefaultBottomBarEntries, UiSharedPreferences.decodeBottomBarItems(""))
}
@Test
fun customizedBarIsStoredVerbatimAndRoundTrips() {
val custom =
listOf(
BottomBarEntry.BuiltIn(NavBarItem.HOME),
BottomBarEntry.Favorite("url:https://example.com"),
)
val encoded = UiSharedPreferences.encodeBottomBarItems(custom)
assertEquals(custom, UiSharedPreferences.decodeBottomBarItems(encoded))
}
}