diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bb3f1a69..3b6101cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,7 @@ plugins { android { defaultConfig { - val buildVersion = 177 + val buildVersion = 178 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt index 5e433533..49d2f7f8 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt @@ -1,6 +1,7 @@ package com.crisiscleanup.feature.organizationmanage import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap @@ -16,6 +17,7 @@ import com.crisiscleanup.core.common.log.CrisisCleanupLoggers.Onboarding import com.crisiscleanup.core.common.log.Logger import com.crisiscleanup.core.common.network.CrisisCleanupDispatchers.IO import com.crisiscleanup.core.common.network.Dispatcher +import com.crisiscleanup.core.common.throttleLatest import com.crisiscleanup.core.data.IncidentSelectManager import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.OrgVolunteerRepository @@ -33,7 +35,6 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -47,7 +48,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds @HiltViewModel class InviteTeammateViewModel @Inject constructor( @@ -65,9 +65,9 @@ class InviteTeammateViewModel @Inject constructor( ) : ViewModel() { private val inviteUrl = "${settingsProvider.baseUrl}/mobile_app_user_invite" - val isValidatingAccount = MutableStateFlow(false) + private val isValidatingAccount = MutableStateFlow(false) - val accountData = accountDataRepository.accountData + private val accountData = accountDataRepository.accountData .shareIn( scope = viewModelScope, replay = 1, @@ -84,7 +84,7 @@ class InviteTeammateViewModel @Inject constructor( val inviteToAnotherOrg = MutableStateFlow(false) private val affiliateOrganizationIds = MutableStateFlow?>(null) - val selectedOtherOrg = MutableStateFlow(OrganizationIdName(0, "")) + private val selectedOtherOrg = MutableStateFlow(OrganizationIdName(0, "")) val organizationNameQuery = MutableStateFlow("") private val isSearchingLocalOrganizations = MutableStateFlow(false) private val isSearchingNetworkOrganizations = MutableStateFlow(false) @@ -163,7 +163,7 @@ class InviteTeammateViewModel @Inject constructor( ) private val qFlow = organizationNameQuery - .debounce(0.3.seconds) + .throttleLatest(300) .map(String::trim) .shareIn( scope = viewModelScope, @@ -200,7 +200,7 @@ class InviteTeammateViewModel @Inject constructor( val incidents = MutableStateFlow(emptyList()) val incidentLookup = MutableStateFlow(emptyMap()) - var selectedIncidentId by mutableStateOf(EmptyIncident.id) + var selectedIncidentId by mutableLongStateOf(EmptyIncident.id) // TODO Size QR codes relative to min screen dimension private val qrCodeSize = 512 + 256 @@ -479,6 +479,23 @@ class InviteTeammateViewModel @Inject constructor( joinMyOrgInvite.value = it } .launchIn(viewModelScope) + + inviteOrgState + .throttleLatest(300) + .onEach { + clearErrors() + } + .launchIn(viewModelScope) + } + + private fun clearErrors() { + emailAddressError = "" + phoneNumberError = "" + firstNameError = "" + lastNameError = "" + selectedIncidentError = "" + + sendInviteErrorMessage.value = "" } private fun makeInviteUrl(userId: Long, invite: JoinOrgInvite): String { @@ -528,11 +545,14 @@ class InviteTeammateViewModel @Inject constructor( break } } - if (selectedOtherOrg.value.id != matchingOrg.id) { + val selectedOrgId = selectedOtherOrg.value.id + if ((selectedOrgId == 0L || selectedOrgId != matchingOrg.id) && + matchingOrg.id == 0L + ) { matchingOrg = OrganizationIdName(0, q) - selectedOtherOrg.value = matchingOrg - organizationNameQuery.value = matchingOrg.name } + selectedOtherOrg.value = matchingOrg + organizationNameQuery.value = matchingOrg.name } private suspend fun inviteToOrgOrAffiliate( @@ -579,7 +599,7 @@ class InviteTeammateViewModel @Inject constructor( sendInvites() } catch (e: Exception) { sendInviteErrorMessage.value = - translator("~~Invites are broken. Sorry for the inconvenience. Please try again later.") + translator("~~Invites are not working at the moment. Please try again later.") logger.logException(e) } finally { isSendingInvite.value = false @@ -649,8 +669,9 @@ class InviteTeammateViewModel @Inject constructor( } var isSentToOrgOrAffiliate = false + var isInviteSuccessful = false if (inviteToAnotherOrg.value) { - if (inviteState.new && emailAddresses.size == 1) { + if (inviteState.new) { val organizationName = q.trim() val emailContact = emailAddresses[0] val isRegisterNewOrganization = orgVolunteerRepository.createOrganization( @@ -672,20 +693,29 @@ class InviteTeammateViewModel @Inject constructor( text = translator("registerOrg.we_will_finalize_registration") .replace("{email}", emailContact), ) + + isInviteSuccessful = true } } else if (inviteState.affiliate) { isSentToOrgOrAffiliate = inviteToOrgOrAffiliate(emailAddresses, selectedOtherOrg.value.id) + isInviteSuccessful = isSentToOrgOrAffiliate } else if (inviteState.nonAffiliate) { // TODO Finish when API supports a corresponding endpoint } } else { isSentToOrgOrAffiliate = inviteToOrgOrAffiliate(emailAddresses) + isInviteSuccessful = isSentToOrgOrAffiliate } if (isSentToOrgOrAffiliate) { onInviteSentToOrgOrAffiliate(emailAddresses) } + + if (!isInviteSuccessful) { + sendInviteErrorMessage.value = + translator("~~Invites are not working at the moment. Please try again later.") + } } } diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt index 9390ddb1..8487e5c4 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.onGloballyPositioned @@ -158,6 +159,10 @@ fun InviteTeammateContent( val onDismissOrgOptions = remember(viewModel) { { dismissOrganizationQuery = organizationNameQuery + } + } + val onQueryFocusOut = remember(viewModel) { + { viewModel.onOrgQueryClose() } } @@ -233,6 +238,7 @@ fun InviteTeammateContent( organizations = matchingOrganizations, onOrgSelect = onOrgSelect, onDismissDropdown = onDismissOrgOptions, + onFocusOut = onQueryFocusOut, ) if (inviteToAnotherOrg) { @@ -358,6 +364,7 @@ private fun OrgQueryInput( organizations: List, onOrgSelect: (OrganizationIdName) -> Unit, onDismissDropdown: () -> Unit, + onFocusOut: () -> Unit, ) { val t = LocalAppTranslator.current @@ -369,6 +376,11 @@ private fun OrgQueryInput( .fillMaxWidth() .onGloballyPositioned { contentSize = it.size.toSize() + } + .onFocusChanged { + if (!it.isFocused) { + onFocusOut() + } }, label = t("profileOrg.organization_name"), value = organizationNameQuery,