diff --git a/app/src/main/java/tech/relaycorp/letro/account/SwitchAccountViewModel.kt b/app/src/main/java/tech/relaycorp/letro/account/SwitchAccountViewModel.kt new file mode 100644 index 00000000..f5c3beb6 --- /dev/null +++ b/app/src/main/java/tech/relaycorp/letro/account/SwitchAccountViewModel.kt @@ -0,0 +1,71 @@ +package tech.relaycorp.letro.account + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import tech.relaycorp.letro.account.model.Account +import tech.relaycorp.letro.account.storage.repository.AccountRepository +import tech.relaycorp.letro.account.utils.AccountsSorter +import javax.inject.Inject + +@HiltViewModel +class SwitchAccountViewModel @Inject constructor( + private val accountRepository: AccountRepository, + private val accountsSorter: AccountsSorter, +) : ViewModel() { + + private val accounts: MutableStateFlow> = MutableStateFlow(emptyList()) + + private val _switchAccountsBottomSheetState = MutableStateFlow(SwitchAccountsBottomSheetState()) + val switchAccountBottomSheetState: StateFlow + get() = _switchAccountsBottomSheetState + + init { + viewModelScope.launch { + accountRepository.allAccounts.collect { + accounts.emit( + accountsSorter.withCurrentAccountFirst(it), + ) + } + } + } + + fun onSwitchAccountsClick() { + setSwitchBottomSheetVisible(true) + } + + fun onSwitchAccountRequested(account: Account) { + setSwitchBottomSheetVisible(false) + viewModelScope.launch(Dispatchers.IO) { + accountRepository.switchAccount(account) + } + } + + fun onSwitchAccountDialogDismissed() { + setSwitchBottomSheetVisible(false) + } + + suspend fun onSwitchAccountRequested(accountId: String) { + setSwitchBottomSheetVisible(false) + accountRepository.switchAccount(accountId) + } + + private fun setSwitchBottomSheetVisible(isVisible: Boolean) { + _switchAccountsBottomSheetState.update { + it.copy( + isShown = isVisible, + accounts = if (isVisible) accounts.value else emptyList(), + ) + } + } +} + +data class SwitchAccountsBottomSheetState( + val isShown: Boolean = false, + val accounts: List = emptyList(), +) diff --git a/app/src/main/java/tech/relaycorp/letro/account/di/AccountModule.kt b/app/src/main/java/tech/relaycorp/letro/account/di/AccountModule.kt index 7a247f87..408ba6cb 100644 --- a/app/src/main/java/tech/relaycorp/letro/account/di/AccountModule.kt +++ b/app/src/main/java/tech/relaycorp/letro/account/di/AccountModule.kt @@ -6,6 +6,8 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import tech.relaycorp.letro.account.storage.repository.AccountRepository import tech.relaycorp.letro.account.storage.repository.AccountRepositoryImpl +import tech.relaycorp.letro.account.utils.AccountsSorter +import tech.relaycorp.letro.account.utils.AccountsSorterImpl import javax.inject.Singleton @Module @@ -17,4 +19,9 @@ interface AccountModule { fun bindAccountRepository( impl: AccountRepositoryImpl, ): AccountRepository + + @Binds + fun bindAccountSorter( + impl: AccountsSorterImpl, + ): AccountsSorter } diff --git a/app/src/main/java/tech/relaycorp/letro/account/storage/dao/AccountDao.kt b/app/src/main/java/tech/relaycorp/letro/account/storage/dao/AccountDao.kt index 972ed9f8..91bf065f 100644 --- a/app/src/main/java/tech/relaycorp/letro/account/storage/dao/AccountDao.kt +++ b/app/src/main/java/tech/relaycorp/letro/account/storage/dao/AccountDao.kt @@ -21,6 +21,9 @@ interface AccountDao { @Update suspend fun update(entity: Account): Int + @Update + suspend fun update(accounts: List) + @Query("SELECT * FROM $TABLE_NAME_ACCOUNT WHERE id=:id") suspend fun getById(id: Long): Account? diff --git a/app/src/main/java/tech/relaycorp/letro/account/storage/repository/AccountRepository.kt b/app/src/main/java/tech/relaycorp/letro/account/storage/repository/AccountRepository.kt index 47e44e75..81d6ed10 100644 --- a/app/src/main/java/tech/relaycorp/letro/account/storage/repository/AccountRepository.kt +++ b/app/src/main/java/tech/relaycorp/letro/account/storage/repository/AccountRepository.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import tech.relaycorp.letro.account.model.Account import tech.relaycorp.letro.account.storage.dao.AccountDao @@ -34,6 +35,8 @@ interface AccountRepository { suspend fun updateAccount(account: Account, accountId: String, veraidBundle: ByteArray) suspend fun deleteAccount(account: Account) + suspend fun switchAccount(newCurrentAccount: Account) + suspend fun switchAccount(accountId: String) } class AccountRepositoryImpl @Inject constructor( @@ -67,12 +70,31 @@ class AccountRepositoryImpl @Inject constructor( } } + override suspend fun switchAccount(accountId: String) { + _allAccounts.value.find { it.accountId == accountId }?.let { + switchAccount(it) + } + } + + override suspend fun switchAccount(newCurrentAccount: Account) { + if (newCurrentAccount.accountId == _currentAccount.value?.accountId) { + return + } + markAllExistingAccountsAsNonCurrent() + accountDao.update( + newCurrentAccount.copy( + isCurrent = true, + ), + ) + } + override suspend fun createAccount( requestedUserName: String, domainName: String, locale: Locale, veraidPrivateKey: PrivateKey, ) { + markAllExistingAccountsAsNonCurrent() accountDao.insert( Account( accountId = "$requestedUserName@$domainName", @@ -91,6 +113,15 @@ class AccountRepositoryImpl @Inject constructor( ) override suspend fun deleteAccount(account: Account) { + if (account.isCurrent) { + _allAccounts.value.firstOrNull { !it.isCurrent }?.let { + accountDao.update( + it.copy( + isCurrent = true, + ), + ) + } + } accountDao.deleteAccount(account) } @@ -107,4 +138,12 @@ class AccountRepositoryImpl @Inject constructor( ), ) } + + private suspend fun markAllExistingAccountsAsNonCurrent() { + val updatedAccounts = _allAccounts.value + .map { it.copy(isCurrent = false) } + if (updatedAccounts.isNotEmpty()) { + accountDao.update(updatedAccounts) + } + } } diff --git a/app/src/main/java/tech/relaycorp/letro/account/ui/SwitchAccountsBottomSheet.kt b/app/src/main/java/tech/relaycorp/letro/account/ui/SwitchAccountsBottomSheet.kt new file mode 100644 index 00000000..6946567b --- /dev/null +++ b/app/src/main/java/tech/relaycorp/letro/account/ui/SwitchAccountsBottomSheet.kt @@ -0,0 +1,102 @@ +package tech.relaycorp.letro.account.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import tech.relaycorp.letro.R +import tech.relaycorp.letro.account.model.Account +import tech.relaycorp.letro.ui.common.bottomsheet.LetroBottomSheet +import tech.relaycorp.letro.ui.theme.BodyMediumProminent + +@Composable +fun SwitchAccountsBottomSheet( + accounts: List, + onAccountClick: (Account) -> Unit, + onManageContactsClick: () -> Unit, + onDismissRequest: () -> Unit, +) { + LetroBottomSheet( + onDismissRequest = { onDismissRequest() }, + title = stringResource(id = R.string.switch_accounts), + ) { + Column { + for (i in accounts.indices) { + Account( + account = accounts[i], + onClick = { onAccountClick(accounts[i]) }, + ) + } + ManageContactsButton(onClick = onManageContactsClick) + } + } +} + +@Composable +private fun Account( + account: Account, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ), + ) { + Text( + text = account.accountId, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Icon( + painter = painterResource(id = if (account.isCurrent) R.drawable.radio_button_selected else R.drawable.radio_button_unselected), + contentDescription = stringResource(id = if (account.isCurrent) R.string.content_description_selected_item else R.string.content_description_unselected_item), + tint = MaterialTheme.colorScheme.onSurface, + ) + } +} + +@Composable +private fun ManageContactsButton( + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_settings_18), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.manage_accounts), + style = MaterialTheme.typography.BodyMediumProminent, + color = MaterialTheme.colorScheme.primary, + ) + } +} diff --git a/app/src/main/java/tech/relaycorp/letro/account/utils/AccountsSorter.kt b/app/src/main/java/tech/relaycorp/letro/account/utils/AccountsSorter.kt new file mode 100644 index 00000000..92fb1462 --- /dev/null +++ b/app/src/main/java/tech/relaycorp/letro/account/utils/AccountsSorter.kt @@ -0,0 +1,25 @@ +package tech.relaycorp.letro.account.utils + +import tech.relaycorp.letro.account.model.Account +import javax.inject.Inject + +interface AccountsSorter { + fun withCurrentAccountFirst(accounts: List): List +} + +class AccountsSorterImpl @Inject constructor() : AccountsSorter { + + override fun withCurrentAccountFirst(accounts: List): List { + val currentIndex = accounts.indexOfFirst { it.isCurrent } + if (currentIndex == -1) { + return accounts + } + return arrayListOf().apply { + add(accounts[currentIndex]) + for (i in accounts.indices) { + if (currentIndex == i) continue + add(accounts[i]) + } + } + } +} diff --git a/app/src/main/java/tech/relaycorp/letro/contacts/ui/ContactsScreen.kt b/app/src/main/java/tech/relaycorp/letro/contacts/ui/ContactsScreen.kt index f8b7edeb..b2d59891 100644 --- a/app/src/main/java/tech/relaycorp/letro/contacts/ui/ContactsScreen.kt +++ b/app/src/main/java/tech/relaycorp/letro/contacts/ui/ContactsScreen.kt @@ -17,8 +17,8 @@ import androidx.compose.ui.unit.dp import tech.relaycorp.letro.R import tech.relaycorp.letro.contacts.ContactsViewModel import tech.relaycorp.letro.contacts.model.Contact -import tech.relaycorp.letro.ui.common.BottomSheetAction -import tech.relaycorp.letro.ui.common.LetroActionsBottomSheet +import tech.relaycorp.letro.ui.common.bottomsheet.BottomSheetAction +import tech.relaycorp.letro.ui.common.bottomsheet.LetroActionsBottomSheet import tech.relaycorp.letro.ui.common.text.BoldText import tech.relaycorp.letro.ui.theme.LabelLargeProminent import tech.relaycorp.letro.ui.theme.LetroColor diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/list/ui/ConversationsListScreen.kt b/app/src/main/java/tech/relaycorp/letro/conversation/list/ui/ConversationsListScreen.kt index 8f83754e..9dcf2aef 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/list/ui/ConversationsListScreen.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/list/ui/ConversationsListScreen.kt @@ -40,8 +40,8 @@ import tech.relaycorp.letro.conversation.list.ConversationsListViewModel import tech.relaycorp.letro.conversation.list.section.ConversationSectionInfo import tech.relaycorp.letro.conversation.model.ExtendedConversation import tech.relaycorp.letro.conversation.model.ExtendedMessage -import tech.relaycorp.letro.ui.common.BottomSheetAction -import tech.relaycorp.letro.ui.common.LetroActionsBottomSheet +import tech.relaycorp.letro.ui.common.bottomsheet.BottomSheetAction +import tech.relaycorp.letro.ui.common.bottomsheet.LetroActionsBottomSheet import tech.relaycorp.letro.ui.theme.BodyLargeProminent import tech.relaycorp.letro.ui.theme.BodyMediumProminent import tech.relaycorp.letro.ui.theme.LabelSmallProminent diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewConversationProcessor.kt b/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewConversationProcessor.kt index 68158090..0c06257c 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewConversationProcessor.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewConversationProcessor.kt @@ -59,7 +59,10 @@ class NewConversationProcessorImpl @Inject constructor( PushData( title = conversationWrapper.senderVeraId, text = conversationWrapper.messageText, - action = PushAction.OpenConversation(conversationWrapper.conversationId), + action = PushAction.OpenConversation( + conversationId = conversationWrapper.conversationId, + accountId = conversation.ownerVeraId, + ), recipientAccountId = conversation.ownerVeraId, notificationId = messageId.toInt(), ), diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewMessageProcessor.kt b/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewMessageProcessor.kt index c94ba131..9f03b50c 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewMessageProcessor.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/server/processor/NewMessageProcessor.kt @@ -62,7 +62,10 @@ class NewMessageProcessorImpl @Inject constructor( PushData( title = message.senderVeraId, text = message.text, - action = PushAction.OpenConversation(messageWrapper.conversationId), + action = PushAction.OpenConversation( + conversationId = messageWrapper.conversationId, + accountId = conversation.ownerVeraId, + ), recipientAccountId = conversation.ownerVeraId, notificationId = messageId.toInt(), ), diff --git a/app/src/main/java/tech/relaycorp/letro/conversation/storage/repository/ConversationsRepository.kt b/app/src/main/java/tech/relaycorp/letro/conversation/storage/repository/ConversationsRepository.kt index dfc799cb..00d96e7d 100644 --- a/app/src/main/java/tech/relaycorp/letro/conversation/storage/repository/ConversationsRepository.kt +++ b/app/src/main/java/tech/relaycorp/letro/conversation/storage/repository/ConversationsRepository.kt @@ -81,15 +81,16 @@ class ConversationsRepositoryImpl @Inject constructor( init { scope.launch { - accountRepository.currentAccount.collect { - if (it != null) { - startCollectContacts(it) - startCollectConversations(it) + accountRepository.allAccounts.collect { + val currentAccount = it.firstOrNull { it.isCurrent } + conversationsCollectionJob?.cancel() + conversationsCollectionJob = null + contactsCollectionJob?.cancel() + contactsCollectionJob = null + if (currentAccount != null) { + startCollectContacts(currentAccount) + startCollectConversations(currentAccount) } else { - conversationsCollectionJob?.cancel() - conversationsCollectionJob = null - contactsCollectionJob?.cancel() - contactsCollectionJob = null _extendedConversations.emit(emptyList()) contacts.emit(emptyList()) } diff --git a/app/src/main/java/tech/relaycorp/letro/push/PushManager.kt b/app/src/main/java/tech/relaycorp/letro/push/PushManager.kt index b026ebce..3352a281 100644 --- a/app/src/main/java/tech/relaycorp/letro/push/PushManager.kt +++ b/app/src/main/java/tech/relaycorp/letro/push/PushManager.kt @@ -13,6 +13,7 @@ import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import tech.relaycorp.letro.R import tech.relaycorp.letro.main.ui.MainActivity +import tech.relaycorp.letro.push.model.PushAction import tech.relaycorp.letro.push.model.PushChannel import tech.relaycorp.letro.push.model.PushData import javax.inject.Inject @@ -43,6 +44,11 @@ class PushManagerImpl @Inject constructor( } val groupName = pushData.recipientAccountId + + val groupIntent = Intent(context, MainActivity::class.java).apply { + putExtra(KEY_PUSH_ACTION, PushAction.OpenMainPage(groupName)) + } + val notification = NotificationCompat.Builder(context, pushData.channelId) .setSmallIcon(R.drawable.letro_notification_icon) .setContentTitle(pushData.title) @@ -59,6 +65,8 @@ class PushManagerImpl @Inject constructor( .setContentText(context.resources.getQuantityString(R.plurals.new_notifications_group_count, notificationsInGroupCount, notificationsInGroupCount)) .setGroup(groupName) .setGroupSummary(true) + .setContentIntent(PendingIntent.getActivity(context, groupName.hashCode(), groupIntent, PendingIntent.FLAG_IMMUTABLE)) + .setAutoCancel(true) .build() if (permissionManager.isPermissionGranted()) { diff --git a/app/src/main/java/tech/relaycorp/letro/push/model/PushData.kt b/app/src/main/java/tech/relaycorp/letro/push/model/PushData.kt index b5531f8a..50483d41 100644 --- a/app/src/main/java/tech/relaycorp/letro/push/model/PushData.kt +++ b/app/src/main/java/tech/relaycorp/letro/push/model/PushData.kt @@ -17,5 +17,11 @@ sealed interface PushAction : Parcelable { @Parcelize data class OpenConversation( val conversationId: String, + val accountId: String, + ) : PushAction + + @Parcelize + data class OpenMainPage( + val accountId: String, ) : PushAction } diff --git a/app/src/main/java/tech/relaycorp/letro/settings/SettingsScreen.kt b/app/src/main/java/tech/relaycorp/letro/settings/SettingsScreen.kt index e42dbea3..667ffc65 100644 --- a/app/src/main/java/tech/relaycorp/letro/settings/SettingsScreen.kt +++ b/app/src/main/java/tech/relaycorp/letro/settings/SettingsScreen.kt @@ -43,6 +43,7 @@ fun SettingsScreen( onNotificationsClick: () -> Unit, onTermsAndConditionsClick: () -> Unit, onBackClick: () -> Unit, + onAddAccountClick: () -> Unit, onAccountDeleted: () -> Unit, viewModel: SettingsViewModel = hiltViewModel(), ) { @@ -66,6 +67,9 @@ fun SettingsScreen( onAccountDeleteClick = { viewModel.onAccountDeleteClick(it) }, + onAddAccountClick = { + onAddAccountClick() + }, ) Spacer(modifier = Modifier.height(24.dp)) NotificationsBlock( @@ -91,6 +95,7 @@ fun SettingsScreen( @Composable private fun AccountsBlock( accounts: List, + onAddAccountClick: () -> Unit, onAccountDeleteClick: (Account) -> Unit, ) { SettingsBlock( @@ -102,6 +107,27 @@ private fun AccountsBlock( onDeleteClick = { onAccountDeleteClick(accounts[i]) }, ) } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onAddAccountClick() } + .padding( + horizontal = ELEMENT_HORIZONTAL_PADDING, + vertical = ELEMENT_VERTICAL_PADDING, + ), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_plus_18), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.add_another_account), + style = MaterialTheme.typography.LabelLargeProminent, + color = MaterialTheme.colorScheme.primary, + ) + } } } @@ -115,8 +141,8 @@ private fun Account( modifier = Modifier .fillMaxWidth() .padding( - horizontal = 16.dp, - vertical = 14.dp, + horizontal = ELEMENT_HORIZONTAL_PADDING, + vertical = ELEMENT_VERTICAL_PADDING, ), ) { Text( @@ -209,8 +235,8 @@ private fun SettingElement( clickable { onClick?.invoke() } } .padding( - horizontal = 16.dp, - vertical = 12.dp, + horizontal = ELEMENT_HORIZONTAL_PADDING, + vertical = ELEMENT_VERTICAL_PADDING, ), ) { Icon( @@ -283,3 +309,6 @@ private fun DeleteAccountDialog( }, ) } + +private val ELEMENT_HORIZONTAL_PADDING = 16.dp +private val ELEMENT_VERTICAL_PADDING = 12.dp diff --git a/app/src/main/java/tech/relaycorp/letro/settings/SettingsViewModel.kt b/app/src/main/java/tech/relaycorp/letro/settings/SettingsViewModel.kt index bc67f929..d65cc2b8 100644 --- a/app/src/main/java/tech/relaycorp/letro/settings/SettingsViewModel.kt +++ b/app/src/main/java/tech/relaycorp/letro/settings/SettingsViewModel.kt @@ -12,16 +12,18 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tech.relaycorp.letro.account.model.Account import tech.relaycorp.letro.account.storage.repository.AccountRepository +import tech.relaycorp.letro.account.utils.AccountsSorter import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( private val accountRepository: AccountRepository, + private val accountsSorter: AccountsSorter, ) : ViewModel() { val accounts: StateFlow> get() = accountRepository.allAccounts - .map(::moveCurrentAccountToTop) + .map(accountsSorter::withCurrentAccountFirst) .stateIn(viewModelScope, SharingStarted.Eagerly, accountRepository.allAccounts.value) private val _deleteAccountConfirmationDialog = MutableStateFlow(DeleteAccountDialogState()) @@ -38,6 +40,7 @@ class SettingsViewModel @Inject constructor( } fun onConfirmAccountDeleteClick(account: Account) { + onConfirmAccountDeleteDialogDismissed() viewModelScope.launch { accountRepository.deleteAccount(account) } @@ -51,20 +54,6 @@ class SettingsViewModel @Inject constructor( ) } } - - private fun moveCurrentAccountToTop(accounts: List): List { - val currentIndex = accounts.indexOfFirst { it.isCurrent } - if (currentIndex == -1) { - return accounts - } - return arrayListOf().apply { - add(accounts[currentIndex]) - for (i in 0 until currentIndex) { - if (currentIndex == i) continue - add(accounts[i]) - } - } - } } data class DeleteAccountDialogState( diff --git a/app/src/main/java/tech/relaycorp/letro/ui/common/LetroActionsBottomSheet.kt b/app/src/main/java/tech/relaycorp/letro/ui/common/bottomsheet/LetroBottomSheet.kt similarity index 71% rename from app/src/main/java/tech/relaycorp/letro/ui/common/LetroActionsBottomSheet.kt rename to app/src/main/java/tech/relaycorp/letro/ui/common/bottomsheet/LetroBottomSheet.kt index 3464b2b2..efce51b6 100644 --- a/app/src/main/java/tech/relaycorp/letro/ui/common/LetroActionsBottomSheet.kt +++ b/app/src/main/java/tech/relaycorp/letro/ui/common/bottomsheet/LetroBottomSheet.kt @@ -1,4 +1,4 @@ -package tech.relaycorp.letro.ui.common +package tech.relaycorp.letro.ui.common.bottomsheet import androidx.annotation.DrawableRes import androidx.annotation.StringRes @@ -32,6 +32,35 @@ import tech.relaycorp.letro.ui.theme.TitleSmallProminent import tech.relaycorp.letro.utils.ext.applyIf import androidx.compose.ui.res.painterResource as painterResource1 +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LetroBottomSheet( + onDismissRequest: () -> Unit, + title: String? = null, + content: @Composable () -> Unit, +) { + ModalBottomSheet( + containerColor = LetroColor.SurfaceContainerLow, + onDismissRequest = { + onDismissRequest() + }, + ) { + Column( + modifier = Modifier + .padding( + PaddingValues( + bottom = 44.dp, + ), + ), + ) { + if (title != null) { + BottomSheetTitle(title = title) + } + content() + } + } +} + data class BottomSheetAction( @DrawableRes val icon: Int, @StringRes val title: Int, @@ -53,7 +82,7 @@ fun LetroActionsBottomSheet( onDismissRequest() }, ) { - BottomSheetContent( + ActionsBottomSheetContent( actions = actions, title = title, ) @@ -61,7 +90,7 @@ fun LetroActionsBottomSheet( } @Composable -private fun BottomSheetContent( +private fun ActionsBottomSheetContent( actions: List, title: String? = null, ) { @@ -74,33 +103,47 @@ private fun BottomSheetContent( ), ) { if (title != null) { - Text( - text = title, - style = MaterialTheme.typography.TitleSmallProminent, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - ), - ) - Spacer( - modifier = Modifier.height(14.dp), - ) - Divider( - color = MaterialTheme.colorScheme.outlineVariant, - ) + BottomSheetTitle(title = title) } - LazyColumn { - items(actions) { - BottomSheetActionView( - icon = it.icon, - title = it.title, - onClick = it.action, - isChosen = it.isChosen, - trailingText = it.trailingText, - ) - } + ActionsContent(actions = actions) + } +} + +@Composable +private fun BottomSheetTitle( + title: String, +) { + Text( + text = title, + style = MaterialTheme.typography.TitleSmallProminent, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + ), + ) + Spacer( + modifier = Modifier.height(14.dp), + ) + Divider( + color = MaterialTheme.colorScheme.outlineVariant, + ) +} + +@Composable +private fun ActionsContent( + actions: List, +) { + LazyColumn { + items(actions) { + BottomSheetActionView( + icon = it.icon, + title = it.title, + onClick = it.action, + isChosen = it.isChosen, + trailingText = it.trailingText, + ) } } } 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 aa5e156d..9fd89257 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 @@ -1,3 +1,5 @@ +@file:OptIn(DelicateCoroutinesApi::class) + package tech.relaycorp.letro.ui.navigation import android.util.Log @@ -33,8 +35,15 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import tech.relaycorp.letro.R +import tech.relaycorp.letro.account.SwitchAccountViewModel import tech.relaycorp.letro.account.registration.ui.RegistrationScreen +import tech.relaycorp.letro.account.ui.SwitchAccountsBottomSheet import tech.relaycorp.letro.awala.ui.error.AwalaInitializationError import tech.relaycorp.letro.awala.ui.initialization.AwalaInitializationInProgress import tech.relaycorp.letro.awala.ui.notinstalled.AwalaNotInstalledScreen @@ -67,6 +76,7 @@ fun LetroNavHost( onGoToNotificationsSettingsClick: () -> Unit, mainViewModel: MainViewModel, homeViewModel: HomeViewModel = hiltViewModel(), + switchAccountViewModel: SwitchAccountViewModel = hiltViewModel(), ) { val navController = rememberNavController() val scope = rememberCoroutineScope() @@ -75,6 +85,8 @@ fun LetroNavHost( val uiState by mainViewModel.uiState.collectAsState() + val switchAccountsBottomSheetState by switchAccountViewModel.switchAccountBottomSheetState.collectAsState() + val homeUiState by homeViewModel.uiState.collectAsState() val floatingActionButtonConfig = homeUiState.floatingActionButtonConfig @@ -109,13 +121,21 @@ fun LetroNavHost( return@LaunchedEffect } mainViewModel.pushAction.collect { pushAction -> - when (pushAction) { - is PushAction.OpenConversation -> { - navController.navigate( - Route.Conversation.getRouteName( - conversationId = pushAction.conversationId, - ), - ) + withContext(Dispatchers.IO) { + when (pushAction) { + is PushAction.OpenConversation -> { + GlobalScope.launch(Dispatchers.Main) { + navController.navigate( + Route.Conversation.getRouteName( + conversationId = pushAction.conversationId, + ), + ) + } + switchAccountViewModel.onSwitchAccountRequested(pushAction.accountId) + } + is PushAction.OpenMainPage -> { + switchAccountViewModel.onSwitchAccountRequested(pushAction.accountId) + } } } } @@ -129,12 +149,23 @@ fun LetroNavHost( .fillMaxSize() .padding(paddingValues), ) { + if (switchAccountsBottomSheetState.isShown) { + SwitchAccountsBottomSheet( + accounts = switchAccountsBottomSheetState.accounts, + onAccountClick = { switchAccountViewModel.onSwitchAccountRequested(it) }, + onManageContactsClick = { + navController.navigate(Route.Settings.name) + switchAccountViewModel.onSwitchAccountDialogDismissed() + }, + onDismissRequest = { switchAccountViewModel.onSwitchAccountDialogDismissed() }, + ) + } Column { if (currentRoute.showTopBar && currentAccount != null) { LetroTopBar( accountVeraId = currentAccount, isAccountCreated = uiState.isCurrentAccountCreated, - onChangeAccountClicked = { /*TODO*/ }, + onChangeAccountClicked = { switchAccountViewModel.onSwitchAccountsClick() }, onSettingsClicked = { navController.navigate(Route.Settings.name) }, ) } @@ -355,6 +386,7 @@ fun LetroNavHost( } composable(Route.Settings.name) { SettingsScreen( + onAddAccountClick = { navController.navigate(Route.Registration.name) }, onNotificationsClick = onGoToNotificationsSettingsClick, onTermsAndConditionsClick = { mainViewModel.onTermsAndConditionsClick() }, onBackClick = { navController.popBackStack() }, diff --git a/app/src/main/res/drawable/ic_plus_18.xml b/app/src/main/res/drawable/ic_plus_18.xml new file mode 100644 index 00000000..83f77416 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_18.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_settings_18.xml b/app/src/main/res/drawable/ic_settings_18.xml new file mode 100644 index 00000000..564ea047 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_18.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/radio_button_selected.xml b/app/src/main/res/drawable/radio_button_selected.xml new file mode 100644 index 00000000..eb7d414d --- /dev/null +++ b/app/src/main/res/drawable/radio_button_selected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/radio_button_unselected.xml b/app/src/main/res/drawable/radio_button_unselected.xml new file mode 100644 index 00000000..f27e1143 --- /dev/null +++ b/app/src/main/res/drawable/radio_button_unselected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5b988cd..a597abf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,13 +148,16 @@ Google are yet to fix this issue, but we know that reinstalling Letro helps in some cases. Manage accounts - + Add another account About letro App version (%s) Terms & Conditions Delete account By deleting the account %s you will lose all your contacts and conversations. Account deleted. + Switch accounts + Selected item + Unselected item %s MB %s KB