mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
refactor: move chess dismissed games to DataStore, dropping the old data
Reverses the decision made one commit earlier to leave this on SharedPreferences. That call was made to avoid a visible regression — seeding asynchronously means a dismissed game can reappear for a frame or two — but chess has few enough users that the regression does not matter, which also removes the reason to carry the old data over. Replaces the whole expect/actual trio (commonMain expect, androidMain SharedPreferences, jvmMain java.util.prefs, iosMain in-memory) with one commonMain ChessDismissedGamesStore over DataStore. That is the payoff from making the preference layer multiplatform in the first commit of this series: the platform now supplies a file location rather than an implementation. iOS gains real persistence as a side effect. Its actual had been an in-memory map, commented as standing in until an iosApp module existed, so dismissals there were lost on every launch. No migration, deliberately. The old stores keep their data and nothing reads it; a user who had dismissed a game sees it once more and dismisses it again. Android writes to a new file (chess_dismissed_games_v2) rather than reusing the old name, so the two cannot be confused. ChessLobbyLogic now seeds the dismissed set in an init launch rather than a property initializer. The seed unions rather than replaces, so a dismissal the user makes before the read lands is not overwritten by it. The two writes are fire-and-forget on the logic's own scope. Desktop gets its store from commons rather than building one itself, so a front end does not need DataStore on its own classpath. The per-OS data directory that SecretEncryption had inlined is now a single commons/jvmMain definition both use, so the key file and the stores land in the same place. 5 tests: unknown user, round trip, per-user isolation, that an empty set removes the key rather than storing an empty one, and that a later save replaces rather than merges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
This commit is contained in:
+11
-15
@@ -18,21 +18,17 @@
|
||||
* 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.nip64Chess
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
|
||||
/**
|
||||
* Persists dismissed chess game IDs locally per user.
|
||||
* Uses expect/actual for platform-specific storage.
|
||||
* Where Android keeps the dismissed-chess-games store.
|
||||
*
|
||||
* A new file rather than a migration of `chess_dismissed_games`: the dismissed
|
||||
* list is a convenience, and chess has few enough users that carrying the old
|
||||
* data over is not worth the code. Anyone who had dismissed a game sees it once
|
||||
* more and dismisses it again.
|
||||
*/
|
||||
expect class ChessDismissedGamesStorage private constructor() {
|
||||
companion object {
|
||||
fun create(context: Any? = null): ChessDismissedGamesStorage
|
||||
}
|
||||
|
||||
fun load(userPubkey: String): Set<String>
|
||||
|
||||
fun save(
|
||||
userPubkey: String,
|
||||
ids: Set<String>,
|
||||
)
|
||||
}
|
||||
internal val Context.chessDismissedGamesData by preferencesDataStore(name = "chess_dismissed_games_v2")
|
||||
+2
-2
@@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStorage
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessLobbyLogic
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessSyncStatus
|
||||
@@ -61,7 +61,7 @@ class ChessViewModelNew(
|
||||
private val publisher = AndroidChessPublisher(account)
|
||||
private val fetcher = AndroidRelayFetcher(account)
|
||||
private val metadataProvider = AndroidMetadataProvider()
|
||||
private val dismissedStorage = ChessDismissedGamesStorage.create(application)
|
||||
private val dismissedStorage = ChessDismissedGamesStore(application.chessDismissedGamesData)
|
||||
|
||||
// Shared business logic (creates its own ChessLobbyState internally)
|
||||
private val logic =
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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.nip64Chess
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* Still on SharedPreferences, deliberately, while the rest of the app moved to
|
||||
* DataStore.
|
||||
*
|
||||
* ChessLobbyLogic reads this in a property initializer and writes it from two
|
||||
* non-suspending functions. DataStore is suspend-only, so migrating would mean
|
||||
* seeding the dismissed set asynchronously — and until that load lands, a game
|
||||
* the user already dismissed reappears in the list. That is a visible
|
||||
* regression in exchange for consistency alone: this store is already an
|
||||
* expect/actual with a working iOS implementation, so unlike the rest of the
|
||||
* preference layer it is not blocking any target.
|
||||
*
|
||||
* Worth revisiting if ChessLobbyLogic ever gains a suspending initialisation
|
||||
* path of its own.
|
||||
*/
|
||||
actual class ChessDismissedGamesStorage private actual constructor() {
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
actual companion object {
|
||||
private const val PREFS_NAME = "chess_dismissed_games"
|
||||
|
||||
private fun prefsKey(userPubkey: String) = "dismissed_$userPubkey"
|
||||
|
||||
actual fun create(context: Any?): ChessDismissedGamesStorage {
|
||||
val storage = ChessDismissedGamesStorage()
|
||||
val ctx =
|
||||
context as? Context
|
||||
?: throw IllegalArgumentException("Android context required")
|
||||
storage.prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return storage
|
||||
}
|
||||
}
|
||||
|
||||
// getStringSet returns a live reference to the internal set — must copy defensively
|
||||
actual fun load(userPubkey: String): Set<String> = prefs?.getStringSet(prefsKey(userPubkey), null)?.toHashSet() ?: emptySet()
|
||||
|
||||
actual fun save(
|
||||
userPubkey: String,
|
||||
ids: Set<String>,
|
||||
) {
|
||||
prefs?.edit { putStringSet(prefsKey(userPubkey), ids) }
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.nip64Chess
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okio.IOException
|
||||
|
||||
/**
|
||||
* The chess games a user has dismissed from their completed list, per pubkey.
|
||||
*
|
||||
* Replaces the previous expect/actual trio. DataStore is multiplatform, so one
|
||||
* implementation now serves every target — and iOS gains real persistence,
|
||||
* where its actual had been an in-memory map standing in until an iosApp
|
||||
* module existed.
|
||||
*
|
||||
* Nothing is carried over from the old per-platform stores: the dismissed list
|
||||
* is a convenience, losing it costs a user one re-dismissal, and chess has few
|
||||
* enough users that a migration is not worth the code that would carry it.
|
||||
*/
|
||||
class ChessDismissedGamesStore(
|
||||
private val store: DataStore<Preferences>,
|
||||
) {
|
||||
companion object {
|
||||
internal fun keyFor(userPubkey: String) = stringSetPreferencesKey("dismissed_$userPubkey")
|
||||
}
|
||||
|
||||
suspend fun load(userPubkey: String): Set<String> =
|
||||
store.data
|
||||
.catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e }
|
||||
.first()[keyFor(userPubkey)]
|
||||
?: emptySet()
|
||||
|
||||
/** An empty set removes the key rather than storing an empty one. */
|
||||
suspend fun save(
|
||||
userPubkey: String,
|
||||
ids: Set<String>,
|
||||
) {
|
||||
store.edit { prefs ->
|
||||
if (ids.isEmpty()) prefs.remove(keyFor(userPubkey)) else prefs[keyFor(userPubkey)] = ids
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-5
@@ -128,13 +128,33 @@ class ChessLobbyLogic(
|
||||
private val metadataProvider: IUserMetadataProvider,
|
||||
private val scope: CoroutineScope,
|
||||
pollingConfig: ChessPollingConfig = ChessPollingDefaults.android,
|
||||
private val dismissedStorage: ChessDismissedGamesStorage? = null,
|
||||
private val dismissedStorage: ChessDismissedGamesStore? = null,
|
||||
) {
|
||||
val state = ChessLobbyState(userPubkey, scope)
|
||||
|
||||
private val dismissedGameIdsLock = KmpLock()
|
||||
private val dismissedGameIds: MutableSet<String> =
|
||||
(dismissedStorage?.load(userPubkey)?.toMutableSet() ?: mutableSetOf())
|
||||
|
||||
/**
|
||||
* Seeded asynchronously: the store is DataStore-backed and reads suspend,
|
||||
* so this starts empty and fills shortly after construction. Until it does,
|
||||
* a previously dismissed game can appear in the completed list for a frame
|
||||
* or two.
|
||||
*
|
||||
* The seed unions rather than replaces, so a dismissal the user makes
|
||||
* before the read lands is not overwritten by it.
|
||||
*/
|
||||
private val dismissedGameIds: MutableSet<String> = mutableSetOf()
|
||||
|
||||
init {
|
||||
dismissedStorage?.let { storage ->
|
||||
scope.launch {
|
||||
val stored = storage.load(userPubkey)
|
||||
if (stored.isNotEmpty()) {
|
||||
dismissedGameIdsLock.withLock { dismissedGameIds.addAll(stored) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track when games were last loaded to prevent duplicate fetches
|
||||
// (e.g., discoverUserGames loads a game, then polling immediately re-fetches it).
|
||||
@@ -969,7 +989,7 @@ class ChessLobbyLogic(
|
||||
dismissedGameIds.add(gameId)
|
||||
dismissedGameIds.toSet()
|
||||
}
|
||||
dismissedStorage?.save(userPubkey, snapshot)
|
||||
dismissedStorage?.let { storage -> scope.launch { storage.save(userPubkey, snapshot) } }
|
||||
}
|
||||
|
||||
fun dismissAllCompletedGames() {
|
||||
@@ -980,7 +1000,7 @@ class ChessLobbyLogic(
|
||||
dismissedGameIds.addAll(allIds)
|
||||
dismissedGameIds.toSet()
|
||||
}
|
||||
dismissedStorage?.save(userPubkey, snapshot)
|
||||
dismissedStorage?.let { storage -> scope.launch { storage.save(userPubkey, snapshot) } }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-15
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.preferences
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.util.appDataDir
|
||||
import com.vitorpamplona.amethyst.commons.util.restrictToOwner
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import java.io.File
|
||||
@@ -56,21 +57,7 @@ actual class SecretEncryption internal constructor(
|
||||
private const val GCM_TAG_LENGTH_BITS = 128
|
||||
private const val KEY_FILE_NAME = "secret.key"
|
||||
|
||||
/** Where this OS keeps per-user application data. */
|
||||
internal fun defaultKeyFile(): File {
|
||||
val home = System.getProperty("user.home") ?: "."
|
||||
val os = System.getProperty("os.name").orEmpty().lowercase()
|
||||
val dir =
|
||||
when {
|
||||
os.contains("mac") || os.contains("darwin") ->
|
||||
File(home, "Library/Application Support/Amethyst")
|
||||
os.contains("win") ->
|
||||
File(System.getenv("APPDATA") ?: "$home\\AppData\\Roaming", "Amethyst")
|
||||
else ->
|
||||
File(System.getenv("XDG_DATA_HOME")?.takeIf { it.isNotBlank() } ?: "$home/.local/share", "amethyst")
|
||||
}
|
||||
return File(dir, KEY_FILE_NAME)
|
||||
}
|
||||
internal fun defaultKeyFile(): File = File(appDataDir, KEY_FILE_NAME)
|
||||
}
|
||||
|
||||
// A Cipher holds the state of the operation in progress, so two coroutines
|
||||
|
||||
+16
-20
@@ -20,25 +20,21 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip64Chess
|
||||
|
||||
// Phase 2 compile-only iOS actual. In-memory only; persistence via
|
||||
// NSUserDefaults arrives with the iosApp module in Phase 3.
|
||||
actual class ChessDismissedGamesStorage private actual constructor() {
|
||||
private val dismissed = mutableMapOf<String, Set<String>>()
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import com.vitorpamplona.amethyst.commons.util.appDataDir
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import java.io.File
|
||||
|
||||
actual companion object {
|
||||
actual fun create(context: Any?): ChessDismissedGamesStorage = ChessDismissedGamesStorage()
|
||||
}
|
||||
|
||||
actual fun load(userPubkey: String): Set<String> = dismissed[userPubkey] ?: emptySet()
|
||||
|
||||
actual fun save(
|
||||
userPubkey: String,
|
||||
ids: Set<String>,
|
||||
) {
|
||||
if (ids.isEmpty()) {
|
||||
dismissed.remove(userPubkey)
|
||||
} else {
|
||||
dismissed[userPubkey] = ids
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The desktop dismissed-games store, in the shared app data directory.
|
||||
*
|
||||
* Built here rather than in desktopApp so a front end does not need DataStore
|
||||
* on its own classpath to get one. Replaces a `java.util.prefs` node without
|
||||
* carrying it over — the dismissed list is a convenience, and chess has few
|
||||
* enough users that a migration is not worth the code.
|
||||
*/
|
||||
fun desktopChessDismissedGamesStore(): ChessDismissedGamesStore {
|
||||
val file = File(appDataDir, "chess_dismissed_games.preferences_pb")
|
||||
file.parentFile?.mkdirs()
|
||||
return ChessDismissedGamesStore(PreferenceDataStoreFactory.createWithPath(produceFile = { file.toOkioPath() }))
|
||||
}
|
||||
+19
-27
@@ -18,34 +18,26 @@
|
||||
* 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.nip64Chess
|
||||
package com.vitorpamplona.amethyst.commons.util
|
||||
|
||||
import java.util.prefs.Preferences
|
||||
import java.io.File
|
||||
|
||||
actual class ChessDismissedGamesStorage private actual constructor() {
|
||||
private val prefs: Preferences = Preferences.userNodeForPackage(ChessDismissedGamesStorage::class.java)
|
||||
|
||||
actual companion object {
|
||||
private const val NODE_PREFIX = "chess_dismissed_"
|
||||
private const val DELIMITER = ","
|
||||
|
||||
actual fun create(context: Any?): ChessDismissedGamesStorage = ChessDismissedGamesStorage()
|
||||
}
|
||||
|
||||
actual fun load(userPubkey: String): Set<String> {
|
||||
val raw = prefs.get("$NODE_PREFIX$userPubkey", "")
|
||||
if (raw.isEmpty()) return emptySet()
|
||||
return raw.split(DELIMITER).toSet()
|
||||
}
|
||||
|
||||
actual fun save(
|
||||
userPubkey: String,
|
||||
ids: Set<String>,
|
||||
) {
|
||||
if (ids.isEmpty()) {
|
||||
prefs.remove("$NODE_PREFIX$userPubkey")
|
||||
} else {
|
||||
prefs.put("$NODE_PREFIX$userPubkey", ids.joinToString(DELIMITER))
|
||||
/**
|
||||
* Where this desktop OS keeps per-user application data.
|
||||
*
|
||||
* One definition, so the key file, the preference stores and anything else
|
||||
* persistent land together rather than each picking their own convention.
|
||||
*/
|
||||
val appDataDir: File
|
||||
get() {
|
||||
val home = System.getProperty("user.home") ?: "."
|
||||
val os = System.getProperty("os.name").orEmpty().lowercase()
|
||||
return when {
|
||||
os.contains("mac") || os.contains("darwin") ->
|
||||
File(home, "Library/Application Support/Amethyst")
|
||||
os.contains("win") ->
|
||||
File(System.getenv("APPDATA") ?: "$home\\AppData\\Roaming", "Amethyst")
|
||||
else ->
|
||||
File(System.getenv("XDG_DATA_HOME")?.takeIf { it.isNotBlank() } ?: "$home/.local/share", "amethyst")
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.nip64Chess
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
|
||||
class ChessDismissedGamesStoreTest {
|
||||
@get:Rule
|
||||
val folder = TemporaryFolder()
|
||||
|
||||
private var seq = 0
|
||||
|
||||
private fun raw(): DataStore<Preferences> {
|
||||
val file = File(folder.root, "chess_${seq++}.preferences_pb")
|
||||
return PreferenceDataStoreFactory.createWithPath(
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
produceFile = { file.toOkioPath() },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anUnknownUserHasDismissedNothing() =
|
||||
runTest {
|
||||
assertTrue(ChessDismissedGamesStore(raw()).load("npub1").isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun savedIdsReadBack() =
|
||||
runTest {
|
||||
val store = ChessDismissedGamesStore(raw())
|
||||
|
||||
store.save("npub1", setOf("game1", "game2"))
|
||||
|
||||
assertEquals(setOf("game1", "game2"), store.load("npub1"))
|
||||
}
|
||||
|
||||
/** Two accounts on one device must not see each other's dismissals. */
|
||||
@Test
|
||||
fun usersAreIsolated() =
|
||||
runTest {
|
||||
val store = ChessDismissedGamesStore(raw())
|
||||
|
||||
store.save("npub1", setOf("game1"))
|
||||
store.save("npub2", setOf("game2"))
|
||||
|
||||
assertEquals(setOf("game1"), store.load("npub1"))
|
||||
assertEquals(setOf("game2"), store.load("npub2"))
|
||||
}
|
||||
|
||||
/** An empty set removes the key rather than storing an empty one. */
|
||||
@Test
|
||||
fun savingAnEmptySetClearsTheEntry() =
|
||||
runTest {
|
||||
val raw = raw()
|
||||
val store = ChessDismissedGamesStore(raw)
|
||||
store.save("npub1", setOf("game1"))
|
||||
|
||||
store.save("npub1", emptySet())
|
||||
|
||||
assertTrue(store.load("npub1").isEmpty())
|
||||
assertFalse(raw.data.first().contains(ChessDismissedGamesStore.keyFor("npub1")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aLaterSaveReplacesTheSet() =
|
||||
runTest {
|
||||
val store = ChessDismissedGamesStore(raw())
|
||||
|
||||
store.save("npub1", setOf("a", "b"))
|
||||
store.save("npub1", setOf("c"))
|
||||
|
||||
assertEquals(setOf("c"), store.load("npub1"))
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.desktop.chess
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.UserMetadataCache
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStorage
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessLobbyLogic
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessSyncStatus
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.CompletedGame
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.PublicGame
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.desktopChessDismissedGamesStore
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -60,7 +60,7 @@ class DesktopChessViewModelNew(
|
||||
private val publisher = DesktopChessPublisher(account, relayManager)
|
||||
private val fetcher = DesktopRelayFetcher(relayManager, account.pubKeyHex)
|
||||
private val metadataProvider = DesktopMetadataProvider(userMetadataCache)
|
||||
private val dismissedStorage = ChessDismissedGamesStorage.create()
|
||||
private val dismissedStorage = desktopChessDismissedGamesStore()
|
||||
|
||||
// Shared business logic (creates its own ChessLobbyState internally)
|
||||
private val logic =
|
||||
|
||||
Reference in New Issue
Block a user