diff --git a/.gitignore b/.gitignore index adfa9bf..5edbcd0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ captures !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings node_modules/ +.artifacts/ \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ContactListScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ContactListScreen.kt index 175285a..d0eda89 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ContactListScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ContactListScreen.kt @@ -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(null) } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt index 1e97291..400c708 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -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) } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt index 70052d3..9aa39a9 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt @@ -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) }, diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt index 4ded00f..71f535f 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewChatScreen.kt @@ -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) } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt index 0726066..d8aeba0 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt @@ -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() diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RelayScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RelayScreen.kt index 036008a..5048959 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RelayScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RelayScreen.kt @@ -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()) { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/repository/AccountRepository.kt b/shared/src/commonMain/kotlin/su/reya/coop/repository/AccountRepository.kt index f92956f..c0f5cb9 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/repository/AccountRepository.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/repository/AccountRepository.kt @@ -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 = emptySet(), + val userRelayList: Map = emptyMap(), + val userMsgRelayList: List = emptyList(), ) class AccountRepository( @@ -77,18 +80,6 @@ class AccountRepository( } .stateIn(scope, SharingStarted.WhileSubscribed(5000), null) - private val _contactList = MutableStateFlow>(emptySet()) - val contactList: StateFlow> = _contactList.asStateFlow() - - private val _isRelayListEmpty = MutableStateFlow(false) - val isRelayListEmpty: StateFlow = _isRelayListEmpty.asStateFlow() - - private val _userRelayList = MutableStateFlow>(emptyMap()) - val userRelayList: StateFlow> = _userRelayList.asStateFlow() - - private val _userMsgRelayList = MutableStateFlow>(emptyList()) - val userMsgRelayList: StateFlow> = _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}") } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AccountViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AccountViewModel.kt index 8030dab..5a99450 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AccountViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AccountViewModel.kt @@ -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 = repository.state val isUpdatingProfile: StateFlow = repository.isUpdatingProfile val currentUserProfile: StateFlow = repository.currentUserProfile - val contactList: StateFlow> = repository.contactList - val isRelayListEmpty: StateFlow = repository.isRelayListEmpty - val userRelayList: StateFlow> = repository.userRelayList - val userMsgRelayList: StateFlow> = repository.userMsgRelayList fun logout(onLogout: () -> Unit = {}) = repository.logout(onLogout) fun dismissNotificationBanner() = repository.dismissNotificationBanner()