Skip to content

Commit

Permalink
fix: Design review fixes (#52)
Browse files Browse the repository at this point in the history
- design review fixes https://docs.google.com/document/d/1uGrbFwrzmr-WuZJsbFb53QWaZHzgeznfjUlT_ChEeaU/edit#heading=h.5tgvnsjdxlv7
- pair with others id validation
- welcome to letro screen displaying until the first pair request
- Got it screen moved to the manage contact screen
  • Loading branch information
migulyaev authored Sep 18, 2023
1 parent b83285a commit da8e89c
Show file tree
Hide file tree
Showing 21 changed files with 299 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
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.update
import kotlinx.coroutines.launch
import tech.relaycorp.letro.R
Expand All @@ -24,6 +26,7 @@ import tech.relaycorp.letro.utils.ext.decodeFromUTF
import tech.relaycorp.letro.utils.ext.nullIfBlankOrEmpty
import javax.inject.Inject

@OptIn(FlowPreview::class)
@HiltViewModel
class ManageContactViewModel @Inject constructor(
private val contactsRepository: ContactsRepository,
Expand All @@ -42,14 +45,21 @@ class ManageContactViewModel @Inject constructor(
EDIT_CONTACT -> ManageContactTexts.EditContact()
else -> throw IllegalStateException("Unknown screen type: $screenType")
},
isActionButtonEnabled = screenType == EDIT_CONTACT,
),
)
val uiState: StateFlow<PairWithOthersUiState>
get() = _uiState

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

private val _onEditContactCompleted = MutableSharedFlow<String>()
val onEditContactCompleted: SharedFlow<String>
get() = _onEditContactCompleted

private val _goBackSignal = MutableSharedFlow<Unit>()
val goBackSignal: SharedFlow<Unit>
get() = _goBackSignal

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

Expand Down Expand Up @@ -78,6 +88,13 @@ class ManageContactViewModel @Inject constructor(
}
}
}
viewModelScope.launch {
checkActionButtonAvailabilityFlow
.debounce(CHECK_ID_DEBOUNCE_DELAY_MS)
.collect {
checkIfIdIsCorrect(it)
}
}
}

fun onIdChanged(id: String) {
Expand All @@ -87,9 +104,9 @@ class ManageContactViewModel @Inject constructor(
it.copy(
veraId = trimmedId,
isSentRequestAgainHintVisible = contacts.any { it.contactVeraId == trimmedId && it.status == ContactPairingStatus.REQUEST_SENT },
pairingErrorCaption = getPairingErrorMessage(trimmedId),
)
}
checkActionButtonAvailabilityFlow.emit(trimmedId)
}
}

Expand All @@ -103,14 +120,45 @@ class ManageContactViewModel @Inject constructor(
}
}

fun onActionButtonClick() {
fun onUpdateContactButtonClick() {
when (screenType) {
NEW_CONTACT -> sendNewContactRequest()
EDIT_CONTACT -> updateContact()
NEW_CONTACT -> {
sendNewContactRequest()
viewModelScope.launch {
_uiState.update {
it.copy(
showRequestSentScreen = true,
)
}
}
}
EDIT_CONTACT -> {
updateContact()
viewModelScope.launch {
_onEditContactCompleted.emit(uiState.value.veraId)
}
}
else -> throw IllegalStateException("Unknown screen type: $screenType")
}
}

fun onGotItClick() {
contactsRepository.saveRequestWasOnceSent()
viewModelScope.launch {
_goBackSignal.emit(Unit)
}
}

private fun checkIfIdIsCorrect(id: String) {
viewModelScope.launch {
_onActionCompleted.emit(uiState.value.veraId)
val isValidId = id.matches(CORRECT_ID_REGEX)
val errorMessage = getPairingErrorMessage(id, isValidId)
_uiState.update {
it.copy(
isActionButtonEnabled = isValidId && errorMessage == null,
pairingErrorCaption = errorMessage,
)
}
}
}

Expand All @@ -137,16 +185,22 @@ class ManageContactViewModel @Inject constructor(
}
}

