Skip to content

Commit

Permalink
feat: multi accounts pt.1 (#91)
Browse files Browse the repository at this point in the history
multi accounts pt.1
  • Loading branch information
migulyaev authored Oct 3, 2023
1 parent e4ff96d commit 1e0627c
Show file tree
Hide file tree
Showing 22 changed files with 487 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -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<List<Account>> = MutableStateFlow(emptyList())

private val _switchAccountsBottomSheetState = MutableStateFlow(SwitchAccountsBottomSheetState())
val switchAccountBottomSheetState: StateFlow<SwitchAccountsBottomSheetState>
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<Account> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,4 +19,9 @@ interface AccountModule {
fun bindAccountRepository(
impl: AccountRepositoryImpl,
): AccountRepository

@Binds
fun bindAccountSorter(
impl: AccountsSorterImpl,
): AccountsSorter
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ interface AccountDao {
@Update
suspend fun update(entity: Account): Int

@Update
suspend fun update(accounts: List<Account>)

@Query("SELECT * FROM $TABLE_NAME_ACCOUNT WHERE id=:id")
suspend fun getById(id: Long): Account?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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)
}

Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Account>,
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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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<Account>): List<Account>
}

class AccountsSorterImpl @Inject constructor() : AccountsSorter {

override fun withCurrentAccountFirst(accounts: List<Account>): List<Account> {
val currentIndex = accounts.indexOfFirst { it.isCurrent }
if (currentIndex == -1) {
return accounts
}
return arrayListOf<Account>().apply {
add(accounts[currentIndex])
for (i in accounts.indices) {
if (currentIndex == i) continue
add(accounts[i])
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
Loading

0 comments on commit 1e0627c

Please sign in to comment.