refactor: JavaSerializable, shared notices in the NIP-46 signer, client-injected subscription stats

- JavaSerializable (commons): java.io.Serializable on JVM/Android, a marker on iOS, for the
  chat list's saveable lazy keys.
- Nip46SignerScreen drops its Toasts (and an unused Context parameter) for the shared notices.
- ActiveSubscriptionsViewModel polls the client the screen hands it instead of Amethyst.instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-29 12:56:36 +00:00
parent 4b6d9c7b6d
commit b752a42d01
7 changed files with 90 additions and 20 deletions
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.util.JavaSerializable
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
@@ -81,7 +82,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import java.io.Serializable
@Composable
fun ChatroomListFeedView(
@@ -302,7 +302,7 @@ private fun relayShortName(relay: NormalizedRelayUrl): String =
// stores LazyColumn item keys in a SaveableStateHolder, which on Android
// requires Bundle-storable types, so each variant is Serializable and only
// holds primitives.
private sealed interface ChatroomLazyKey : Serializable
private sealed interface ChatroomLazyKey : JavaSerializable
private data class MarmotChatroomLazyKey(
val groupId: HexKey,
@@ -127,7 +127,7 @@ fun ActiveSubscriptionsScreen(
nav: INav,
viewModel: ActiveSubscriptionsViewModel = viewModel(),
) {
LaunchedEffect(Unit) { viewModel.startPolling() }
LaunchedEffect(Unit) { viewModel.startPolling(accountViewModel.account.client) }
val state by viewModel.state.collectAsStateWithLifecycle()
Scaffold(
@@ -23,14 +23,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.subscriptions
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -143,18 +144,17 @@ class ActiveSubscriptionsViewModel : ViewModel() {
val state: StateFlow<ActiveSubscriptionsState> = _state.asStateFlow()
/** Polls while the screen is on. [REFRESH_MS] is slow enough to be free, fast enough to feel live. */
fun startPolling() {
fun startPolling(client: INostrClient) {
viewModelScope.launch(Dispatchers.Default) {
while (isActive) {
_state.value = snapshot()
kotlinx.coroutines.delay(REFRESH_MS)
_state.value = snapshot(client)
delay(REFRESH_MS)
}
}
}
private suspend fun snapshot(): ActiveSubscriptionsState =
private suspend fun snapshot(client: INostrClient): ActiveSubscriptionsState =
withContext(Dispatchers.Default) {
val client = Amethyst.instance.client
aggregateSubscriptions(
client.connectedRelaysFlow().value.associateWith { relay ->
client.activeRequests(relay).values.flatten()
@@ -20,8 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46
import android.content.Context
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -73,7 +71,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -119,6 +116,8 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_scan_connect
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_status_no_relays
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_title
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_turn_on
import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -142,7 +141,8 @@ fun Nip46SignerScreen(
val account = accountViewModel.account
val signer = account.nip46Signer
val scope = rememberCoroutineScope()
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val longNotice = rememberLongNotice()
val clipboard = LocalClipboard.current
val copiedStr = stringRes(Res.string.nip46_signer_copied)
val regeneratedStr = stringRes(Res.string.nip46_signer_regenerated)
@@ -175,7 +175,7 @@ fun Nip46SignerScreen(
fun onConnect(uri: String) {
scope.launch {
val result = signer.connectViaNostrConnect(uri.trim())
Toast.makeText(context, describe(context, result), Toast.LENGTH_LONG).show()
longNotice.show(describe(result))
refreshKey++
}
}
@@ -231,7 +231,7 @@ fun Nip46SignerScreen(
uri = uri,
onCopy = {
scope.launch { clipboard.setText(uri) }
Toast.makeText(context, copiedStr, Toast.LENGTH_SHORT).show()
shortNotice.show(copiedStr)
},
onRegenerate = { confirmRotate = true },
)
@@ -273,7 +273,7 @@ fun Nip46SignerScreen(
onConfirm = {
confirmRotate = false
signer.rotateAddress()
Toast.makeText(context, regeneratedStr, Toast.LENGTH_SHORT).show()
shortNotice.show(regeneratedStr)
},
onDismiss = { confirmRotate = false },
)
@@ -625,10 +625,7 @@ private fun ReadOnlyNotice() {
}
}
private suspend fun describe(
context: Context,
result: Nip46SignerState.ConnectResult,
): String =
private suspend fun describe(result: Nip46SignerState.ConnectResult): String =
when (result) {
is Nip46SignerState.ConnectResult.Connected ->
result.name?.let { loadStringRes(Res.string.nip46_signer_connected_named, it) }
@@ -0,0 +1,27 @@
/*
* 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.util
/**
* `java.io.Serializable` where there is a JVM, a marker elsewhere. For values Android must store in
* a Bundle (saveable state, lazy-list keys) that shared code declares.
*/
expect interface JavaSerializable
@@ -0,0 +1,23 @@
/*
* 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.util
actual interface JavaSerializable
@@ -0,0 +1,23 @@
/*
* 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.util
actual typealias JavaSerializable = java.io.Serializable