This commit is contained in:
2026-07-18 16:11:34 +07:00
parent 9defad522c
commit 255c840847
9 changed files with 36 additions and 60 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
.artifacts/

View File

@@ -73,7 +73,8 @@ fun ContactListScreen(
) {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val contactList = accountState.contactList
var openAddContactDialog by remember { mutableStateOf(false) }
var contactToDelete by remember { mutableStateOf<PublicKey?>(null) }

View File

@@ -126,13 +126,13 @@ fun HomeScreen(
val userProfile by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val isRelayListEmpty by accountViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val isRelayListEmpty = accountState.isRelayListEmpty
val isBannerDismissed = accountState.isNotificationBannerDismissed
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val isBannerDismissed = accountState.isNotificationBannerDismissed
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
var showBottomSheet by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) }

View File

@@ -101,13 +101,6 @@ fun ImportScreen(viewModel: AccountViewModel) {
}
}
// Show import errors via snackbar
LaunchedEffect(accountState.importError) {
accountState.importError?.let {
snackbarHostState.showSnackbar(it)
}
}
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
snackbarHost = { SnackbarHost(snackbarHostState) },

View File

@@ -75,7 +75,8 @@ fun NewChatScreen(
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val contactList = accountState.contactList
var query by remember { mutableStateOf("") }
val createGroup = remember { mutableStateOf(false) }

View File

@@ -70,13 +70,6 @@ fun OnboardingScreen(viewModel: AccountViewModel) {
}
}
// Show connection errors
LaunchedEffect(accountState.importError) {
accountState.importError?.let {
snackbarHostState.showSnackbar(it)
}
}
val logoPainter = painterResource(Res.drawable.coop)
val expressiveFont = getExpressiveFontFamily()

View File

