Chat screen refactor for latency reduction from 5 sec to 1 sec (#8752)

This commit is contained in:
Girish Suragani
2023-11-27 19:37:11 +05:30
committed by GitHub
parent bef562d2f0
commit 033289fc39
14 changed files with 2276 additions and 9 deletions

View File

@@ -257,7 +257,7 @@ object Constants {
const val EXPIRY_TIME_IN_MIN = "expiryTimeInMinutes"
const val NOTIFICATION_SENT_TIME = "notificationSentTimeInUTC"
const val SOURCE = "source"
const val SHOW_CSAT = "showCsatFlag"
const val SHOW_CSAT = "shouldShowCsat"
const val SHOW_RESOLUTION = "showResolutionStatusWidgetFlag"
const val CONVERSATION_ID = "conversationId"
const val SKIP_TUTORIAL_SCREEN = "SKIP_TUTORIAL_SCREEN"

View File

@@ -18,6 +18,7 @@ import com.navi.chat.ui.activities.NaviTicketViewActivity
import com.navi.chat.ui.activities.SupportScreenActivity
import com.navi.chat.ui.fragments.ChatAttachmentBottomSheet
import com.navi.chat.ui.fragments.NaviChatFragment
import com.navi.chat.ui.fragments.NaviChatFragmentV2
import dagger.BindsInstance
import dagger.Component
@@ -34,6 +35,7 @@ interface NaviChatComponent {
}
fun inject(naviChatFragment: NaviChatFragment)
fun inject(naviChatFragmentV2: NaviChatFragmentV2)
fun inject(naviChatViewImageActivity: NaviChatViewImageActivity)
fun inject(chatAttachmentBottomSheet: ChatAttachmentBottomSheet)

View File

@@ -0,0 +1,14 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.chat.models.response
import com.google.gson.annotations.SerializedName
data class ChatConversationIdResponse(
@SerializedName("conversation_id") val conversationId: String? = null
)

View File

@@ -0,0 +1,27 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.chat.models.response
import com.google.firebase.Timestamp
import com.google.gson.annotations.SerializedName
import com.navi.naviwidgets.models.NaviChatWidget
data class NaviChatConversationResponse(
@SerializedName("channelInfo") val channelInfo: ChatChannelInfo? = null,
@SerializedName("lastMessageTimestamp") val lastMessageTimeStamp: Timestamp? = null,
@SerializedName("content") val listOfMessages: List<NaviChatWidget>? = null,
@SerializedName("showTextbox") val showFreeText: Boolean? = false,
@SerializedName("last") val isLast: Boolean? = true
)
data class ChatChannelInfo(
@SerializedName("readPath") val readPath: String? = null,
@SerializedName("writePath") val writePath: String? = null,
@SerializedName("eventPath") val eventsPath: String? = null,
@SerializedName("chatControllerPath") val controllerWidgetPath: String? = null
)

View File

@@ -11,8 +11,10 @@ import com.navi.chat.di.scopes.NaviChatScope
import com.navi.chat.models.ChatConfigResponse
import com.navi.chat.models.request.NaviChatInitiateRequest
import com.navi.chat.models.request.NaviMarkConversationsReadRequest
import com.navi.chat.models.response.ChatConversationIdResponse
import com.navi.chat.models.response.HelpCenterResponse
import com.navi.chat.models.response.NaviChatActivityRedirectionValidationResponse
import com.navi.chat.models.response.NaviChatConversationResponse
import com.navi.chat.models.response.NaviChatHistoryResponse
import com.navi.chat.models.response.NaviChatHistoryResponseByConvId
import com.navi.chat.models.response.NaviChatInitiateResponse
@@ -29,6 +31,17 @@ import retrofit2.http.*
@NaviChatScope
interface NaviChatApiInterface {
@POST("/crm/api/v2/customersupport/conversations/initiate")
suspend fun fetchConversationId(
@Body naviChatInitiateRequest: NaviChatInitiateRequest
): Response<GenericResponse<ChatConversationIdResponse>>
@GET("/crm/api/v2/chat/history/{conversationId}")
suspend fun fetchChatConversations(
@Path("conversationId") conversationId: String,
@Query("page") page: Int
): Response<GenericResponse<NaviChatConversationResponse>>
@POST("/crm/api/v1/customersupport/conversations/initiate")
suspend fun initiateChat(
@Body naviChatInitiateRequest: NaviChatInitiateRequest

View File

@@ -0,0 +1,35 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.chat.paging
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.navi.chat.utils.PAGE_SIZE
abstract class PaginationScrollV2Listener(private val layoutManager: LinearLayoutManager) :
RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val totalItemCount = layoutManager.itemCount
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
if (!isLoading() && canLoadMore()) {
if (totalItemCount - 1 == lastVisibleItem) {
loadMoreItems()
}
}
}
abstract fun loadMoreItems()
abstract fun canLoadMore(): Boolean
abstract fun isLoading(): Boolean
}

View File

@@ -26,6 +26,35 @@ import java.util.UUID
class NaviChatRepository @Inject constructor(private val chatApiInterface: NaviChatApiInterface) :
ResponseCallback(), AwsRepositoryInterface {
suspend fun fetchConversationID(
source: String,
sourceId: String,
userName: String,
metaData: HashMap<String?, String?>? = null
) =
apiResponseCallback(
chatApiInterface.fetchConversationId(
naviChatInitiateRequest =
NaviChatInitiateRequest(
source = source,
sourceId = sourceId,
userName = userName,
metaData = metaData
)
)
)
suspend fun fetchChatConversations(
conversationId: String,
pageNo: Int
) =
apiResponseCallback(
chatApiInterface.fetchChatConversations(
conversationId = conversationId,
page = pageNo
)
)
suspend fun initiateChat(
source: String,
sourceId: String,
@@ -41,7 +70,7 @@ class NaviChatRepository @Inject constructor(private val chatApiInterface: NaviC
userName = userName,
metaData = metaData
)
),
)
)
suspend fun getHelpCenterDetails() =
@@ -121,6 +150,6 @@ class NaviChatRepository @Inject constructor(private val chatApiInterface: NaviC
readTimestamp = readTimestamp,
deliveredTimestamp = deliveredTimestamp
)
),
)
)
}