private fun getPairingErrorMessage(contactId: String): PairingErrorCaption? {
private fun getPairingErrorMessage(contactId: String, isValidId: Boolean): PairingErrorCaption? {
val contact = contacts.find { it.contactVeraId == contactId }
return when {
!isValidId -> PairingErrorCaption(R.string.pair_request_invalid_id)
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
}
}

private companion object {
private const val CHECK_ID_DEBOUNCE_DELAY_MS = 1_000L
private val CORRECT_ID_REGEX = """^([^@]+@)?\p{L}{1,63}(\.\p{L}{1,63})+$""".toRegex()
}

@IntDef(NEW_CONTACT, EDIT_CONTACT)
annotation class Type {
companion object {
Expand All @@ -160,9 +214,11 @@ data class PairWithOthersUiState(
val manageContactTexts: ManageContactTexts,
val veraId: String = "",
val alias: String? = null,
val isActionButtonEnabled: Boolean = false,
val isSentRequestAgainHintVisible: Boolean = false,
val isVeraIdInputEnabled: Boolean = true,
val pairingErrorCaption: PairingErrorCaption? = null,
val showRequestSentScreen: Boolean = false,
)

@Immutable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,42 @@ import tech.relaycorp.letro.awala.message.MessageType
import tech.relaycorp.letro.contacts.model.Contact
import tech.relaycorp.letro.contacts.model.ContactPairingStatus
import tech.relaycorp.letro.main.MainViewModel
import tech.relaycorp.letro.storage.Preferences
import javax.inject.Inject

interface ContactsRepository {
val isPairedContactsExist: StateFlow<Boolean>
val contactsState: StateFlow<ContactsState>
fun getContacts(ownerVeraId: String): Flow<List<Contact>>
fun getContactById(id: Long): Contact?

fun addNewContact(contact: Contact)
fun deleteContact(contact: Contact)
fun updateContact(contact: Contact)
fun saveRequestWasOnceSent()
}

class ContactsRepositoryImpl @Inject constructor(
private val contactsDao: ContactsDao,
private val accountRepository: AccountRepository,
private val awalaManager: AwalaManager,
private val preferences: Preferences,
) : ContactsRepository {

private val scope = CoroutineScope(Dispatchers.IO)
private val contacts = MutableStateFlow<List<Contact>>(emptyList())

private var currentAccount: Account? = null
private val _isPairedContactsExist: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isPairedContactsExist: StateFlow<Boolean>
get() = _isPairedContactsExist
private val _contactsState: MutableStateFlow<ContactsState> = MutableStateFlow(ContactsState())
override val contactsState: StateFlow<ContactsState>
get() = _contactsState

init {
scope.launch {
contactsDao.getAll().collect {
contacts.emit(it)
startCollectAccountFlow()
if (currentAccount != null) {
updatePairedContactExist(currentAccount)
updateContactsState(currentAccount)
}
}
}
Expand Down Expand Up @@ -108,31 +111,54 @@ class ContactsRepositoryImpl @Inject constructor(
}
}

override fun saveRequestWasOnceSent() {
val currentAccount = currentAccount ?: return
scope.launch {
preferences.putBoolean(getContactRequestHasEverBeenSentKey(currentAccount.veraId), true)
updateContactsState(currentAccount)
}
}

private fun startCollectAccountFlow() {
scope.launch {
accountRepository.currentAccount.collect {
currentAccount = it
updatePairedContactExist(it)
updateContactsState(it)
}
}
}

private suspend fun updatePairedContactExist(account: Account?) {
private suspend fun updateContactsState(account: Account?) {
Log.d(MainViewModel.TAG, "ContactsRepository.emit(pairedContactExist)")
account ?: run {
_isPairedContactsExist.emit(false)
_contactsState.emit(ContactsState())
return
}
_isPairedContactsExist.emit(
contacts
.value
.any {
it.ownerVeraId == account.veraId && it.status == ContactPairingStatus.COMPLETED
},
val isPairedContactExist = contacts
.value
.any {
it.ownerVeraId == account.veraId && it.status == ContactPairingStatus.COMPLETED
}
val isPairRequestWasEverSent = preferences.getBoolean(getContactRequestHasEverBeenSentKey(account.veraId), false)
_contactsState.emit(
ContactsState(
isPairedContactExist = isPairedContactExist,
isPairRequestWasEverSent = isPairRequestWasEverSent,
),
)
}

private fun getContactRequestHasEverBeenSentKey(
veraId: String,
) = "${KEY_CONTACT_REQUEST_HAS_EVER_BEEN_SENT_PREFIX}$veraId"

private companion object {
private const val TAG = "ContactsRepository"
private const val KEY_CONTACT_REQUEST_HAS_EVER_BEEN_SENT_PREFIX = "contact_request_has_ever_been_sent_"
}
}

data class ContactsState(
val isPairedContactExist: Boolean = false,
val isPairRequestWasEverSent: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ fun ContactsScreen(
Box {
if (editBottomSheet.isShown && editBottomSheet.contact != null) {
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = { viewModel.onEditBottomSheetDismissed() },
) {
EditContactBottomSheet(
Expand Down Expand Up @@ -147,11 +148,12 @@ private fun EditContactBottomSheet(
onDeleteClick: () -> Unit,
) {
Column(
modifier = Modifier.padding(
PaddingValues(
bottom = 44.dp,
modifier = Modifier
.padding(
PaddingValues(
bottom = 44.dp,
),
),
),
) {
Text(
text = title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import tech.relaycorp.letro.R
import tech.relaycorp.letro.contacts.ManageContactViewModel
import tech.relaycorp.letro.contacts.PairWithOthersUiState
import tech.relaycorp.letro.onboarding.actionTaking.ActionTakingScreen
import tech.relaycorp.letro.onboarding.actionTaking.ActionTakingScreenUIStateModel
import tech.relaycorp.letro.ui.common.LetroButtonMaxWidthFilled
import tech.relaycorp.letro.ui.common.LetroInfoView
import tech.relaycorp.letro.ui.common.LetroOutlinedTextField
Expand All @@ -32,17 +35,47 @@ import tech.relaycorp.letro.ui.theme.LetroTheme
@Composable
fun ManageContactScreen(
onBackClick: () -> Unit,
onActionCompleted: (String) -> Unit,
onEditContactCompleted: (String) -> Unit,
viewModel: ManageContactViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()

LaunchedEffect(Unit) {
viewModel.onActionCompleted.collect {
onActionCompleted(it)
viewModel.onEditContactCompleted.collect {
onEditContactCompleted(it)
}
}

LaunchedEffect(Unit) {
viewModel.goBackSignal.collect {
onBackClick()
}
}

if (!uiState.showRequestSentScreen) {
ManageContactView(
onBackClick,
uiState,
viewModel,
)
} else {
ActionTakingScreen(
actionTakingScreenUIStateModel = ActionTakingScreenUIStateModel.PairingRequestSent(
boldPartOfMessage = uiState.veraId,
onGotItClicked = {
viewModel.onGotItClick()
},
),
)
}
}

@Composable
private fun ManageContactView(
onBackClick: () -> Unit,
uiState: PairWithOthersUiState,
viewModel: ManageContactViewModel,
) {
val errorCaption = uiState.pairingErrorCaption

Column(
Expand Down Expand Up @@ -119,8 +152,8 @@ fun ManageContactScreen(
)
LetroButtonMaxWidthFilled(
text = stringResource(id = uiState.manageContactTexts.button),
onClick = { viewModel.onActionButtonClick() },
isEnabled = errorCaption == null,
onClick = { viewModel.onUpdateContactButtonClick() },
isEnabled = uiState.isActionButtonEnabled,
)
}
}
Expand All @@ -131,7 +164,7 @@ private fun UseExistingAccountPreview() {
LetroTheme {
ManageContactScreen(
onBackClick = {},
onActionCompleted = {},
onEditContactCompleted = {},
viewModel = hiltViewModel(),
)
}
Expand Down
Loading

0 comments on commit da8e89c

Please sign in to comment.