diff --git a/app/lint.xml b/app/lint.xml index f2037aad..aed74b67 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -4,6 +4,7 @@ + diff --git a/app/src/main/java/tech/relaycorp/letro/contacts/ManageContactViewModel.kt b/app/src/main/java/tech/relaycorp/letro/contacts/ManageContactViewModel.kt index 2833a8e8..e7730a93 100644 --- a/app/src/main/java/tech/relaycorp/letro/contacts/ManageContactViewModel.kt +++ b/app/src/main/java/tech/relaycorp/letro/contacts/ManageContactViewModel.kt @@ -1,5 +1,6 @@ package tech.relaycorp.letro.contacts +import android.util.Log import androidx.annotation.IntDef import androidx.annotation.StringRes import androidx.compose.runtime.Immutable @@ -7,15 +8,19 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tech.relaycorp.letro.R +import tech.relaycorp.letro.account.storage.repository.AccountRepository import tech.relaycorp.letro.contacts.ManageContactScreenContent.Companion.REQUEST_SENT import tech.relaycorp.letro.contacts.ManageContactViewModel.Type.Companion.EDIT_CONTACT import tech.relaycorp.letro.contacts.ManageContactViewModel.Type.Companion.NEW_CONTACT @@ -23,20 +28,19 @@ import tech.relaycorp.letro.contacts.model.Contact import tech.relaycorp.letro.contacts.model.ContactPairingStatus import tech.relaycorp.letro.contacts.storage.repository.ContactsRepository import tech.relaycorp.letro.ui.navigation.Route -import tech.relaycorp.letro.utils.ext.decodeFromUTF import tech.relaycorp.letro.utils.ext.nullIfBlankOrEmpty import javax.inject.Inject -@OptIn(FlowPreview::class) +@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @HiltViewModel class ManageContactViewModel @Inject constructor( private val contactsRepository: ContactsRepository, + private val accountRepository: AccountRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { @Type private val screenType: Int = savedStateHandle[Route.ManageContact.KEY_SCREEN_TYPE]!! - private val currentAccountId: String? = (savedStateHandle.get(Route.ManageContact.KEY_CURRENT_ACCOUNT_ID_ENCODED) as? String)?.decodeFromUTF() // by default Android decode strings inside navigation library, but just in case we decode it here private val contactIdToEdit: Long? = savedStateHandle[Route.ManageContact.KEY_CONTACT_ID_TO_EDIT] private val _uiState = MutableStateFlow( @@ -66,6 +70,7 @@ class ManageContactViewModel @Inject constructor( val showPermissionGoToSettingsSignal: SharedFlow get() = _showPermissionGoToSettingsSignal + private var currentAccountId: String? = null private val contacts: HashSet = hashSetOf() private var editingContact: Contact? = null @@ -86,12 +91,19 @@ class ManageContactViewModel @Inject constructor( } } viewModelScope.launch { - currentAccountId?.let { currentAccountId -> - contactsRepository.getContacts(currentAccountId).collect { + accountRepository.currentAccount + .flatMapLatest { + currentAccountId = it?.accountId + if (it != null) { + contactsRepository.getContacts(it.accountId) + } else { + emptyFlow() + } + } + .collect { contacts.clear() contacts.addAll(it) } - } } viewModelScope.launch { checkActionButtonAvailabilityFlow @@ -138,6 +150,10 @@ class ManageContactViewModel @Inject constructor( } } EDIT_CONTACT -> { + if (contacts.any { it.id == contactIdToEdit }) { + Log.w(TAG, IllegalStateException("You cannot edit this contact. Contact belongs to ${contacts.firstOrNull()?.ownerVeraId}, but yours is $currentAccountId")) // TODO: log? + return + } updateContact() viewModelScope.launch { _onEditContactCompleted.emit(uiState.value.accountId) @@ -211,6 +227,7 @@ class ManageContactViewModel @Inject constructor( } private companion object { + private const val TAG = "ManageContactViewModel" private const val CHECK_ID_DEBOUNCE_DELAY_MS = 1_500L private val CORRECT_ID_REGEX = """^([^@]+@)?\p{L}{1,63}(\.\p{L}{1,63})+$""".toRegex() } diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/compose/ui/ComposeNewMessageScreen.kt b/app/src/main/java/tech/relaycorp/letro/conversation/compose/ui/ComposeNewMessageScreen.kt index 845c992e..ac8cb82c 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/compose/ui/ComposeNewMessageScreen.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/compose/ui/ComposeNewMessageScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -307,6 +308,9 @@ fun ComposeNewMessageScreen( placeHolderText = stringResource(id = R.string.new_message_body_hint), singleLine = false, placeholderColor = MaterialTheme.colorScheme.onSurfaceVariant, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), modifier = Modifier .then(Modifier) .applyIf(uiState.messageExceedsLimitTextError != null) { diff --git a/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt b/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt index 99cb91ee..d2fe4577 100644 --- a/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt +++ b/app/src/main/java/tech/relaycorp/letro/ui/navigation/LetroNavHost.kt @@ -70,8 +70,8 @@ import tech.relaycorp.letro.ui.theme.LetroColor import tech.relaycorp.letro.ui.utils.StringsProvider import tech.relaycorp.letro.utils.compose.navigation.navigateSingleTop import tech.relaycorp.letro.utils.compose.navigation.navigateWithPoppingAllBackStack +import tech.relaycorp.letro.utils.compose.navigation.popBackStackSafe import tech.relaycorp.letro.utils.compose.showSnackbar -import tech.relaycorp.letro.utils.ext.encodeToUTF @Composable fun LetroNavHost( @@ -214,7 +214,6 @@ fun LetroNavHost( navController.navigate( Route.ManageContact.getRouteName( screenType = ManageContactViewModel.Type.NEW_CONTACT, - currentAccountIdEncoded = uiState.currentAccount?.encodeToUTF(), ), ) }, @@ -234,7 +233,6 @@ fun LetroNavHost( navController.navigate( Route.ManageContact.getRouteName( screenType = ManageContactViewModel.Type.NEW_CONTACT, - currentAccountIdEncoded = uiState.currentAccount?.encodeToUTF(), ), ) }, @@ -250,7 +248,7 @@ fun LetroNavHost( ) } composable( - route = "${Route.ManageContact.name}/{${Route.ManageContact.KEY_CURRENT_ACCOUNT_ID_ENCODED}}&{${Route.ManageContact.KEY_SCREEN_TYPE}}&{${Route.ManageContact.KEY_CONTACT_ID_TO_EDIT}}", + route = "${Route.ManageContact.name}/{${Route.ManageContact.KEY_SCREEN_TYPE}}&{${Route.ManageContact.KEY_CONTACT_ID_TO_EDIT}}", arguments = listOf( navArgument(Route.ManageContact.KEY_CURRENT_ACCOUNT_ID_ENCODED) { type = NavType.StringType @@ -269,12 +267,12 @@ fun LetroNavHost( ) { entry -> ManageContactScreen( onBackClick = { - navController.popBackStack() + navController.popBackStackSafe() }, onEditContactCompleted = { when (val type = entry.arguments?.getInt(Route.ManageContact.KEY_SCREEN_TYPE)) { ManageContactViewModel.Type.EDIT_CONTACT -> { - navController.popBackStack() + navController.popBackStackSafe() snackbarHostState.showSnackbar(scope, stringsProvider.snackbar.contactEdited) } else -> throw IllegalStateException("Unknown screen type: $type") @@ -310,7 +308,6 @@ fun LetroNavHost( navController.navigate( Route.ManageContact.getRouteName( screenType = ManageContactViewModel.Type.EDIT_CONTACT, - currentAccountIdEncoded = uiState.currentAccount?.encodeToUTF(), contactIdToEdit = contact.id, ), ) @@ -343,7 +340,7 @@ fun LetroNavHost( val screenType = it.arguments?.getInt(Route.CreateNewMessage.KEY_SCREEN_TYPE) ComposeNewMessageScreen( conversationsStringsProvider = stringsProvider.conversations, - onBackClicked = { navController.popBackStack() }, + onBackClicked = { navController.popBackStackSafe() }, onMessageSent = { when (screenType) { ComposeNewMessageViewModel.ScreenType.REPLY_TO_EXISTING_CONVERSATION -> { @@ -353,7 +350,7 @@ fun LetroNavHost( ) } ComposeNewMessageViewModel.ScreenType.NEW_CONVERSATION -> { - navController.popBackStack() + navController.popBackStackSafe() } } snackbarHostState.showSnackbar(scope, stringsProvider.snackbar.messageSent) @@ -373,11 +370,11 @@ fun LetroNavHost( ConversationScreen( conversationsStringsProvider = stringsProvider.conversations, onConversationDeleted = { - navController.popBackStack() + navController.popBackStackSafe() snackbarHostState.showSnackbar(scope, stringsProvider.snackbar.conversationDeleted) }, onConversationArchived = { isArchived -> - navController.popBackStack() + navController.popBackStackSafe() snackbarHostState.showSnackbar(scope, if (isArchived) stringsProvider.snackbar.conversationArchived else stringsProvider.snackbar.conversationUnarchived) }, onReplyClick = { @@ -389,7 +386,7 @@ fun LetroNavHost( ) }, onBackClicked = { - navController.popBackStack() + navController.popBackStackSafe() }, onAttachmentClick = { fileId -> mainViewModel.onAttachmentClick(fileId) @@ -401,7 +398,7 @@ fun LetroNavHost( onAddAccountClick = { navController.navigate(Route.Registration.name) }, onNotificationsClick = onGoToNotificationsSettingsClick, onTermsAndConditionsClick = { mainViewModel.onTermsAndConditionsClick() }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navController.popBackStackSafe() }, onAccountDeleted = { snackbarHostState.showSnackbar(scope, stringsProvider.snackbar.accountDeleted) }, @@ -421,7 +418,6 @@ fun LetroNavHost( navController.navigate( Route.ManageContact.getRouteName( screenType = ManageContactViewModel.Type.NEW_CONTACT, - currentAccountIdEncoded = uiState.currentAccount?.encodeToUTF(), ), ) homeViewModel.onOptionFromContactsFloatingMenuClicked() diff --git a/app/src/main/java/tech/relaycorp/letro/ui/navigation/Route.kt b/app/src/main/java/tech/relaycorp/letro/ui/navigation/Route.kt index 94471798..69cc2eeb 100644 --- a/app/src/main/java/tech/relaycorp/letro/ui/navigation/Route.kt +++ b/app/src/main/java/tech/relaycorp/letro/ui/navigation/Route.kt @@ -84,9 +84,8 @@ sealed class Route( fun getRouteName( @ManageContactViewModel.Type screenType: Int, - currentAccountIdEncoded: String?, contactIdToEdit: Long = NO_ID, - ) = "${ManageContact.name}/$currentAccountIdEncoded&$screenType&$contactIdToEdit" + ) = "${ManageContact.name}/$screenType&$contactIdToEdit" } object Home : Route( diff --git a/app/src/main/java/tech/relaycorp/letro/utils/compose/navigation/NavigationUtils.kt b/app/src/main/java/tech/relaycorp/letro/utils/compose/navigation/NavigationUtils.kt index 0ab591cf..505d89e3 100644 --- a/app/src/main/java/tech/relaycorp/letro/utils/compose/navigation/NavigationUtils.kt +++ b/app/src/main/java/tech/relaycorp/letro/utils/compose/navigation/NavigationUtils.kt @@ -17,6 +17,12 @@ fun NavController.navigateSingleTop(route: Route) { } } +fun NavController.popBackStackSafe() { + if (currentBackStack.value.size > 2) { // StartDestination (Splash) + Root screen. + popBackStack() + } +} + fun NavController.navigateWithDropCurrentScreen(route: String) { navigate(route) { popBackStack()