Merge pull request #385 from greenart7c3/claude/move-intents-to-utils-tv9Iw

Refactor intent management to use IntentUtils singleton
This commit is contained in:
greenart7c3
2026-04-10 10:50:31 -03:00
committed by GitHub
6 changed files with 63 additions and 69 deletions
@@ -28,6 +28,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.ui.AccountScreen
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
@@ -56,6 +57,7 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
Amber.instance.setMainActivity(this)
mainViewModel = MainViewModel(applicationContext)
intent?.let { mainViewModel.onNewIntent(it, callingPackage) }
setContent {
val isStartingApp = Amber.instance.isStartingAppState.collectAsStateWithLifecycle()
if (isStartingApp.value) {
@@ -188,7 +190,7 @@ class MainActivity : AppCompatActivity() {
}
override fun onDestroy() {
mainViewModel.clear()
IntentUtils.clear()
super.onDestroy()
}
@@ -8,7 +8,6 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.greenart7c3.nostrsigner.models.IntentData
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
@@ -17,36 +16,15 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@Stable
@SuppressLint("StaticFieldLeak")
class MainViewModel(val context: Context) : ViewModel() {
private val _intents = MutableStateFlow<ImmutableList<IntentData>>(persistentListOf())
val intents = _intents.asStateFlow()
var navController: NavHostController? = null
fun addAll(list: List<IntentData>) {
val existingIds = intents.value.mapTo(mutableSetOf()) { it.id }
val newList = list.filter { existingIds.add(it.id) }
_intents.value = (_intents.value + newList).toPersistentList()
}
fun removeAll(intents: List<IntentData>) {
_intents.value = (_intents.value - intents.toSet()).toPersistentList()
}
fun clear() {
_intents.value = persistentListOf()
}
fun getAccount(userFromIntent: String?): String? {
val currentAccount = LocalPreferences.currentAccount(context)
try {
@@ -66,7 +44,7 @@ class MainViewModel(val context: Context) : ViewModel() {
}
val pubKeys =
intents.value.mapNotNull {
IntentUtils.intents.value.mapNotNull {
it.event?.pubKey
}.filter { it.isNotBlank() } + BunkerRequestUtils.getBunkerRequests().mapNotNull {
when (val parsed = Nip19Parser.uriToRoute(it.currentAccount)?.entity) {
@@ -149,7 +127,7 @@ class MainViewModel(val context: Context) : ViewModel() {
account?.let { acc ->
val intentData = IntentUtils.getIntentData(context, intent, callingPackage, intent.getStringExtra("route"), acc)
if (intentData != null) {
addAll(listOf(intentData))
IntentUtils.addAll(listOf(intentData))
}
intent.getStringExtra("route")?.let { route ->
@@ -35,6 +35,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.ui.AccountScreen
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
@@ -60,6 +61,7 @@ class SignerActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
Amber.instance.setMainActivity(this)
mainViewModel = MainViewModel(applicationContext)
intent?.let { mainViewModel.onNewIntent(it, callingPackage) }
setContent {
val isStartingApp = Amber.instance.isStartingAppState.collectAsStateWithLifecycle()
NostrSignerTheme {
@@ -206,7 +208,7 @@ class SignerActivity : AppCompatActivity() {
}
override fun onDestroy() {
mainViewModel.clear()
IntentUtils.clear()
super.onDestroy()
}
@@ -16,9 +16,7 @@ import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.FailedMigrationException
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.MainActivity
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.SignerActivity
import com.greenart7c3.nostrsigner.database.ApplicationEntity
import com.greenart7c3.nostrsigner.database.ApplicationPermissionsEntity
import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions
@@ -52,10 +50,35 @@ import java.net.URLDecoder
import java.util.Base64
import java.util.UUID
import java.util.zip.GZIPOutputStream
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
object IntentUtils {
private val _intents = MutableStateFlow<ImmutableList<IntentData>>(persistentListOf())
val intents = _intents.asStateFlow()
fun addAll(list: List<IntentData>) {
_intents.update { current ->
val existingIds = current.mapTo(mutableSetOf()) { it.id }
val newList = list.filter { existingIds.add(it.id) }
(current + newList).toPersistentList()
}
}
fun removeAll(list: List<IntentData>) {
_intents.update { current -> (current - list.toSet()).toPersistentList() }
}
fun clear() {
_intents.value = persistentListOf()
}
// Compiled once at class load time; avoids recompiling the pattern on every isUrlEncoded() call.
private val URL_ENCODED_REGEX = Regex("%[0-9a-fA-F]{2}")
@@ -561,16 +584,8 @@ object IntentUtils {
}
val id = intent.extras?.getString("id") ?: ""
val mainViewModel = when (val mainActivity = Amber.instance.getMainActivity()) {
is MainActivity -> mainActivity.mainViewModel
is SignerActivity -> mainActivity.mainViewModel
else -> null
}
mainViewModel?.let {
if (it.intents.value.any { intent -> intent.id == id }) {
return null
}
if (intents.value.any { it.id == id }) {
return null
}
var localAccount = currentLoggedInAccount
@@ -71,7 +71,7 @@ fun AccountScreen(
MainLoginPage(accountStateViewModel, newNavController)
}
is AccountState.LoggedIn -> {
val intents by mainViewModel.intents.collectAsState(initial = persistentListOf())
val intents by IntentUtils.intents.collectAsState(initial = persistentListOf())
LaunchedEffect(intent) {
intent.intent?.let {
IntentUtils.getIntentData(
@@ -81,7 +81,7 @@ fun AccountScreen(
it.getStringExtra("route"),
state.account,
)?.let { intentData ->
mainViewModel.addAll(listOf(intentData))
IntentUtils.addAll(listOf(intentData))
}
}
}
@@ -107,11 +107,11 @@ fun AccountScreen(
onRemoveIntentData = { results, type ->
when (type) {
IntentResultType.ADD -> {
mainViewModel.addAll(results)
IntentUtils.addAll(results)
}
IntentResultType.REMOVE -> {
mainViewModel.removeAll(results)
IntentUtils.removeAll(results)
}
}
},
@@ -1,22 +1,19 @@
package com.greenart7c3.nostrsigner
import android.content.Context
import com.greenart7c3.nostrsigner.models.CompressionType
import com.greenart7c3.nostrsigner.models.IntentData
import com.greenart7c3.nostrsigner.models.ReturnType
import com.greenart7c3.nostrsigner.models.SignerType
import io.mockk.mockk
import com.greenart7c3.nostrsigner.service.IntentUtils
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class MainViewModelTest {
private lateinit var viewModel: MainViewModel
@Before
fun setUp() {
viewModel = MainViewModel(mockk<Context>(relaxed = true))
@After
fun tearDown() {
IntentUtils.clear()
}
private fun intentData(id: String, data: String = "data") = IntentData(
@@ -37,48 +34,48 @@ class MainViewModelTest {
@Test
fun `addAll adds new intents`() {
viewModel.addAll(listOf(intentData("id1"), intentData("id2")))
assertEquals(2, viewModel.intents.value.size)
IntentUtils.addAll(listOf(intentData("id1"), intentData("id2")))
assertEquals(2, IntentUtils.intents.value.size)
}
@Test
fun `addAll ignores intents with duplicate id already in state`() {
viewModel.addAll(listOf(intentData("id1")))
viewModel.addAll(listOf(intentData("id1")))
assertEquals(1, viewModel.intents.value.size)
IntentUtils.addAll(listOf(intentData("id1")))
IntentUtils.addAll(listOf(intentData("id1")))
assertEquals(1, IntentUtils.intents.value.size)
}
@Test
fun `addAll ignores intents with duplicate id even when other fields differ`() {
viewModel.addAll(listOf(intentData("id1", data = "original")))
viewModel.addAll(listOf(intentData("id1", data = "different")))
assertEquals(1, viewModel.intents.value.size)
assertEquals("original", viewModel.intents.value.first().data)
IntentUtils.addAll(listOf(intentData("id1", data = "original")))
IntentUtils.addAll(listOf(intentData("id1", data = "different")))
assertEquals(1, IntentUtils.intents.value.size)
assertEquals("original", IntentUtils.intents.value.first().data)
}
@Test
fun `addAll deduplicates within a single batch`() {
viewModel.addAll(listOf(intentData("id1"), intentData("id1")))
assertEquals(1, viewModel.intents.value.size)
IntentUtils.addAll(listOf(intentData("id1"), intentData("id1")))
assertEquals(1, IntentUtils.intents.value.size)
}
@Test
fun `addAll allows intents with different ids`() {
viewModel.addAll(listOf(intentData("id1"), intentData("id2"), intentData("id3")))
assertEquals(3, viewModel.intents.value.size)
IntentUtils.addAll(listOf(intentData("id1"), intentData("id2"), intentData("id3")))
assertEquals(3, IntentUtils.intents.value.size)
}
@Test
fun `addAll appends to existing intents`() {
viewModel.addAll(listOf(intentData("id1")))
viewModel.addAll(listOf(intentData("id2")))
assertEquals(2, viewModel.intents.value.size)
IntentUtils.addAll(listOf(intentData("id1")))
IntentUtils.addAll(listOf(intentData("id2")))
assertEquals(2, IntentUtils.intents.value.size)
}
@Test
fun `addAll with empty list leaves state unchanged`() {
viewModel.addAll(listOf(intentData("id1")))
viewModel.addAll(emptyList())
assertEquals(1, viewModel.intents.value.size)
IntentUtils.addAll(listOf(intentData("id1")))
IntentUtils.addAll(emptyList())
assertEquals(1, IntentUtils.intents.value.size)
}
}