.
This commit is contained in:
@@ -10,16 +10,8 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import su.reya.coop.coop.storage.SecretStore
|
import su.reya.coop.coop.storage.SecretStore
|
||||||
import su.reya.coop.nostr.NostrManager
|
import su.reya.coop.nostr.NostrManager
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@@ -31,14 +23,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private val factory by lazy {
|
private val factory by lazy {
|
||||||
object : ViewModelProvider.Factory {
|
object : ViewModelProvider.Factory {
|
||||||
private val secretStore = SecretStore(this@MainActivity)
|
|
||||||
private val androidSigner =
|
private val androidSigner =
|
||||||
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||||
private val nostrVM = NostrViewModel(NostrManager.instance, secretStore, androidSigner)
|
private val secretStore = SecretStore(this@MainActivity)
|
||||||
|
private val nostrViewModel =
|
||||||
|
NostrViewModel(NostrManager.instance, secretStore, androidSigner)
|
||||||
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return when {
|
return when {
|
||||||
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrVM
|
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
||||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
@@ -87,9 +80,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
nostrViewModel.signerRequired.value == null
|
nostrViewModel.signerRequired.value == null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind the lifecycle of the ViewModels
|
|
||||||
nostrViewModel.bindLifecycle(ProcessLifecycleOwner.get().lifecycle)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
App(
|
App(
|
||||||
nostrViewModel = nostrViewModel,
|
nostrViewModel = nostrViewModel,
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package su.reya.coop
|
package su.reya.coop
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -18,13 +17,17 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -52,25 +55,52 @@ import kotlin.time.Clock
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
data class NostrAppState(
|
||||||
|
val isBusy: Boolean = false,
|
||||||
|
val isRelayListEmpty: Boolean = false,
|
||||||
|
val isSyncing: Boolean = false,
|
||||||
|
val isPartialProcessedGiftWrap: Boolean = false,
|
||||||
|
val isNotificationBannerDismissed: Boolean = false,
|
||||||
|
val signerRequired: Boolean? = null,
|
||||||
|
)
|
||||||
|
|
||||||
class NostrViewModel(
|
class NostrViewModel(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
private val secretStore: SecretStorage,
|
private val secretStore: SecretStorage,
|
||||||
private val externalSignerHandler: ExternalSignerHandler? = null,
|
private val externalSignerHandler: ExternalSignerHandler? = null,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _isNotificationBannerDismissed = MutableStateFlow(false)
|
companion object {
|
||||||
val isNotificationBannerDismissed = _isNotificationBannerDismissed.asStateFlow()
|
private const val KEY_USER_SIGNER = "user_signer"
|
||||||
|
private const val KEY_APP_KEYS = "app_keys"
|
||||||
|
private const val KEY_BANNER_DISMISSED = "notification_banner_dismissed"
|
||||||
|
}
|
||||||
|
|
||||||
private val _signerRequired = MutableStateFlow<Boolean?>(null)
|
private val backgroundWorkTrigger = flow {
|
||||||
val signerRequired = _signerRequired.asStateFlow()
|
coroutineScope {
|
||||||
|
val observerJob = launch { runObserver() }
|
||||||
|
val batchingJob = launch { runMetadataBatching() }
|
||||||
|
try {
|
||||||
|
emit(Unit)
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
observerJob.cancel()
|
||||||
|
batchingJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val _isBusy = MutableStateFlow(false)
|
private val _appState = MutableStateFlow(NostrAppState())
|
||||||
val isBusy = _isBusy.asStateFlow()
|
val appState: StateFlow<NostrAppState> = combine(
|
||||||
|
_appState,
|
||||||
private val _isPartialProcessedGiftWrap = MutableStateFlow(false)
|
nostr.messages.messageSyncState.map { it.isSyncing },
|
||||||
val isPartialProcessedGiftWrap = _isPartialProcessedGiftWrap.asStateFlow()
|
backgroundWorkTrigger
|
||||||
|
) { state, isSyncing, _ ->
|
||||||
private val _isRelayListEmpty = MutableStateFlow(false)
|
state.copy(isSyncing = isSyncing)
|
||||||
val isRelayListEmpty = _isRelayListEmpty.asStateFlow()
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = NostrAppState()
|
||||||
|
)
|
||||||
|
|
||||||
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
|
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
|
||||||
val chatRooms = _chatRooms.asStateFlow()
|
val chatRooms = _chatRooms.asStateFlow()
|
||||||
@@ -87,16 +117,23 @@ class NostrViewModel(
|
|||||||
private val _errorEvents = Channel<String>(Channel.BUFFERED)
|
private val _errorEvents = Channel<String>(Channel.BUFFERED)
|
||||||
val errorEvents = _errorEvents.receiveAsFlow()
|
val errorEvents = _errorEvents.receiveAsFlow()
|
||||||
|
|
||||||
|
private val profilesMutex = Mutex()
|
||||||
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
||||||
|
|
||||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||||
|
|
||||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
||||||
|
|
||||||
val isSyncing = nostr.messages.messageSyncState
|
val isNotificationBannerDismissed = appState.map { it.isNotificationBannerDismissed }
|
||||||
.map { it.isSyncing }
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
val signerRequired = appState.map { it.signerRequired }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
val isBusy = appState.map { it.isBusy }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
val isPartialProcessedGiftWrap = appState.map { it.isPartialProcessedGiftWrap }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
val isRelayListEmpty = appState.map { it.isRelayListEmpty }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
val isSyncing = appState.map { it.isSyncing }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val currentUserProfile = nostr.signer.publicKeyFlow
|
val currentUserProfile = nostr.signer.publicKeyFlow
|
||||||
@@ -122,17 +159,6 @@ class NostrViewModel(
|
|||||||
getCacheMetadata()
|
getCacheMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bindLifecycle(lifecycle: Lifecycle) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
coroutineScope {
|
|
||||||
launch { runObserver() }
|
|
||||||
launch { runMetadataBatching() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|
||||||
@@ -152,8 +178,8 @@ class NostrViewModel(
|
|||||||
|
|
||||||
private fun checkNotificationBannerDismissedStatus() {
|
private fun checkNotificationBannerDismissedStatus() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isNotificationBannerDismissed.value =
|
val dismissed = secretStore.get(KEY_BANNER_DISMISSED) == "true"
|
||||||
secretStore.get("notification_banner_dismissed") == "true"
|
_appState.update { it.copy(isNotificationBannerDismissed = dismissed) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +196,7 @@ class NostrViewModel(
|
|||||||
nostr.messages.messageSyncState.collect { state ->
|
nostr.messages.messageSyncState.collect { state ->
|
||||||
// When at least some messages are processed, allow UI to show the list
|
// When at least some messages are processed, allow UI to show the list
|
||||||
if (state.processedCount > 0) {
|
if (state.processedCount > 0) {
|
||||||
_isPartialProcessedGiftWrap.value = true
|
_appState.update { it.copy(isPartialProcessedGiftWrap = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh UI every 10 messages OR when sync is fully done
|
// Refresh UI every 10 messages OR when sync is fully done
|
||||||
@@ -270,11 +296,11 @@ class NostrViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val secret = withTimeoutOrNull(3.seconds) {
|
val secret = withTimeoutOrNull(3.seconds) {
|
||||||
secretStore.get("user_signer")
|
secretStore.get(KEY_USER_SIGNER)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secret == null) {
|
if (secret == null) {
|
||||||
_signerRequired.value = true
|
_appState.update { it.copy(signerRequired = true) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,14 +308,14 @@ class NostrViewModel(
|
|||||||
val signer = createSigner(secret)
|
val signer = createSigner(secret)
|
||||||
nostr.setSigner(signer)
|
nostr.setSigner(signer)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
_signerRequired.value = false
|
_appState.update { it.copy(signerRequired = false) }
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
showError("Login failed: ${e.message}")
|
showError("Login failed: ${e.message}")
|
||||||
_signerRequired.value = true
|
_appState.update { it.copy(signerRequired = true) }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Login failed: ${e.message}")
|
showError("Login failed: ${e.message}")
|
||||||
_signerRequired.value = true
|
_appState.update { it.copy(signerRequired = true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +330,7 @@ class NostrViewModel(
|
|||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
||||||
if (rooms.isNotEmpty()) {
|
if (rooms.isNotEmpty()) {
|
||||||
mergeChatRooms(rooms)
|
mergeChatRooms(rooms)
|
||||||
_isPartialProcessedGiftWrap.value = true
|
_appState.update { it.copy(isPartialProcessedGiftWrap = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all metadata for the current user
|
// Get all metadata for the current user
|
||||||
@@ -315,7 +341,7 @@ class NostrViewModel(
|
|||||||
|
|
||||||
// Check if the relay list is empty
|
// Check if the relay list is empty
|
||||||
val relays = nostr.relays.getMsgRelays(currentUser)
|
val relays = nostr.relays.getMsgRelays(currentUser)
|
||||||
if (relays.isEmpty()) _isRelayListEmpty.value = true
|
if (relays.isEmpty()) _appState.update { it.copy(isRelayListEmpty = true) }
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -334,7 +360,11 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
private fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
||||||
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
viewModelScope.launch {
|
||||||
|
profilesMutex.withLock {
|
||||||
|
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
||||||
@@ -347,7 +377,7 @@ class NostrViewModel(
|
|||||||
fun logout() {
|
fun logout() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
_isBusy.value = true
|
_appState.update { it.copy(isBusy = true) }
|
||||||
// Reset the nostr signer and prune the database
|
// Reset the nostr signer and prune the database
|
||||||
nostr.signer.switch(Keys.generate())
|
nostr.signer.switch(Keys.generate())
|
||||||
nostr.prune()
|
nostr.prune()
|
||||||
@@ -355,14 +385,13 @@ class NostrViewModel(
|
|||||||
showError("Logout encountered an error: ${e.message}")
|
showError("Logout encountered an error: ${e.message}")
|
||||||
} finally {
|
} finally {
|
||||||
// Clear credentials from persistent storage
|
// Clear credentials from persistent storage
|
||||||
secretStore.clear("user_signer")
|
secretStore.clear(KEY_USER_SIGNER)
|
||||||
secretStore.clear("notification_banner_dismissed")
|
secretStore.clear(KEY_BANNER_DISMISSED)
|
||||||
|
|
||||||
// Reset all UI states
|
// Reset all UI states
|
||||||
resetInternalState()
|
resetInternalState()
|
||||||
|
|
||||||
_isBusy.value = false
|
_appState.update { it.copy(isBusy = false, signerRequired = true) }
|
||||||
_signerRequired.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,24 +399,28 @@ class NostrViewModel(
|
|||||||
private fun resetInternalState() {
|
private fun resetInternalState() {
|
||||||
_chatRooms.value = emptySet()
|
_chatRooms.value = emptySet()
|
||||||
_contactList.value = emptySet()
|
_contactList.value = emptySet()
|
||||||
_isPartialProcessedGiftWrap.value = false
|
_appState.update {
|
||||||
_isRelayListEmpty.value = false
|
it.copy(
|
||||||
_isNotificationBannerDismissed.value = false
|
isPartialProcessedGiftWrap = false,
|
||||||
|
isRelayListEmpty = false,
|
||||||
|
isNotificationBannerDismissed = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissNotificationBanner() {
|
fun dismissNotificationBanner() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
secretStore.set("notification_banner_dismissed", "true")
|
secretStore.set(KEY_BANNER_DISMISSED, "true")
|
||||||
_isNotificationBannerDismissed.value = true
|
_appState.update { it.copy(isNotificationBannerDismissed = true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissRelayWarning() {
|
fun dismissRelayWarning() {
|
||||||
_isRelayListEmpty.value = false
|
_appState.update { it.copy(isRelayListEmpty = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getOrInitAppKeys(): Keys {
|
private suspend fun getOrInitAppKeys(): Keys {
|
||||||
val secret = secretStore.get("app_keys")
|
val secret = secretStore.get(KEY_APP_KEYS)
|
||||||
|
|
||||||
// If app keys are already stored, use them
|
// If app keys are already stored, use them
|
||||||
if (secret != null) {
|
if (secret != null) {
|
||||||
@@ -396,7 +429,7 @@ class NostrViewModel(
|
|||||||
|
|
||||||
// Generate new app keys and save to the secret storage
|
// Generate new app keys and save to the secret storage
|
||||||
val keys = Keys.generate()
|
val keys = Keys.generate()
|
||||||
secretStore.set("app_keys", keys.secretKey().toBech32())
|
secretStore.set(KEY_APP_KEYS, keys.secretKey().toBech32())
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
@@ -436,7 +469,7 @@ class NostrViewModel(
|
|||||||
picture: ByteArray? = null,
|
picture: ByteArray? = null,
|
||||||
contentType: String? = null
|
contentType: String? = null
|
||||||
) {
|
) {
|
||||||
_isBusy.value = true
|
_appState.update { it.copy(isBusy = true) }
|
||||||
try {
|
try {
|
||||||
val avatarUrl = picture?.let { blossomUpload(it, contentType ?: "image/jpeg") }
|
val avatarUrl = picture?.let { blossomUpload(it, contentType ?: "image/jpeg") }
|
||||||
val newMetadata = nostr.profiles.updateProfile(name, bio, avatarUrl)
|
val newMetadata = nostr.profiles.updateProfile(name, bio, avatarUrl)
|
||||||
@@ -444,7 +477,7 @@ class NostrViewModel(
|
|||||||
// Update the metadata state after successfully published
|
// Update the metadata state after successfully published
|
||||||
updateMetadata(currentUser, Profile(currentUser, newMetadata))
|
updateMetadata(currentUser, Profile(currentUser, newMetadata))
|
||||||
// Update local state
|
// Update local state
|
||||||
_isBusy.value = false
|
_appState.update { it.copy(isBusy = false) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -456,7 +489,7 @@ class NostrViewModel(
|
|||||||
picture: ByteArray?,
|
picture: ByteArray?,
|
||||||
contentType: String? = null
|
contentType: String? = null
|
||||||
) {
|
) {
|
||||||
_isBusy.value = true
|
_appState.update { it.copy(isBusy = true) }
|
||||||
|
|
||||||
val keys = Keys.generate()
|
val keys = Keys.generate()
|
||||||
val secret = keys.secretKey().toBech32()
|
val secret = keys.secretKey().toBech32()
|
||||||
@@ -466,10 +499,9 @@ class NostrViewModel(
|
|||||||
// Create identity
|
// Create identity
|
||||||
nostr.profiles.createIdentity(keys = keys, name = name, bio, picture = avatarUrl)
|
nostr.profiles.createIdentity(keys = keys, name = name, bio, picture = avatarUrl)
|
||||||
// Persist the secret in the secret storage
|
// Persist the secret in the secret storage
|
||||||
secretStore.set("user_signer", secret)
|
secretStore.set(KEY_USER_SIGNER, secret)
|
||||||
// Update local states
|
// Update local states
|
||||||
_isBusy.value = false
|
_appState.update { it.copy(isBusy = false, signerRequired = false) }
|
||||||
_signerRequired.value = false
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -517,16 +549,15 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importIdentity(secret: String) {
|
suspend fun importIdentity(secret: String) {
|
||||||
_isBusy.value = true
|
_appState.update { it.copy(isBusy = true) }
|
||||||
try {
|
try {
|
||||||
val signer = createSigner(secret)
|
val signer = createSigner(secret)
|
||||||
// Update signer
|
// Update signer
|
||||||
nostr.setSigner(signer)
|
nostr.setSigner(signer)
|
||||||
// Persist the secret in the secret storage
|
// Persist the secret in the secret storage
|
||||||
secretStore.set("user_signer", secret)
|
secretStore.set(KEY_USER_SIGNER, secret)
|
||||||
// Update local states
|
// Update local states
|
||||||
_signerRequired.value = false
|
_appState.update { it.copy(signerRequired = false, isBusy = false) }
|
||||||
_isBusy.value = false
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -534,7 +565,7 @@ class NostrViewModel(
|
|||||||
|
|
||||||
suspend fun connectExternalSigner() {
|
suspend fun connectExternalSigner() {
|
||||||
val handler = externalSignerHandler ?: throw IllegalStateException("Signer not available")
|
val handler = externalSignerHandler ?: throw IllegalStateException("Signer not available")
|
||||||
_isBusy.value = true
|
_appState.update { it.copy(isBusy = true) }
|
||||||
try {
|
try {
|
||||||
val permissions = SignerPermissions.toJson(
|
val permissions = SignerPermissions.toJson(
|
||||||
listOf(
|
listOf(
|
||||||
@@ -557,10 +588,12 @@ class NostrViewModel(
|
|||||||
// Update signer
|
// Update signer
|
||||||
nostr.setSigner(signer)
|
nostr.setSigner(signer)
|
||||||
// Store the signer in the secret storage
|
// Store the signer in the secret storage
|
||||||
secretStore.set("user_signer", "nip55://${result.packageName}/${result.pubkey.toHex()}")
|
secretStore.set(
|
||||||
|
KEY_USER_SIGNER,
|
||||||
|
"nip55://${result.packageName}/${result.pubkey.toHex()}"
|
||||||
|
)
|
||||||
// Update local states
|
// Update local states
|
||||||
_signerRequired.value = false
|
_appState.update { it.copy(signerRequired = false, isBusy = false) }
|
||||||
_isBusy.value = false
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Notice: ${e.message}")
|
throw Exception("Notice: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user