From 424e3017cf7af40748bec6846d6472565efbbf25 Mon Sep 17 00:00:00 2001 From: Kishan Kumar Date: Wed, 4 Dec 2024 21:23:19 +0530 Subject: [PATCH] NTP-17778 | Add batch fetching of contacts (#13960) --- .../ui/compose/ReferralContactBottomSheet.kt | 212 ++++++++++++++++-- .../rr/referral/utils/ReferralContactsUtil.kt | 38 ++++ .../vm/ReferralContactsBottomSheetVM.kt | 27 ++- 3 files changed, 252 insertions(+), 25 deletions(-) diff --git a/android/navi-rr/src/main/java/com/navi/rr/referral/ui/compose/ReferralContactBottomSheet.kt b/android/navi-rr/src/main/java/com/navi/rr/referral/ui/compose/ReferralContactBottomSheet.kt index 5a24fc7eb9..b971ec308b 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/referral/ui/compose/ReferralContactBottomSheet.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/referral/ui/compose/ReferralContactBottomSheet.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -41,6 +43,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -54,6 +57,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import co.hyperverge.hypersnapsdk.R as HyperSnapR import com.navi.common.R as CommonR +import com.navi.common.commoncomposables.utils.loadingShimmerEffect import com.navi.common.utils.shareutil.ContentSharingManager import com.navi.design.R as DesignR import com.navi.design.font.FontWeightEnum @@ -64,6 +68,7 @@ import com.navi.naviwidgets.R as WidgetsR import com.navi.naviwidgets.models.ShareData import com.navi.rr.R import com.navi.rr.referral.models.ReferralContact +import com.navi.rr.referral.vm.ContactState import com.navi.rr.referral.vm.ReferralContactsBottomSheetVM import com.navi.rr.utils.NaviRRAnalytics import com.navi.rr.utils.constants.Constants @@ -87,10 +92,168 @@ fun ReferralContactBottomSheetRenderer( ) { val searchText by viewModel.searchText.collectAsState() val contacts by viewModel.listOfContacts.collectAsState() + val contactState by viewModel.contactState.collectAsState() var isValid by remember { mutableStateOf(true) } var isClickable by remember { mutableStateOf(false) } val context = LocalContext.current val screenHeight = (LocalConfiguration.current.screenHeightDp / 1.8).dp + + fun LazyListScope.NoContactFoundRenderer(contacts: List, searchText: String) { + if (contacts.isEmpty()) { + if (searchText.matches(Regex(NUMBER_REGEX_WITHOUT_COUNTRY_CODE))) { + item { + Contacts( + name = searchText, + phoneNum = "", + clickable = isClickable, + isValid = isValid, + isEmptyList = true, + onClick = { + if (isClickable) { + handleContactClickOnManualSearch( + searchText, + context, + message, + imageUrl, + shareableLink + ) + } + } + ) + } + isClickable = false + isValid = true + if (searchText.length == CONTACT_NUMBER_LENGTH) { + isClickable = true + isValid = true + } else if (searchText.length > CONTACT_NUMBER_LENGTH) { + isClickable = false + isValid = false + } + } else if (searchText.matches(Regex(NUMBER_REGEX_WITH_ZERO))) { + item { + Contacts( + name = searchText, + phoneNum = "", + clickable = isClickable, + isValid = isValid, + isEmptyList = true, + onClick = { + if (isClickable) { + handleContactClickOnManualSearch( + searchText, + context, + message, + imageUrl, + shareableLink + ) + } + } + ) + } + isClickable = false + isValid = true + if (searchText.length == CONTACT_NUMBER_LENGTH_WITH_ZERO) { + isClickable = true + isValid = true + } else if (searchText.length > CONTACT_NUMBER_LENGTH_WITH_ZERO) { + isClickable = false + isValid = false + } + } else if (searchText.matches(Regex(NUMBER_REGEX_WITH_COUNTRY_CODE))) { + item { + Contacts( + name = searchText, + phoneNum = "", + clickable = isClickable, + isValid = isValid, + isEmptyList = true, + onClick = { + if (isClickable) { + handleContactClickOnManualSearch( + searchText, + context, + message, + imageUrl, + shareableLink + ) + } + } + ) + } + isClickable = false + isValid = true + if (searchText.length == CONTACT_NUMBER_LENGTH_WITH_COUNTRY_CODE) { + isClickable = true + isValid = true + } else if (searchText.length > CONTACT_NUMBER_LENGTH_WITH_COUNTRY_CODE) { + isClickable = false + isValid = false + } + } else { + item { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.no_contact_found_image), + contentDescription = "", + modifier = Modifier.size(168.dp) + ) + Text( + text = stringResource(id = R.string.contacts_book_empty_state_title), + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 16.dp, start = 54.dp, end = 54.dp), + fontSize = 12.sp, + color = colorResource(id = DesignR.color.color_6B6B6B), + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + letterSpacing = 0.01.sp, + lineHeight = 16.sp + ) + } + } + } + } + } + + @Composable + fun ContactShimmerItem() { + Row( + modifier = Modifier.padding(vertical = 12.dp).fillMaxWidth().height(40.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = + Modifier.padding(start = 16.dp) + .size(32.dp) + .clip(shape = CircleShape) + .loadingShimmerEffect() + ) + Column( + modifier = Modifier.wrapContentHeight().padding(start = 8.dp), + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = + Modifier.height(20.dp) + .width(248.dp) + .clip(RoundedCornerShape(2.dp)) + .loadingShimmerEffect() + ) + Spacer(modifier = Modifier.height(2.dp)) + Box( + modifier = + Modifier.height(20.dp) + .width(66.dp) + .clip(RoundedCornerShape(2.dp)) + .loadingShimmerEffect() + ) + } + } + } + Column(Modifier.fillMaxWidth().background(White).height(screenHeight)) { Text( text = stringResource(id = R.string.contacts_book_bottom_sheet_title), @@ -124,29 +287,38 @@ fun ReferralContactBottomSheetRenderer( ) } LazyColumn(modifier = Modifier.fillMaxWidth().weight(0.5f).padding(top = 20.dp)) { - if (contacts.isEmpty().not()) { - items(contacts) { contact -> - Contacts( - name = contact.name.orEmpty(), - phoneNum = contact.number.toString(), - initials = getInitialsFromName(contact.name.orEmpty()), - clickable = true, - onClick = { - contact.number?.let { - handleContactClick( - contact.isWhatsappContact ?: false, - contact = contact, - context, - message = message, - imageUrl = imageUrl, - shareLink = shareableLink - ) + when (contactState) { + ContactState.Loading -> { + items(10) { ContactShimmerItem() } + return@LazyColumn + } + ContactState.NotFound -> { + NoContactFoundRenderer(contacts, searchText) + return@LazyColumn + } + ContactState.Success -> { + items(contacts) { contact -> + Contacts( + name = contact.name.orEmpty(), + phoneNum = contact.number.toString(), + initials = getInitialsFromName(contact.name.orEmpty()), + clickable = true, + onClick = { + contact.number?.let { + handleContactClick( + contact.isWhatsappContact ?: false, + contact = contact, + context, + message = message, + imageUrl = imageUrl, + shareLink = shareableLink + ) + } } - } - ) + ) + } } } - if (contacts.isEmpty()) { if (searchText.matches(Regex(NUMBER_REGEX_WITHOUT_COUNTRY_CODE))) { item { diff --git a/android/navi-rr/src/main/java/com/navi/rr/referral/utils/ReferralContactsUtil.kt b/android/navi-rr/src/main/java/com/navi/rr/referral/utils/ReferralContactsUtil.kt index d70c915efa..937c0ef215 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/referral/utils/ReferralContactsUtil.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/referral/utils/ReferralContactsUtil.kt @@ -20,6 +20,8 @@ import com.navi.rr.utils.ext.removeSpaces import com.navi.rr.utils.removeCodeFromPhoneNumber import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext class ReferralContactsUtil @Inject constructor() { @@ -47,6 +49,42 @@ class ReferralContactsUtil @Inject constructor() { } } + fun getContactListAsFlow(batchSize: Int = 200): Flow> = flow { + var allContactsCursor: Cursor? = null + try { + val batchContactsList = mutableListOf() + allContactsCursor = + executeQueryToReadAllContactsWithThumbnails( + AppServiceManager.application.applicationContext + ) + + if (allContactsCursor == null) { + return@flow + } + + allContactsCursor.run { + if (!moveToFirst()) { + return@flow + } + do { + readContact(batchContactsList) + if (batchContactsList.size == batchSize) { + emit(batchContactsList.distinctBy { it.number }) + batchContactsList.clear() + } + } while (moveToNext()) + } + if (batchContactsList.isNotEmpty()) { + emit(batchContactsList.distinctBy { it.number }) + } + } catch (e: Exception) { + e.log() + return@flow + } finally { + allContactsCursor?.close() + } + } + private fun executeQueryToReadAllContactsWithThumbnails(applicationContext: Context): Cursor? { return applicationContext.contentResolver.query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, diff --git a/android/navi-rr/src/main/java/com/navi/rr/referral/vm/ReferralContactsBottomSheetVM.kt b/android/navi-rr/src/main/java/com/navi/rr/referral/vm/ReferralContactsBottomSheetVM.kt index 233f8a4f8b..207f0811ff 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/referral/vm/ReferralContactsBottomSheetVM.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/referral/vm/ReferralContactsBottomSheetVM.kt @@ -14,7 +14,6 @@ import com.navi.rr.referral.utils.ReferralContactsUtil import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow @@ -29,6 +28,9 @@ class ReferralContactsBottomSheetVM constructor(private val referralContactsUtil: ReferralContactsUtil) : BaseVM(isExceptionNeedToShow = false) { + private val _contactState = MutableStateFlow(ContactState.Loading) + val contactState = _contactState.asStateFlow() + private val _searchText = MutableStateFlow("") val searchText = _searchText.asStateFlow() @@ -54,10 +56,25 @@ constructor(private val referralContactsUtil: ReferralContactsUtil) : private fun fetchContacts() { viewModelScope.launch(Dispatchers.IO) { - var contactList = mutableListOf() - val contactTask = async { contactList = referralContactsUtil.getContactList() } - contactTask.await() - _listOfContacts.update { contactList } + referralContactsUtil.getContactListAsFlow().collect { newContacts -> + _listOfContacts.update { currentList -> + (currentList + newContacts).distinctBy { it.number } + } + _contactState.value = + if (_listOfContacts.value.isEmpty()) { + ContactState.NotFound + } else { + ContactState.Success + } + } } } } + +sealed interface ContactState { + data object Success : ContactState + + data object NotFound : ContactState + + data object Loading : ContactState +}