mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
feat: persist the geohash nickname so it survives app restarts
The location-chat nickname was in-memory only (ChannelNewMessageViewModel .geohashNickname), so it reset to empty on process death. It can't be recovered from relays either: the anonymous per-cell key publishes no kind-0 profile, and kind-20000 messages are ephemeral, so the nickname (a per-message `n` tag) has no durable home on the network. Persist it on-device instead, as a single global handle: - GeohashChatIdentityState gains nickname()/setNickname(), stored in the account's encrypted storage next to the per-cell device seed (survives restarts, switches with the account). - The chat screen restores it into the composer on room open, and the nickname dialog writes it back on Save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172JoMccseEKenyWan6txWV
This commit is contained in:
@@ -58,6 +58,34 @@ class GeohashChatIdentityState(
|
||||
|
||||
@Volatile private var cachedDeviceSeed: ByteArray? = null
|
||||
|
||||
@Volatile private var cachedNickname: String? = null
|
||||
|
||||
/**
|
||||
* The user's display handle for location chats: a single global nickname, persisted per account.
|
||||
* Bitchat carries this as the per-message `["n", …]` tag rather than a kind-0 profile, and kind-20000
|
||||
* messages are ephemeral (relays needn't store them), so the only durable home for it is the device.
|
||||
* Kept in this account's encrypted storage, so it survives restarts and switches with the account.
|
||||
* Empty string means "no nickname set". Reads touch disk on first call — invoke off the main thread.
|
||||
*/
|
||||
fun nickname(): String {
|
||||
cachedNickname?.let { return it }
|
||||
synchronized(lock) {
|
||||
cachedNickname?.let { return it }
|
||||
val value = Amethyst.instance.encryptedStorage(signer.pubKey).getString(PREF_NICKNAME, "") ?: ""
|
||||
cachedNickname = value
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/** Persists the global location-chat nickname (trimmed) for this account. */
|
||||
fun setNickname(value: String) {
|
||||
val trimmed = value.trim()
|
||||
synchronized(lock) {
|
||||
cachedNickname = trimmed
|
||||
Amethyst.instance.encryptedStorage(signer.pubKey).edit { putString(PREF_NICKNAME, trimmed) }
|
||||
}
|
||||
}
|
||||
|
||||
/** The Nostr key pair to use inside [geohash]. Derivation is cheap but cached; call off the main thread. */
|
||||
fun keyPair(geohash: String): KeyPair =
|
||||
synchronized(lock) {
|
||||
@@ -90,5 +118,6 @@ class GeohashChatIdentityState(
|
||||
|
||||
companion object {
|
||||
private const val PREF_KEY = "geohash_chat_device_seed"
|
||||
private const val PREF_NICKNAME = "geohash_chat_nickname"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -87,6 +87,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.E
|
||||
import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon
|
||||
|
||||
/**
|
||||
@@ -143,6 +145,13 @@ private fun GeohashChatRoom(
|
||||
newMessageModel.init(accountViewModel)
|
||||
newMessageModel.load(channel)
|
||||
|
||||
// The nickname lives on-device (the throwaway key has no kind-0 profile, and kind-20000 messages
|
||||
// are ephemeral), so restore the account's saved global handle into the composer on open.
|
||||
LaunchedEffect(newMessageModel) {
|
||||
val saved = withContext(Dispatchers.IO) { accountViewModel.account.geohashIdentity.nickname() }
|
||||
if (saved.isNotBlank()) newMessageModel.geohashNickname = saved
|
||||
}
|
||||
|
||||
// Teleport is a fact, not a per-message choice: the app decides whether the sender is physically
|
||||
// in this cell. When the device's location is known we compare it to the channel cell (objective);
|
||||
// otherwise we fall back to how the user arrived — the map picker passes teleported=true, "near me"
|
||||
@@ -277,7 +286,7 @@ private fun GeohashIdentityAvatar(
|
||||
) {
|
||||
var showNickname by remember { mutableStateOf(false) }
|
||||
if (showNickname) {
|
||||
GeohashNicknameDialog(model) { showNickname = false }
|
||||
GeohashNicknameDialog(model, accountViewModel) { showNickname = false }
|
||||
}
|
||||
|
||||
val postAsSelf = model.geohashPostAsSelf
|
||||
@@ -342,6 +351,7 @@ private fun GeohashIdentityAvatar(
|
||||
@Composable
|
||||
private fun GeohashNicknameDialog(
|
||||
model: ChannelNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var text by remember { mutableStateOf(model.geohashNickname) }
|
||||
@@ -349,7 +359,10 @@ private fun GeohashNicknameDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
model.geohashNickname = text.trim()
|
||||
val trimmed = text.trim()
|
||||
model.geohashNickname = trimmed
|
||||
// Persist the global handle so it survives app restarts (see GeohashChatIdentityState).
|
||||
accountViewModel.account.geohashIdentity.setNickname(trimmed)
|
||||
onDismiss()
|
||||
}) { Text("Save") }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user