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()