View File

@@ -21,6 +21,7 @@ import com.navi.chat.db.utils.getFileName
import com.navi.chat.interfaces.AttachmentOptionClickListener
import com.navi.chat.models.NaviChatSystemLocalData
import com.navi.chat.ui.fragments.NaviChatFragment
import com.navi.chat.ui.fragments.NaviChatFragmentV2
import com.navi.chat.utils.CONVERSATION_ID_PARAM
import com.navi.chat.utils.DOCUMENT_ITEM
import com.navi.chat.utils.FILE_SIZES
@@ -31,12 +32,16 @@ import com.navi.chat.utils.NaviChatAnalytics
import com.navi.chat.utils.NaviChatAnalytics.Companion.CHAT_SCREEN_CLOSES
import com.navi.chat.utils.URI_LIST
import com.navi.chat.viewmodels.AttachmentPickerViewModel
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
class NaviChatActivity : ChatBaseActivity() , AttachmentOptionClickListener{
private lateinit var binding: ActivityNaviChatBinding
private val currentScreenTag: String = NaviChatFragment.TAG
private val chatScreenTag: String = NaviChatFragment.TAG
private val chatV2ScreenTag: String = NaviChatFragmentV2.TAG
private var chatFragment: NaviChatFragment? = null
private var chatV2Fragment: NaviChatFragmentV2? = null
private val attachmentPickerViewModel by lazy {
ViewModelProvider(this)[AttachmentPickerViewModel::class.java]
}
@@ -58,7 +63,7 @@ class NaviChatActivity : ChatBaseActivity() , AttachmentOptionClickListener{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_navi_chat)
setContentView(binding.root)
addFragment()
checkFirebaseAndAddFragment()
isChatActivityVisible = true
}
@@ -72,13 +77,21 @@ class NaviChatActivity : ChatBaseActivity() , AttachmentOptionClickListener{
chatFragment?.scrollChatToLatestMessageReceived()
}
private fun addFragment() {
private fun checkFirebaseAndAddFragment() {
if (FirebaseRemoteConfigHelper.getBoolean(FirebaseRemoteConfigHelper.ENABLE_CHAT_V2)) {
addChatV2Fragment()
} else {
addChatFragment()
}
}
private fun addChatFragment() {
val fragment =
supportFragmentManager.findFragmentByTag(currentScreenTag) ?: getChatFragment()
supportFragmentManager.findFragmentByTag(chatScreenTag) ?: getChatFragment()
val fragmentTransaction = supportFragmentManager.beginTransaction()
chatFragment = fragment as? NaviChatFragment
if (!supportFragmentManager.isStateSaved && !supportFragmentManager.isDestroyed) {
fragmentTransaction.replace(R.id.fcvLayout, fragment, currentScreenTag)
fragmentTransaction.replace(R.id.fcvLayout, fragment, chatScreenTag)
fragmentTransaction.commit()
}
}
@@ -96,6 +109,30 @@ class NaviChatActivity : ChatBaseActivity() , AttachmentOptionClickListener{
arguments = bundle
}
private fun addChatV2Fragment() {
val fragment =
supportFragmentManager.findFragmentByTag(chatV2ScreenTag) ?: getChatV2Fragment()
val fragmentTransaction = supportFragmentManager.beginTransaction()
chatV2Fragment = fragment as? NaviChatFragmentV2
if (!supportFragmentManager.isStateSaved && !supportFragmentManager.isDestroyed) {
fragmentTransaction.replace(R.id.fcvLayout, fragment, chatScreenTag)
fragmentTransaction.commit()
}
}
private fun getChatV2Fragment() =
NaviChatFragmentV2().apply {
val bundle = Bundle()
bundle.putParcelable(
NAVI_CHAT_SYSTEM_LOCAL_DATA,
intent.extras?.getParcelable<NaviChatSystemLocalData>(NAVI_CHAT_SYSTEM_LOCAL_DATA)
)
intent.extras?.getString(CONVERSATION_ID_PARAM)?.let {
bundle.putString(CONVERSATION_ID_PARAM, it)
}
arguments = bundle
}
override fun onBackPressed() {
crmEventTracker.sendEvent(CHAT_SCREEN_CLOSES)
super.onBackPressed()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,372 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.chat.viewmodels
import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.firebase.Timestamp
import com.navi.base.awsupload.AWSFileType
import com.navi.base.model.LineItem
import com.navi.base.utils.isNotNull
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.base.utils.orFalse
import com.navi.chat.base.ChatBaseVM
import com.navi.chat.db.utils.getDisplayableMessages
import com.navi.chat.di.scopes.NaviChatScope
import com.navi.chat.models.ChatActivityRedirectionConstructedResponse
import com.navi.chat.models.ChatConfigConstructedResponse
import com.navi.chat.models.ChatConfigResponse
import com.navi.chat.models.response.ChatChannelInfo
import com.navi.chat.models.response.ChatConversationIdResponse
import com.navi.chat.repositories.NaviChatRepository
import com.navi.chat.utils.CONVERSATION_ID_PARAM
import com.navi.chat.utils.NODE_UUID_PARAM
import com.navi.common.awsupload.FileDownloadManager
import com.navi.common.awsupload.model.DownloadTask
import com.navi.naviwidgets.models.NaviChatWidget
import com.navi.naviwidgets.models.response.NaviChatControllerWidget
import com.navi.naviwidgets.models.response.NaviChatCsatRatingWidget
import com.navi.naviwidgets.models.response.NaviChatResolutionStatusWidget
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@SuppressLint("StaticFieldLeak")
@NaviChatScope
class NaviChatV2ViewModel
@Inject
constructor(private val naviChatRepository: NaviChatRepository, private val context: Context) :
ChatBaseVM(), FileDownloadManager.StatusCallBack {
private val _naviChatInitResponse = MutableLiveData<ChatConversationIdResponse>()
val naviChatInitResponse: LiveData<ChatConversationIdResponse>
get() = _naviChatInitResponse
private val _chatHistoryMessages = MutableLiveData<Pair<Int, List<NaviChatWidget>>>()
val chatHistoryMessages: LiveData<Pair<Int, List<NaviChatWidget>>>
get() = _chatHistoryMessages
private val _chatChannelInfo = MutableLiveData<ChatChannelInfo>()
val chatChannelInfo: LiveData<ChatChannelInfo>
get() = _chatChannelInfo
private val _showFreeText = MutableLiveData<Boolean>()
val showFreeText: LiveData<Boolean>
get() = _showFreeText
private val _naviChatConfigResponse = MutableLiveData<ChatConfigConstructedResponse?>()
val naviChatConfigResponse: LiveData<ChatConfigConstructedResponse?>
get() = _naviChatConfigResponse
private var _downloadSuccessSharedFlow = MutableSharedFlow<DownloadTask>(0)
val downloadSuccessSharedFlow = _downloadSuccessSharedFlow.asSharedFlow()
private var _downloadFailureSharedFlow = MutableSharedFlow<DownloadTask>(0)
val downloadFailureSharedFlow = _downloadFailureSharedFlow.asSharedFlow()
private val _csatRatingWidget = MutableLiveData<NaviChatCsatRatingWidget?>()
val csatRatingWidget: LiveData<NaviChatCsatRatingWidget?>
get() = _csatRatingWidget
private val _chatResolutionWidget = MutableLiveData<NaviChatResolutionStatusWidget?>()
val chatResolutionWidget: LiveData<NaviChatResolutionStatusWidget?>
get() = _chatResolutionWidget
private val _activityRedirectionValidationResponse = MutableSharedFlow<ChatActivityRedirectionConstructedResponse>(0)
val activityRedirectionValidationResponse = _activityRedirectionValidationResponse.asSharedFlow()
private val _chatControllerWidget = MutableLiveData<NaviChatControllerWidget?>()
val chatControllerWidget: LiveData<NaviChatControllerWidget?>
get() = _chatControllerWidget
private var isCsatFeedbackShown: Boolean = false
private var isChatResolutionShown: Boolean = false
private var isTimerEnabledForMessage: Boolean = false
private var isTimerSet: Boolean = false
private var timerDuration: Long = 10000
private var showRemainingCharactersView: Boolean = false
private var setCharLimit: Int = 1000
private var pageCount = 0
var latestMessageTimeStamp: Timestamp? = null
var isPaginationLoading: Boolean = false
var canLoadMore: Boolean = true
private val downloadManager: FileDownloadManager by lazy {
FileDownloadManager(naviChatRepository, this, context, "/chat/", true)
}
fun fetchConversationID(
source: String,
sourceId: String,
userName: String,
metaData: HashMap<String?, String?>? = null
) {
viewModelScope.launch {
val response =
naviChatRepository.fetchConversationID(
source = source,
sourceId = sourceId,
userName = userName,
metaData = metaData
)
if (response.error == null && response.errors.isNullOrEmpty() && response.data != null) {
_naviChatInitResponse.value = response.data!!
} else {
setError(
errors = response.errors,
errorTag = response.errors?.firstOrNull()?.code,
cancelable = false
)
}
}
}
fun fetchChatConversations(
conversationId: String,
isFromNotification: Boolean,
shouldShowCsat: Boolean,
shouldShowChatResolution: Boolean,
initialLoadComplete: () -> Unit,
hidePaginationLoader: () -> Unit,
hideLoaderFromNotification: (channelInfo: ChatChannelInfo?, showFreeText: Boolean?) -> Unit,
) {
viewModelScope.launch {
val response = naviChatRepository.fetchChatConversations(
conversationId = conversationId,
pageNo = pageCount
)
if (response.error == null && response.errors.isNullOrEmpty()) {
canLoadMore = response.data?.isLast?.not() ?: false
if (pageCount == 0) {
latestMessageTimeStamp = response.data?.lastMessageTimeStamp
_chatHistoryMessages.value =
Pair(
first = pageCount,
second =
response.data?.listOfMessages?.let {
val msgs = getDisplayableMessages(it)
msgs
} ?: emptyList()
)
if (isFromNotification) {
if ((response.data?.listOfMessages?.size ?: 0) > 0) {
response.data?.listOfMessages?.let {
checkMsgForCsat(it, shouldShowCsat, shouldShowChatResolution)
run loop@{
it.forEach { naviChatWidget ->
if (naviChatWidget is NaviChatControllerWidget) {
_chatControllerWidget.value = naviChatWidget
return@loop
}
}
}
}
}
hideLoaderFromNotification.invoke(
response.data?.channelInfo,
response.data?.showFreeText
)
} else {
response.data?.channelInfo?.let {
_chatChannelInfo.value = it
}
response.data?.showFreeText?.let {
_showFreeText.value = it
}
initialLoadComplete.invoke()
}
isPaginationLoading = false
pageCount++
} else {
_chatHistoryMessages.value =
Pair(
first = pageCount,
second =
response.data?.listOfMessages?.let {
val msgs = getDisplayableMessages(it)
msgs
} ?: emptyList()
)
isPaginationLoading = false
pageCount++
}
} else {
if (pageCount == 0) {
setError(
errors = response.errors,
errorTag = response.errors?.firstOrNull()?.code,
cancelable = false
)
} else {
hidePaginationLoader.invoke()
}
}
}
}
fun markConversationRead(
conversationId: String,
user: String,
readTimestamp: String,
deliveredTimestamp: String
) {
viewModelScope.launch {
if (conversationId.isNotNullAndNotEmpty()) {
naviChatRepository.updateReceipts(
conversationId = conversationId,
user = user,
readTimestamp = readTimestamp,
deliveredTimestamp = deliveredTimestamp
)
}
}
}
private fun checkMsgForCsat(msgList: List<NaviChatWidget>, shouldShowCsat: Boolean, shouldShowChatResolution: Boolean) {
msgList.forEach { msg ->
if (msg is NaviChatCsatRatingWidget && shouldShowCsat) {
_csatRatingWidget.value = msg
} else if (msg is NaviChatResolutionStatusWidget && shouldShowChatResolution) {
_chatResolutionWidget.value = msg
}
}
}
fun getFileTypeSizeConfig() {
viewModelScope.launch {
val response = naviChatRepository.getConfig()
if (
response.error == null &&
response.errors.isNullOrEmpty() &&
response.isNotNull() &&
response.data?.fileUpload.isNotNull() &&
response.data?.fileUpload?.sizeInKiloBytes.isNotNull()
) {
_naviChatConfigResponse.value = response.data?.let {
constructConfigResponse(it)
}
}
}
}
private fun constructConfigResponse(response: ChatConfigResponse): ChatConfigConstructedResponse {
return ChatConfigConstructedResponse(
fileSizeInKB = getHashMap(response),
botTypingCuesDelayInMs = response.botTypingCuesDelayInMs?.toLong() ?: 1000L,
csatAppearanceDelayInMs = response.chatAppearanceDelayInMs?.toLong() ?: 2000L
)
}
private fun getHashMap(response: ChatConfigResponse): HashMap<String, Int> {
val hashmap = HashMap<String, Int>()
response.fileUpload?.sizeInKiloBytes?.let { sizeInKb ->
sizeInKb.JPEG?.let { hashmap[AWSFileType.JPEG.value] = it }
sizeInKb.JPG?.let { hashmap[AWSFileType.JPG.value] = it }
sizeInKb.PNG?.let { hashmap[AWSFileType.PNG.value] = it }
sizeInKb.PDF?.let { hashmap[AWSFileType.PDF.value] = it }
sizeInKb.MP4?.let { hashmap[AWSFileType.MP4.value] = it }
sizeInKb.DOC?.let { hashmap[AWSFileType.DOC.value] = it }
sizeInKb.DOCX?.let { hashmap[AWSFileType.DOCX.value] = it }
}
return hashmap
}
fun setLatestMessageLatestTimeStamp(timestamp: Timestamp?) {
timestamp?.let {
latestMessageTimeStamp = it
Timber.d("Setting latest timestamp: $timestamp")
}
}
fun setCsatFeedbackState(value: Boolean) {
isCsatFeedbackShown = value
}
fun getCsatFeedbackState() = isCsatFeedbackShown
fun setChatResolutionState(value: Boolean) {
isChatResolutionShown = value
}
fun getChatResolutionState () = isChatResolutionShown
fun setTimerEnabledForMessage(value: Boolean) {
isTimerEnabledForMessage = value
}
fun getTimerEnabledForMessage() = isTimerEnabledForMessage
fun setTimerFlag(value: Boolean) {
isTimerSet = value
}
fun getTimerFlag() = isTimerSet
fun setTimerDuration(duration: Long) {
timerDuration = duration
}
fun getTimerDuration() = timerDuration
fun setRemainingCharactersView(value: Boolean) {
showRemainingCharactersView = value
}
fun showRemainingCharactersView() = showRemainingCharactersView
fun setCharLimit(value: Int) {
setCharLimit = value
}
fun getCharLimit() = setCharLimit
fun enqueueDownload(task: DownloadTask) {
downloadManager.enqueueDownloadTask(task)
}
fun checkIfActivityRedirectionIsValid(conversationId: String, nodeUuid: String,
url: String?, parameters: List<LineItem>) {
viewModelScope.launch {
val queryMap = java.util.HashMap<String, String>()
queryMap[CONVERSATION_ID_PARAM] = conversationId
queryMap[NODE_UUID_PARAM] = nodeUuid
val response = naviChatRepository.checkIfActivityRedirectionIsValid(
queryMap = queryMap
)
if (response.error == null && response.errors.isNullOrEmpty()) {
_activityRedirectionValidationResponse.emit(
ChatActivityRedirectionConstructedResponse(
isValid = response.data?.isValid, url = url,
parameters = parameters
)
)
} else {
_activityRedirectionValidationResponse.emit(ChatActivityRedirectionConstructedResponse(
isValid = false
))
}
}
}
override suspend fun onDownloadSuccess(downloadTask: DownloadTask) {
Timber.d("NaviChatViewModel:: onSuccess")
_downloadSuccessSharedFlow.emit(downloadTask)
}
override suspend fun onError(error: Throwable, downloadTask: DownloadTask) {
Timber.d("NaviChatViewModel:: onError")
_downloadFailureSharedFlow.emit(downloadTask)
}
}

View File

@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~
~ * Copyright © 2023 by Navi Technologies Limited
~ * All rights reserved. Strictly confidential
~
-->
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.NaviChatFragment">
<com.navi.chat.ui.components.NaviChatToolbar
android:id="@+id/nctLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/chatLoader"
android:layout_width="@dimen/layout_dp_56"
android:layout_height="@dimen/layout_dp_56"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/nctLayout"
app:lottie_autoPlay="true"
app:lottie_fileName="purple_loader.json"
app:lottie_loop="true"
app:lottie_speed="1.0" />
<androidx.constraintlayout.widget.Group
android:id="@+id/chatScreen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="rvChat,viewSeparator,clFreeText" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvChat"
android:background="@color/white"
android:layout_width="match_parent"
android:layout_height="@dimen/layout_dp_0"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/viewSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/nctLayout" />
<View
android:id="@+id/viewSeparator"
android:layout_width="match_parent"
android:layout_height="@dimen/layout_dp_1"
android:visibility="gone"
tools:visibility="visible"
android:background="@color/divider_color_light_grey"
app:layout_constraintBottom_toTopOf="@id/clFreeText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rvChat" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clFreeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/viewSeparator1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/viewSeparator">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clInnerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/dp_12"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/verticalGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.77" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/etFreeText"
android:layout_width="@dimen/layout_dp_0"
android:layout_height="wrap_content"
android:background="@null"
android:cursorVisible="true"
android:fontFamily="@font/tt_regular"
android:gravity="center_vertical"
android:inputType="textMultiLine"
android:maxLines="@integer/integer_3"
android:hint="@string/type_here"
android:imeOptions="actionDone"
android:paddingStart="@dimen/layout_dp_16"
android:paddingEnd="@dimen/layout_dp_16"
android:textColor="@color/title_color_two"
android:textCursorDrawable="@null"
android:textSize="@dimen/font_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/verticalGuideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivSendAttachment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/dp_9"
android:padding="@dimen/dp_7"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_add_attachment_purple" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivSendFreeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_send_msg_purple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.navi.design.textview.NaviTextView
android:id="@+id/tvCharLimitCounter"
style="@style/SmallTextSemiBoldDescriptionColorSeven"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="@color/white"
android:paddingHorizontal="@dimen/_16dp"
android:paddingVertical="@dimen/dp_4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/clInnerLayout"
tools:text="1000 characters left" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/viewSeparator1"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_230"
android:background="@color/divider_color_light_grey"
app:layout_constraintTop_toBottomOf="@id/clFreeText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -70,6 +70,7 @@ object FirebaseRemoteConfigHelper {
const val DISABLE_ALFRED = "DISABLE_ALFRED"
const val INTENT_DATA_INSERT_DISABLED = "INTENT_DATA_INSERT_DISABLED"
const val UPI_NUMBER_DISABLED = "UPI_NUMBER_DISABLED"
const val ENABLE_CHAT_V2 = "ENABLE_CHAT_V2"
const val UW_INGESTION_POLLING = "UW_INGESTION_POLLING"
fun init() {

View File

@@ -18,6 +18,11 @@
<value>false</value>
</entry>
<entry>
<key>ENABLE_CHAT_V2</key>
<value>false</value>
</entry>
<entry>
<key>hundred_ms_setting_for_auto_resize</key>
<value>false</value>

View File

@@ -17,7 +17,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateTint="@color/red"
android:indeterminateTint="@color/dark_purple"
android:indeterminateTintMode="src_atop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"