Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: main screen + contacts implementation #44

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app-old/src/main/java/tech/relaycorp/letro/ui/theme/Type.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ val Typography = Typography(
),
labelLarge = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp,
letterSpacing = (-0.1).sp,
),
labelMedium = TextStyle(
fontFamily = Inter,
Expand Down
3 changes: 3 additions & 0 deletions app/lint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<issue id="LogConditional">
<ignore path="**"/>
</issue>
<issue id="VectorPath">
<ignore path="**" />
</issue>

<issue id="TrustAllX509TrustManager">
<ignore path="org/bouncycastle/est/jcajce/*.class" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package tech.relaycorp.letro.account.storage

import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import tech.relaycorp.letro.account.model.Account
import tech.relaycorp.letro.main.MainViewModel
import javax.inject.Inject

interface AccountRepository {
Expand Down Expand Up @@ -34,6 +36,7 @@ class AccountRepositoryImpl @Inject constructor(
}
databaseScope.launch {
_allAccounts.collect { list ->
Log.d(MainViewModel.TAG, "AccountRepository.emit(currentAccount)")
_currentAccount.emit(
list.firstOrNull { it.isCurrent },
)
Expand Down
138 changes: 138 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/contacts/ContactsViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package tech.relaycorp.letro.contacts

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
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.AccountRepository
import tech.relaycorp.letro.contacts.model.Contact
import tech.relaycorp.letro.contacts.model.ContactPairingStatus
import tech.relaycorp.letro.contacts.storage.ContactsRepository
import javax.inject.Inject

@HiltViewModel
class ContactsViewModel @Inject constructor(
private val contactsRepository: ContactsRepository,
private val accountRepository: AccountRepository,
) : ViewModel() {

private val _contacts: MutableStateFlow<List<Contact>> = MutableStateFlow(emptyList())
val contacts: StateFlow<List<Contact>>
get() = _contacts

private val _editContactBottomSheetStateState = MutableStateFlow(EditContactBottomSheetState())
val editContactBottomSheetState: StateFlow<EditContactBottomSheetState>
get() = _editContactBottomSheetStateState

private val _deleteContactDialogStateState = MutableStateFlow(DeleteContactDialogState())
val deleteContactDialogState: StateFlow<DeleteContactDialogState>
get() = _deleteContactDialogStateState

private val _showContactDeletedSnackbarSignal = MutableSharedFlow<Unit>()
val showContactDeletedSnackbarSignal: SharedFlow<Unit>
get() = _showContactDeletedSnackbarSignal

private var contactsCollectionJob: Job? = null

init {
viewModelScope.launch {
accountRepository.currentAccount.collect {
observeContacts(it)
}
}
}

fun onActionsButtonClick(contact: Contact) {
viewModelScope.launch {
_editContactBottomSheetStateState.update {
it.copy(
isShown = true,
contact = contact,
)
}
}
}

fun onEditBottomSheetDismissed() {
closeEditBottomSheet()
}

fun onEditContactClick() {
closeEditBottomSheet()
}

fun onDeleteContactDialogDismissed() {
closeDeleteContactDialog()
}

fun onDeleteContactClick(contact: Contact) {
closeEditBottomSheet()
viewModelScope.launch {
_deleteContactDialogStateState.update {
it.copy(
isShown = true,
contact = contact,
)
}
}
}

fun onConfirmDeletingContactClick(contact: Contact) {
contactsRepository.deleteContact(contact)
closeDeleteContactDialog()
viewModelScope.launch {
_showContactDeletedSnackbarSignal.emit(Unit)
}
}

private fun closeDeleteContactDialog() {
viewModelScope.launch {
_deleteContactDialogStateState.update {
it.copy(
isShown = false,
contact = null,
)
}
}
}

private fun closeEditBottomSheet() {
viewModelScope.launch {
_editContactBottomSheetStateState.update {
it.copy(
isShown = false,
contact = null,
)
}
}
}

private fun observeContacts(account: Account?) {
contactsCollectionJob?.cancel()
contactsCollectionJob = null
if (account != null) {
contactsCollectionJob = viewModelScope.launch {
contactsRepository.getContacts(account.veraId).collect {
_contacts.emit(it.filter { it.status == ContactPairingStatus.COMPLETED })
}
}
}
}
}

data class EditContactBottomSheetState(
val isShown: Boolean = false,
val contact: Contact? = null,
)

data class DeleteContactDialogState(
val isShown: Boolean = false,
val contact: Contact? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package tech.relaycorp.letro.contacts

import androidx.annotation.IntDef
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tech.relaycorp.letro.R
import tech.relaycorp.letro.contacts.ManageContactViewModel.Type.Companion.EDIT_CONTACT
import tech.relaycorp.letro.contacts.ManageContactViewModel.Type.Companion.NEW_CONTACT
import tech.relaycorp.letro.contacts.model.Contact
import tech.relaycorp.letro.contacts.model.ContactPairingStatus
import tech.relaycorp.letro.contacts.storage.ContactsRepository
import tech.relaycorp.letro.ui.navigation.Route
import tech.relaycorp.letro.utils.ext.nullIfBlankOrEmpty
import javax.inject.Inject

@HiltViewModel
class ManageContactViewModel @Inject constructor(
private val contactsRepository: ContactsRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {

@Type
private val screenType: Int = savedStateHandle[Route.ManageContact.KEY_SCREEN_TYPE]!!
private val currentAccountId: String? = savedStateHandle[Route.ManageContact.KEY_CURRENT_ACCOUNT_ID]
private val contactIdToEdit: Long? = savedStateHandle[Route.ManageContact.KEY_CONTACT_ID_TO_EDIT]

private val _uiState = MutableStateFlow(
PairWithOthersUiState(
manageContactTexts = when (screenType) {
NEW_CONTACT -> ManageContactTexts.PairWithOthers()
EDIT_CONTACT -> ManageContactTexts.EditContact()
else -> throw IllegalStateException("Unknown screen type: $screenType")
},
),
)
val uiState: StateFlow<PairWithOthersUiState>
get() = _uiState

private val _onActionCompleted = MutableSharedFlow<String>()
val onActionCompleted: SharedFlow<String>
get() = _onActionCompleted

private val contacts: HashSet<Contact> = hashSetOf()

private var editingContact: Contact? = null

init {
viewModelScope.launch {
contactIdToEdit?.let { id ->
contactsRepository.getContactById(id)?.let { contactToEdit ->
editingContact = contactToEdit
_uiState.update {
it.copy(
veraId = contactToEdit.contactVeraId,
alias = contactToEdit.alias,
isVeraIdInputEnabled = false,
)
}
}
}
}
viewModelScope.launch {
currentAccountId?.let { currentAccountId ->
contactsRepository.getContacts(currentAccountId).collect {
contacts.clear()
contacts.addAll(it)
}
}
}
}

fun onIdChanged(id: String) {
viewModelScope.launch {
val trimmedId = id.trim()
_uiState.update {
it.copy(
veraId = trimmedId,
isSentRequestAgainHintVisible = contacts.any { it.contactVeraId == trimmedId && it.status == ContactPairingStatus.REQUEST_SENT },
pairingErrorCaption = getPairingErrorMessage(trimmedId),
)
}
}
}

fun onAliasChanged(alias: String) {
viewModelScope.launch {
_uiState.update {
it.copy(
alias = alias,
)
}
}
}

fun onActionButtonClick() {
when (screenType) {
NEW_CONTACT -> sendNewContactRequest()
EDIT_CONTACT -> updateContact()
else -> throw IllegalStateException("Unknown screen type: $screenType")
}
viewModelScope.launch {
_onActionCompleted.emit(uiState.value.veraId)
}
}

private fun updateContact() {
editingContact?.let { editingContact ->
contactsRepository.updateContact(
editingContact.copy(
alias = uiState.value.alias?.nullIfBlankOrEmpty(),
),
)
}
}

private fun sendNewContactRequest() {
currentAccountId?.let { currentAccountId ->
contactsRepository.addNewContact(
contact = Contact(
ownerVeraId = currentAccountId,
contactVeraId = uiState.value.veraId,
alias = uiState.value.alias?.nullIfBlankOrEmpty(),
status = ContactPairingStatus.REQUEST_SENT,
),
)
}
}

private fun getPairingErrorMessage(contactId: String): PairingErrorCaption? {
val contact = contacts.find { it.contactVeraId == contactId }
return when {
contact == null -> null
contact.status == ContactPairingStatus.COMPLETED -> PairingErrorCaption(R.string.pair_request_already_paired)
contact.status >= ContactPairingStatus.MATCH -> PairingErrorCaption(R.string.pair_request_already_in_progress)
else -> null
}
}

@IntDef(NEW_CONTACT, EDIT_CONTACT)
annotation class Type {
companion object {
const val NEW_CONTACT = 0
const val EDIT_CONTACT = 1
}
}
}

data class PairWithOthersUiState(
val manageContactTexts: ManageContactTexts,
val veraId: String = "",
val alias: String? = null,
val isSentRequestAgainHintVisible: Boolean = false,
val isVeraIdInputEnabled: Boolean = true,
val pairingErrorCaption: PairingErrorCaption? = null,
)

@Immutable
sealed class ManageContactTexts(
@StringRes val title: Int,
@StringRes val button: Int,
) {
class PairWithOthers : ManageContactTexts(
title = R.string.general_pair_with_others,
button = R.string.onboarding_pair_with_people_button,
)

class EditContact : ManageContactTexts(
title = R.string.edit_name,
button = R.string.save_changes,
)
}

data class PairingErrorCaption(
@StringRes val message: Int,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tech.relaycorp.letro.contacts.storage

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
Expand Down Expand Up @@ -28,4 +29,7 @@ interface ContactsDao {

@Query("SELECT * FROM $TABLE_NAME_CONTACTS WHERE ownerVeraId = :accountVeraId")
fun getContactsForAccount(accountVeraId: String): Flow<List<Contact>>

@Delete
suspend fun deleteContact(contact: Contact)
}
Loading