@@ -100,8 +100,9 @@ fun RelayScreen(viewModel: AccountViewModel) {
viewModel.loadCurrentUserMsgRelayList()
}
val loadedRelayList by viewModel.userRelayList.collectAsStateWithLifecycle()
val loadedMsgRelayList by viewModel.userMsgRelayList.collectAsStateWithLifecycle()
val accountState by viewModel.state.collectAsStateWithLifecycle()
val loadedRelayList = accountState.userRelayList
val loadedMsgRelayList = accountState.userMsgRelayList
LaunchedEffect(loadedRelayList) {
if (loadedRelayList.isNotEmpty()) {

View File

@@ -47,7 +47,10 @@ data class AccountState(
val signerRequired: Boolean? = null,
val isNotificationBannerDismissed: Boolean = false,
val isImporting: Boolean = false,
val importError: String? = null,
val isRelayListEmpty: Boolean = false,
val contactList: Set<PublicKey> = emptySet(),
val userRelayList: Map<RelayUrl, RelayMetadata?> = emptyMap(),
val userMsgRelayList: List<RelayUrl> = emptyList(),
)
class AccountRepository(
@@ -77,18 +80,6 @@ class AccountRepository(
}
.stateIn(scope, SharingStarted.WhileSubscribed(5000), null)
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
val contactList: StateFlow<Set<PublicKey>> = _contactList.asStateFlow()
private val _isRelayListEmpty = MutableStateFlow(false)
val isRelayListEmpty: StateFlow<Boolean> = _isRelayListEmpty.asStateFlow()
private val _userRelayList = MutableStateFlow<Map<RelayUrl, RelayMetadata?>>(emptyMap())
val userRelayList: StateFlow<Map<RelayUrl, RelayMetadata?>> = _userRelayList.asStateFlow()
private val _userMsgRelayList = MutableStateFlow<List<RelayUrl>>(emptyList())
val userMsgRelayList: StateFlow<List<RelayUrl>> = _userMsgRelayList.asStateFlow()
init {
checkNotificationBannerDismissedStatus()
login()
@@ -116,7 +107,7 @@ class AccountRepository(
} ?: emptyList()
// Automatically update the warning state
_isRelayListEmpty.value = relays.isEmpty()
_state.update { it.copy(isRelayListEmpty = relays.isEmpty()) }
}
}
}
@@ -229,7 +220,7 @@ class AccountRepository(
fun importIdentity(secret: String, password: String? = null) {
scope.launch {
_state.update { it.copy(isImporting = true, importError = null) }
_state.update { it.copy(isImporting = true) }
try {
val (signer, decryptedSecret) = createSigner(secret, password)
@@ -239,14 +230,14 @@ class AccountRepository(
_state.update { it.copy(signerRequired = false, isImporting = false) }
} catch (e: Exception) {
showError("Import failed: ${e.message}")
_state.update { it.copy(isImporting = false, importError = e.message) }
_state.update { it.copy(isImporting = false) }
}
}
}
fun connectExternalSigner() {
scope.launch {
_state.update { it.copy(isImporting = true, importError = null) }
_state.update { it.copy(isImporting = true) }
try {
val handler =
externalSignerHandler ?: throw IllegalStateException("Signer not available")
@@ -276,7 +267,7 @@ class AccountRepository(
_state.update { it.copy(signerRequired = false, isImporting = false) }
} catch (e: Exception) {
showError("External signer connection failed: ${e.message}")
_state.update { it.copy(isImporting = false, importError = e.message) }
_state.update { it.copy(isImporting = false) }
}
}
}
@@ -288,7 +279,7 @@ class AccountRepository(
contentType: String? = null
) {
scope.launch {
_state.update { it.copy(isImporting = true, importError = null) }
_state.update { it.copy(isImporting = true) }
try {
val keys = Keys.generate()
val secret = keys.secretKey().toBech32()
@@ -307,7 +298,7 @@ class AccountRepository(
_state.update { it.copy(signerRequired = false, isImporting = false) }
} catch (e: Exception) {
showError("Identity creation failed: ${e.message}")
_state.update { it.copy(isImporting = false, importError = e.message) }
_state.update { it.copy(isImporting = false) }
}
}
}
@@ -355,14 +346,13 @@ class AccountRepository(
scope.launch {
nostr.waitUntilInitialized()
nostr.profiles.contactListUpdates.collect { contacts ->
_contactList.value = contacts.toSet()
_state.update { it.copy(contactList = contacts.toSet()) }
}
}
}
fun resetInternalState() {
_contactList.value = emptySet()
_isRelayListEmpty.value = false
_state.update { it.copy(contactList = emptySet(), isRelayListEmpty = false) }
}
fun addContact(address: String) {
@@ -378,12 +368,12 @@ class AccountRepository(
return@launch
}
if (pubkey in _contactList.value) return@launch
if (pubkey in _state.value.contactList) return@launch
try {
val updated = _contactList.value + pubkey
val updated = _state.value.contactList + pubkey
nostr.profiles.setContactList(updated.toList())
_contactList.update { it + pubkey }
_state.update { it.copy(contactList = it.contactList + pubkey) }
} catch (e: Exception) {
showError("Error: ${e.message}")
}
@@ -392,12 +382,12 @@ class AccountRepository(
fun removeContact(publicKey: PublicKey) {
scope.launch {
if (publicKey !in _contactList.value) return@launch
if (publicKey !in _state.value.contactList) return@launch
try {
val updated = _contactList.value - publicKey
val updated = _state.value.contactList - publicKey
nostr.profiles.setContactList(updated.toList())
_contactList.update { it - publicKey }
_state.update { it.copy(contactList = it.contactList - publicKey) }
} catch (e: Exception) {
showError("Error: ${e.message}")
}
@@ -460,7 +450,7 @@ class AccountRepository(
}
fun dismissRelayWarning() {
_isRelayListEmpty.value = false
_state.update { it.copy(isRelayListEmpty = false) }
}
fun refetchMsgRelays() {
@@ -487,7 +477,8 @@ class AccountRepository(
scope.launch {
try {
val user = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
_userRelayList.value = nostr.relays.getRelayList(user)
val relayList = nostr.relays.getRelayList(user)
_state.update { it.copy(userRelayList = relayList) }
} catch (e: Exception) {
showError("Error: ${e.message}")
}
@@ -550,7 +541,8 @@ class AccountRepository(
scope.launch {
try {
val user = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
_userMsgRelayList.value = nostr.relays.getMsgRelays(user)
val msgRelays = nostr.relays.getMsgRelays(user)
_state.update { it.copy(userMsgRelayList = msgRelays) }
} catch (e: Exception) {
showError("Error: ${e.message}")
}

View File

@@ -3,8 +3,6 @@ package su.reya.coop.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.StateFlow
import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.RelayMetadata
import rust.nostr.sdk.RelayUrl
import rust.nostr.sdk.Timestamp
import su.reya.coop.Profile
import su.reya.coop.repository.AccountRepository
@@ -16,10 +14,6 @@ class AccountViewModel(
val state: StateFlow<AccountState> = repository.state
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
val contactList: StateFlow<Set<PublicKey>> = repository.contactList
val isRelayListEmpty: StateFlow<Boolean> = repository.isRelayListEmpty
val userRelayList: StateFlow<Map<RelayUrl, RelayMetadata?>> = repository.userRelayList
val userMsgRelayList: StateFlow<List<RelayUrl>> = repository.userMsgRelayList
fun logout(onLogout: () -> Unit = {}) = repository.logout(onLogout)
fun dismissNotificationBanner() = repository.dismissNotificationBanner()