mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix(model): keep TopFilter's pre-move serial names so saved prefs survive
The move to commons changed every subclass's kotlinx default serial name (the FQN), which is the polymorphic type discriminator JsonMapper writes into the per-account DEFAULT_*_FOLLOW_LIST preferences. Decode failures are swallowed by parseTopFilterOrDefault, so without this every user's ~30 saved tab selections silently reset on upgrade. @SerialName pins the old names; TopFilterSerialNameTest pins them (and legacy-JSON decoding) on JVM and iOS. Also annotate the nativeMain Address actual @Serializable to match the jvm/android actuals: @Contextual properties only get the plugin's compile-time fallback on targets whose actual is serializable, so encoding an address-carrying TopFilter threw on iOS. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011S1vbFWVVAMFDT8PTgdibV
This commit is contained in:
+20
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -33,6 +34,7 @@ sealed class TopFilter(
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Global")
|
||||
object Global : TopFilter(" Global ")
|
||||
|
||||
/**
|
||||
@@ -43,18 +45,23 @@ sealed class TopFilter(
|
||||
* [Global] shows every event that p-tags the user instead.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Selected")
|
||||
object Selected : TopFilter(" Selected ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.AllFollows")
|
||||
object AllFollows : TopFilter(" All Follows ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.AllUserFollows")
|
||||
object AllUserFollows : TopFilter(" All User Follows ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.DefaultFollows")
|
||||
object DefaultFollows : TopFilter(" Main User Follows ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.AroundMe")
|
||||
object AroundMe : TopFilter(" Around Me ")
|
||||
|
||||
/**
|
||||
@@ -63,52 +70,64 @@ sealed class TopFilter(
|
||||
* [Geohash] instead — it is never persisted or dispatched to a feed flow.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.TeleportPicker")
|
||||
object TeleportPicker : TopFilter(" Teleport ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Mine")
|
||||
object Mine : TopFilter(" Mine ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.PeopleList")
|
||||
class PeopleList(
|
||||
@Contextual override val address: Address,
|
||||
) : TopFilter(address.toValue()),
|
||||
AddressableTopFilter
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.MuteList")
|
||||
class MuteList(
|
||||
@Contextual override val address: Address,
|
||||
) : TopFilter(address.toValue()),
|
||||
AddressableTopFilter
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Community")
|
||||
class Community(
|
||||
@Contextual override val address: Address,
|
||||
) : TopFilter("Community/${address.toValue()}"),
|
||||
AddressableTopFilter
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Hashtag")
|
||||
class Hashtag(
|
||||
val tag: String,
|
||||
) : TopFilter("Hashtag/$tag")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Geohash")
|
||||
class Geohash(
|
||||
val tag: String,
|
||||
) : TopFilter("Geohash/$tag")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.Relay")
|
||||
class Relay(
|
||||
val url: String,
|
||||
) : TopFilter("Relay/$url")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.FavoriteAlgoFeed")
|
||||
class FavoriteAlgoFeed(
|
||||
@Contextual val address: Address,
|
||||
) : TopFilter("FavoriteAlgoFeed/${address.toValue()}")
|
||||
|
||||
@Serializable object AllFavoriteAlgoFeeds : TopFilter(" All Favourite DVMs ")
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.AllFavoriteAlgoFeeds")
|
||||
object AllFavoriteAlgoFeeds : TopFilter(" All Favourite DVMs ")
|
||||
|
||||
@Serializable
|
||||
@SerialName("com.vitorpamplona.amethyst.model.TopFilter.InterestSet")
|
||||
class InterestSet(
|
||||
@Contextual val address: Address,
|
||||
) : TopFilter("InterestSet/${address.toValue()}")
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.commons.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Pins every [TopFilter] subclass to the serial name it had before the class
|
||||
* moved from `com.vitorpamplona.amethyst.model` to commons. These names are
|
||||
* the polymorphic `type` discriminator [JsonMapper] writes into per-account
|
||||
* preferences (LocalPreferences' DEFAULT_*_FOLLOW_LIST keys); if one changes,
|
||||
* every user's saved tab selections silently reset to defaults on upgrade,
|
||||
* because the decode failure is swallowed by parseTopFilterOrDefault.
|
||||
*/
|
||||
class TopFilterSerialNameTest {
|
||||
private val oldPrefix = "com.vitorpamplona.amethyst.model.TopFilter"
|
||||
private val address = Address(30000, "e28ffe100522bc27e01f0c7c1a0e42722f7d5b5a2ba1fb066bde54a2c53e91a5", "follows")
|
||||
|
||||
private val expected: Map<String, TopFilter> =
|
||||
mapOf(
|
||||
"Global" to TopFilter.Global,
|
||||
"Selected" to TopFilter.Selected,
|
||||
"AllFollows" to TopFilter.AllFollows,
|
||||
"AllUserFollows" to TopFilter.AllUserFollows,
|
||||
"DefaultFollows" to TopFilter.DefaultFollows,
|
||||
"AroundMe" to TopFilter.AroundMe,
|
||||
"TeleportPicker" to TopFilter.TeleportPicker,
|
||||
"Mine" to TopFilter.Mine,
|
||||
"PeopleList" to TopFilter.PeopleList(address),
|
||||
"MuteList" to TopFilter.MuteList(address),
|
||||
"Community" to TopFilter.Community(address),
|
||||
"Hashtag" to TopFilter.Hashtag("nostr"),
|
||||
"Geohash" to TopFilter.Geohash("u4pruyd"),
|
||||
"Relay" to TopFilter.Relay("wss://relay.example.com"),
|
||||
"FavoriteAlgoFeed" to TopFilter.FavoriteAlgoFeed(address),
|
||||
"AllFavoriteAlgoFeeds" to TopFilter.AllFavoriteAlgoFeeds,
|
||||
"InterestSet" to TopFilter.InterestSet(address),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun everySubclassKeepsItsPreMoveSerialName() {
|
||||
expected.forEach { (simpleName, value) ->
|
||||
val encoded = JsonMapper.jsonInstance.encodeToString(TopFilter.serializer(), value)
|
||||
val type =
|
||||
JsonMapper.jsonInstance
|
||||
.parseToJsonElement(encoded)
|
||||
.jsonObject["type"]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
assertEquals(
|
||||
"$oldPrefix.$simpleName",
|
||||
type,
|
||||
"TopFilter.$simpleName must keep the pre-move serial name or saved prefs reset",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodesPrefsWrittenBeforeTheMove() {
|
||||
// Literal JSON as written by the pre-move app: the base class's `code`
|
||||
// constructor property is a serialized field alongside the discriminator.
|
||||
val legacyHashtag = """{"type":"$oldPrefix.Hashtag","code":"Hashtag/nostr","tag":"nostr"}"""
|
||||
val decodedTag = JsonMapper.jsonInstance.decodeFromString(TopFilter.serializer(), legacyHashtag)
|
||||
assertEquals("Hashtag/nostr", decodedTag.code)
|
||||
|
||||
// The address is stored in the same shape main wrote (Address's own fields).
|
||||
val legacyPeopleList =
|
||||
"""{"type":"$oldPrefix.PeopleList","code":"${address.toValue()}","address":{"kind":30000,""" +
|
||||
""""pubKeyHex":"${address.pubKeyHex}","dTag":"follows"}}"""
|
||||
val decodedList = JsonMapper.jsonInstance.decodeFromString(TopFilter.serializer(), legacyPeopleList)
|
||||
assertEquals(address.toValue(), decodedList.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roundTripsEverySubclass() {
|
||||
expected.forEach { (simpleName, value) ->
|
||||
val encoded = JsonMapper.jsonInstance.encodeToString(TopFilter.serializer(), value)
|
||||
val decoded = JsonMapper.jsonInstance.decodeFromString(TopFilter.serializer(), encoded)
|
||||
assertEquals(value.code, decoded.code, "TopFilter.$simpleName must round-trip")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.core
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
// @Serializable matches the jvm/android actuals: common code marks Address
|
||||
// properties @Contextual, and the plugin's compile-time fallback only exists
|
||||
// on targets whose actual is itself serializable — without it, encoding any
|
||||
// address-carrying type throws "Serializer for class 'Address' is not found"
|
||||
// at runtime on native.
|
||||
@Serializable
|
||||
actual data class Address actual constructor(
|
||||
actual val kind: Kind,
|
||||
actual val pubKeyHex: HexKey,
|
||||
|
||||
Reference in New Issue
Block a user