diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bced7e303d..6fc6e89a22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ awaitility = "4.1.0" branch = "5.1.1" cashfree = "2.0.6" chucker = "4.0.0" -coilCompose = "2.4.0" +coil = "2.4.0" compose-bom = "2023.09.01" compose-lib = '1.5.3' delight-advancedWebView = "v3.0.0" @@ -226,8 +226,8 @@ cashfree = { module = "com.cashfree.pg:api", version.ref = "cashfree" } chucker-library = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" } chucker-libraryNoOp = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" } -coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } - +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil" } dagger-hiltAndroid = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } dagger-hiltAndroidCompiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } dagger-hiltAndroidTesting = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } diff --git a/navi-chat/build.gradle b/navi-chat/build.gradle index f26ca0df6d..16de97cbdf 100644 --- a/navi-chat/build.gradle +++ b/navi-chat/build.gradle @@ -55,6 +55,8 @@ dependencies { implementation libs.android.material + implementation libs.coil.video + implementation libs.dagger.hiltAndroid kapt libs.dagger.hiltCompiler diff --git a/navi-chat/src/main/AndroidManifest.xml b/navi-chat/src/main/AndroidManifest.xml index 98060b9ea9..97f1724bf5 100644 --- a/navi-chat/src/main/AndroidManifest.xml +++ b/navi-chat/src/main/AndroidManifest.xml @@ -48,6 +48,11 @@ android:exported="false" android:screenOrientation="portrait" android:theme="@style/BaseThemeStyle" /> + \ No newline at end of file diff --git a/navi-chat/src/main/java/com/navi/chat/db/utils/ChatUtils.kt b/navi-chat/src/main/java/com/navi/chat/db/utils/ChatUtils.kt index 092c0c33f3..c867a65a9d 100644 --- a/navi-chat/src/main/java/com/navi/chat/db/utils/ChatUtils.kt +++ b/navi-chat/src/main/java/com/navi/chat/db/utils/ChatUtils.kt @@ -7,10 +7,21 @@ package com.navi.chat.db.utils +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap import com.google.gson.Gson +import com.navi.chat.utils.TIME_FORMAT_TO_DISPLAY import com.navi.chat.utils.USER_SENDER_TYPE +import com.navi.chat.utils.maxFileSizeInKb +import com.navi.common.utils.log import com.navi.naviwidgets.models.NaviChatWidget import com.navi.naviwidgets.models.response.* +import java.io.File fun transformFirestoreToWidgetModel(data: Map): NaviChatWidget = when (data["widget_name"]) { @@ -105,4 +116,92 @@ fun getTimerExpiredResponse(timerExpired: Boolean, senderName: String) = senderName = senderName ), shouldDisplayInChatHistory = false - ) \ No newline at end of file + ) + +fun getMimeType(context: Context, uri: Uri): String? { + return context.contentResolver.getType(uri) +} + +fun timeInString(seconds: Int): String { + return String.format( + TIME_FORMAT_TO_DISPLAY, (seconds / 3600 * 60 + ((seconds % 3600) / 60)), (seconds % 60) + ) +} + +@SuppressLint("Range") +fun getFileName(contentResolver: ContentResolver, uri: Uri): String { + val uriString = uri.toString() + var displayName = "" + if (uriString.startsWith("content://")) { + var cursor: Cursor? = null + try { + cursor = contentResolver.query(uri, null, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + displayName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } finally { + cursor?.close() + } + } else if (uriString.startsWith("file://")) { + displayName = File(uriString).name + } + return displayName +} + +fun isFileTooLarge( + contentResolver: ContentResolver, + uri: Uri, + fileExtension: String?, + maxFileSizeMap: Map? +): Boolean { + val fileSizeInKb = getFileSizeInBytes(contentResolver, uri) + val maxSize = fileExtension?.let { maxFileSizeMap?.get(it.uppercase()) } ?: maxFileSizeInKb.toLong() + return fileSizeInKb > maxSize * 1024 +} + +private fun getFileSizeInBytes(contentResolver: ContentResolver, uri: Uri): Long { + var fileSize: Long = 0 + + if (uri.scheme == "content") { + val cursor = contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE) + if (sizeIndex != -1) { + fileSize = it.getLong(sizeIndex) + } + } + } + } else if (uri.scheme == "file") { + try { + val file = uri.path?.let { File(it) } + if (file != null) { + if (file.exists()) { + fileSize = file.length() + } + } + } catch (e: Exception) { + e.log() + } + } + + return fileSize +} + +fun stringToMap(data: String): Map { + val keyValuePairs = data.removeSurrounding("{", "}").split(", ").map { it.split("=") } + + return keyValuePairs.associate { (key, value) -> key to value.toLong() } +} + +fun fileSizeWithinLimit( + context: Context, + selectedUri: Uri, + contentResolver: ContentResolver, + fileSizesMap: Map +): Boolean { + val mimeType = getMimeType(context, Uri.parse(selectedUri.toString())) + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.lowercase() + if (!isFileTooLarge(contentResolver, selectedUri, extension, fileSizesMap)) return true + return false +} diff --git a/navi-chat/src/main/java/com/navi/chat/interfaces/AttachmentOptionClickListener.kt b/navi-chat/src/main/java/com/navi/chat/interfaces/AttachmentOptionClickListener.kt new file mode 100644 index 0000000000..94006c764e --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/interfaces/AttachmentOptionClickListener.kt @@ -0,0 +1,13 @@ +/* + * + * * Copyright © 2023 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.chat.interfaces + +interface AttachmentOptionClickListener { + fun onGalleryAttachmentOptionClicked() + fun onFileAttachmentOptionClicked() +} diff --git a/navi-chat/src/main/java/com/navi/chat/ui/activities/AttachmentConfirmationActivity.kt b/navi-chat/src/main/java/com/navi/chat/ui/activities/AttachmentConfirmationActivity.kt new file mode 100644 index 0000000000..c4aceff248 --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/ui/activities/AttachmentConfirmationActivity.kt @@ -0,0 +1,69 @@ +package com.navi.chat.ui.activities + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.compose.setContent +import com.navi.chat.db.utils.getFileName +import com.navi.chat.ui.compose.AttachmentPickerScreen +import com.navi.chat.utils.FILENAME +import com.navi.chat.utils.FILE_SIZES +import com.navi.chat.utils.FILE_TYPE_PARAM +import com.navi.chat.utils.FILE_URI +import com.navi.chat.utils.NAVI_CHAT_ATTACHMENT_CONFIRMATION_ACTIVITY +import com.navi.chat.utils.URI_LIST +import com.navi.common.model.ModuleNameV2 +import com.navi.common.ui.activity.BaseActivity + +class AttachmentConfirmationActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val data = intent.getStringExtra(FILE_TYPE_PARAM) + val fileSizes = intent.getStringExtra(FILE_SIZES) + setContent { + AttachmentPickerScreen( + onBackPress = { onBackPressed() }, + onSendClick = { selectedImages -> + val intent = createMyIntent(selectedImages) + startActivity(intent) + }, + onPlayClick = { fileUri -> + val intent = Intent(this, NaviChatViewVideoActivity::class.java) + val bundle = Bundle() + bundle.putString( + FILENAME, getFileName(this.contentResolver, fileUri) + ) + intent.putExtra(FILE_URI, fileUri.toString()) + intent.putExtras(bundle) + this.startActivity(intent) + }, + onImageClick = { fileUri -> + val intent = Intent(this, NaviChatViewImageActivity::class.java) + val bundle = Bundle() + bundle.putString( + FILENAME, getFileName(this.contentResolver, fileUri) + ) + intent.putExtra(FILE_URI, fileUri.toString()) + intent.putExtras(bundle) + this.startActivity(intent) + }, + data, + fileSizes.toString() + ) + } + } + + private fun createMyIntent(selectedImages: List): Intent { + val intent = Intent(this, NaviChatActivity::class.java) + intent.putParcelableArrayListExtra(URI_LIST, ArrayList(selectedImages)) + setResult(RESULT_OK, intent) + finish() + return intent + } + + override val screenName: String + get() = NAVI_CHAT_ATTACHMENT_CONFIRMATION_ACTIVITY + override val moduleName: ModuleNameV2 + get() = ModuleNameV2.CHAT +} diff --git a/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatActivity.kt b/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatActivity.kt index d4925bfd9c..8d40b6c886 100644 --- a/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatActivity.kt +++ b/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatActivity.kt @@ -7,22 +7,49 @@ package com.navi.chat.ui.activities +import android.app.Activity import android.content.Intent +import android.net.Uri import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider import com.navi.chat.R import com.navi.chat.base.ChatBaseActivity import com.navi.chat.databinding.ActivityNaviChatBinding +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.utils.CONVERSATION_ID_PARAM +import com.navi.chat.utils.DOCUMENT_ITEM +import com.navi.chat.utils.FILE_SIZES +import com.navi.chat.utils.FILE_TYPE_PARAM +import com.navi.chat.utils.GALLERY_ITEM import com.navi.chat.utils.NAVI_CHAT_SYSTEM_LOCAL_DATA +import com.navi.chat.utils.URI_LIST +import com.navi.chat.viewmodels.AttachmentPickerViewModel -class NaviChatActivity : ChatBaseActivity() { +class NaviChatActivity : ChatBaseActivity() , AttachmentOptionClickListener{ private lateinit var binding: ActivityNaviChatBinding private val currentScreenTag: String = NaviChatFragment.TAG private var chatFragment: NaviChatFragment? = null + private val attachmentPickerViewModel by lazy { + ViewModelProvider(this)[AttachmentPickerViewModel::class.java] + } + private var resultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val receivedDataList: List? = data?.getParcelableArrayListExtra(URI_LIST) + receivedDataList?.forEach { receivedData -> + attachmentPickerViewModel.setImagePickerData( + receivedData.toString(), getFileName(contentResolver, receivedData) + ) + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -88,4 +115,17 @@ class NaviChatActivity : ChatBaseActivity() { var isChatActivityVisible: Boolean = false private set } + + override fun onGalleryAttachmentOptionClicked() { + val intent = Intent(this, AttachmentConfirmationActivity::class.java) + intent.putExtra(FILE_TYPE_PARAM, GALLERY_ITEM) + intent.putExtra(FILE_SIZES, attachmentPickerViewModel.getFileSizes().toString()) + resultLauncher.launch(intent) + } + override fun onFileAttachmentOptionClicked() { + val intent = Intent(this, AttachmentConfirmationActivity::class.java) + intent.putExtra(FILE_TYPE_PARAM, DOCUMENT_ITEM) + intent.putExtra(FILE_SIZES, attachmentPickerViewModel.getFileSizes().toString()) + resultLauncher.launch(intent) + } } diff --git a/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewImageActivity.kt b/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewImageActivity.kt index f30162ac58..0124bb0e81 100644 --- a/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewImageActivity.kt +++ b/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewImageActivity.kt @@ -10,6 +10,7 @@ package com.navi.chat.ui.activities import android.os.Bundle import android.widget.Toast import androidx.core.content.FileProvider +import androidx.core.net.toUri import androidx.databinding.DataBindingUtil import com.bumptech.glide.Glide import com.navi.chat.R @@ -20,6 +21,7 @@ import com.navi.chat.di.dependencies.NaviChatModuleDependencies import com.navi.chat.di.modules.NaviChatModule import com.navi.chat.utils.ChatFileHelper import com.navi.chat.utils.FILENAME +import com.navi.chat.utils.FILE_URI import com.navi.chat.utils.ToolbarInteraction import dagger.hilt.android.EntryPointAccessors import java.io.File @@ -71,8 +73,11 @@ class NaviChatViewImageActivity : ChatBaseActivity(), ToolbarInteraction { val sd: File = this.cacheDir val folder = File(sd, "/chat/") val outputFile = File(folder, fileName) - val uri = + val uri = if (outputFile.exists()) { FileProvider.getUriForFile(this, "${this.packageName}.fileprovider", outputFile) + } else { + intent.getStringExtra(FILE_URI)?.toUri() + } Glide.with(this).load(uri).into(binding.ivImageAttachment) } else { Toast.makeText(this, getString(R.string.file_cannot_be_opened), Toast.LENGTH_LONG) diff --git a/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewVideoActivity.kt b/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewVideoActivity.kt index a49f8f0d5e..df53a65cc0 100644 --- a/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewVideoActivity.kt +++ b/navi-chat/src/main/java/com/navi/chat/ui/activities/NaviChatViewVideoActivity.kt @@ -19,6 +19,7 @@ import android.view.* import android.widget.SeekBar import android.widget.Toast import androidx.core.content.FileProvider +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.databinding.DataBindingUtil import com.navi.chat.R @@ -228,10 +229,15 @@ class NaviChatViewVideoActivity : val sd: File = this.cacheDir val folder = File(sd, "/chat/") val outputFile = File(folder, it) - val uri = + val uri = if (outputFile.exists()) { FileProvider.getUriForFile(this, "${this.packageName}.fileprovider", outputFile) + } else { + intent.getStringExtra(FILE_URI)?.toUri() + } mediaPlayer?.apply { - setDataSource(applicationContext, uri) + if (uri != null) { + setDataSource(applicationContext, uri) + } } mediaPlayer?.setOnPreparedListener(this) mediaPlayer?.setOnCompletionListener(this) @@ -304,12 +310,12 @@ class NaviChatViewVideoActivity : private fun pauseMediaPlayer() { mediaPlayer?.pause() - binding.ivPlayPauseVideo.setImageResource(R.drawable.ic_play_video) + binding.ivPlayPauseVideo.setImageResource(R.drawable.ic_play_video_purple) } private fun startMediaPlayer() { mediaPlayer?.start() - binding.ivPlayPauseVideo.setImageResource(R.drawable.ic_pause_icon) + binding.ivPlayPauseVideo.setImageResource(R.drawable.ic_pause_icon_purple) } @SuppressLint("ClickableViewAccessibility") @@ -352,7 +358,7 @@ class NaviChatViewVideoActivity : // Go to start when video completes override fun onCompletion(mp: MediaPlayer?) { setVideoPlayerControlsVisibility(true) - binding.ivPlayPauseVideo.setImageResource(R.drawable.ic_play_video) + binding.ivPlayPauseVideo.setImageResource(R.drawable.ic_play_video_purple) mediaPlayerSeekTo(0) } diff --git a/navi-chat/src/main/java/com/navi/chat/ui/components/FileDocument.kt b/navi-chat/src/main/java/com/navi/chat/ui/components/FileDocument.kt new file mode 100644 index 0000000000..f33b80eaba --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/ui/components/FileDocument.kt @@ -0,0 +1,34 @@ +package com.navi.chat.ui.components + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.CallSuper +import com.navi.chat.utils.MIME_TYPE_DOCX +import com.navi.chat.utils.MIME_TYPE_DOC +import com.navi.chat.utils.MIME_TYPE_PDF + +class FileDocument : ActivityResultContract, Uri?>() { + @CallSuper + override fun createIntent(context: Context, input: Array): Intent { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = MIME_TYPE_PDF + intent.putExtra( + Intent.EXTRA_MIME_TYPES, arrayOf( + MIME_TYPE_PDF, MIME_TYPE_DOC, MIME_TYPE_DOCX + ) + ) + return intent + } + + override fun getSynchronousResult( + context: Context, input: Array + ): SynchronousResult? = null + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return intent.takeIf { resultCode == Activity.RESULT_OK }?.data + } +} \ No newline at end of file diff --git a/navi-chat/src/main/java/com/navi/chat/ui/components/GalleryDocument.kt b/navi-chat/src/main/java/com/navi/chat/ui/components/GalleryDocument.kt new file mode 100644 index 0000000000..d3c63e7017 --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/ui/components/GalleryDocument.kt @@ -0,0 +1,29 @@ +package com.navi.chat.ui.components + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.CallSuper +import com.navi.chat.utils.IMAGE_TYPE +import com.navi.chat.utils.VIDEO_TYPE + +class GalleryDocument : ActivityResultContract, Uri?>() { + @CallSuper + override fun createIntent(context: Context, input: Array): Intent { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = IMAGE_TYPE + intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(IMAGE_TYPE, VIDEO_TYPE)) + return intent + } + + override fun getSynchronousResult( + context: Context, input: Array + ): SynchronousResult? = null + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return intent.takeIf { resultCode == Activity.RESULT_OK }?.data + } +} \ No newline at end of file diff --git a/navi-chat/src/main/java/com/navi/chat/ui/compose/AttachmentPickerScreen.kt b/navi-chat/src/main/java/com/navi/chat/ui/compose/AttachmentPickerScreen.kt new file mode 100644 index 0000000000..fdbe570134 --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/ui/compose/AttachmentPickerScreen.kt @@ -0,0 +1,417 @@ +package com.navi.chat.ui.compose + +import android.annotation.SuppressLint +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.ImageLoader +import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter +import coil.decode.VideoFrameDecoder +import coil.request.ImageRequest +import com.navi.chat.R +import com.navi.chat.db.utils.fileSizeWithinLimit +import com.navi.chat.db.utils.getFileName +import com.navi.chat.db.utils.getMimeType +import com.navi.chat.db.utils.stringToMap +import com.navi.chat.db.utils.timeInString +import com.navi.chat.ui.components.FileDocument +import com.navi.chat.ui.components.GalleryDocument +import com.navi.chat.ui.theme.NaviChatColor +import com.navi.chat.utils.MIME_TYPE_DOCX +import com.navi.chat.utils.MIME_TYPE_DOC +import com.navi.chat.utils.GALLERY_ITEM +import com.navi.chat.utils.IMAGE_TYPE +import com.navi.chat.utils.MIME_TYPE_IMAGE +import com.navi.chat.utils.MIME_TYPE_PDF +import com.navi.chat.utils.MIME_TYPE_VIDEO +import com.navi.chat.utils.VIDEO_TYPE +import com.navi.chat.utils.maxFileLimit +import com.navi.design.theme.composeFontFamily +import com.navi.design.theme.ttComposeFontFamily +import kotlinx.coroutines.launch + +@Composable +fun AttachmentPickerScreen( + onBackPress: () -> Unit = {}, + onSendClick: (selectedImages: MutableList) -> Unit, + onPlayClick: (fileUri: Uri) -> Unit, + onImageClick: (fileUri: Uri) -> Unit, + data: String?, + fileSizes: String, +) { + val selectedImages = remember { mutableStateListOf() } + val primaryButtonEnabled = remember { mutableStateOf(false) } + + Scaffold(modifier = Modifier.fillMaxSize(), topBar = { + NaviChatHeader( + title = stringResource(id = R.string.navi_chat_toolbar_title), + onNavigationIconClick = { onBackPress() }, + modifier = Modifier + ) + }, bottomBar = { + NaviChatFooter( + secondaryButtonText = stringResource(id = R.string.cancel), + primaryButtonText = stringResource(id = R.string.send), + onSecondaryButtonClicked = { onBackPress() }, + onPrimaryButtonClicked = { onSendClick(selectedImages) }, + primaryButtonEnabled = primaryButtonEnabled.value + ) + }, content = { paddingValues -> + NaviChatAttachmentScreenBody( + selectedImages, + data, + paddingValues, + fileSizes, + onPlayClick = { uri -> onPlayClick(uri) }, + onImageClick = { uri -> onImageClick(uri) }, + primaryButtonEnabled = primaryButtonEnabled + ) + }) +} + +@Composable +fun NaviChatAttachmentScreenBody( + selectedImages: MutableList, + data: String?, + padding: PaddingValues, + fileSizes: String, + onPlayClick: (fileUri: Uri) -> Unit, + onImageClick: (fileUri: Uri) -> Unit, + primaryButtonEnabled: MutableState +) { + val context = LocalContext.current + val contentResolver = context.contentResolver + val fileSizesMap = stringToMap(fileSizes) + + primaryButtonEnabled.value = selectedImages.size != 0 + val photoPickerLauncher = rememberLauncherForActivityResult( + contract = GalleryDocument() + ) { selectedUri: Uri? -> + if (selectedUri != null) { + if (fileSizeWithinLimit(context, selectedUri, contentResolver, fileSizesMap)) { + selectedImages.add(selectedUri) + } else Toast.makeText( + context, R.string.file_size_exceeded, Toast.LENGTH_SHORT + ).show() + val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION + contentResolver?.takePersistableUriPermission(selectedUri, takeFlags) + } + } + + val documentPickerLauncher = rememberLauncherForActivityResult( + contract = FileDocument() + ) { selectedUri: Uri? -> + if (selectedUri != null) { + if (fileSizeWithinLimit(context, selectedUri, contentResolver, fileSizesMap)) { + selectedImages.add(selectedUri) + } else Toast.makeText( + context, R.string.file_size_exceeded, Toast.LENGTH_SHORT + ).show() + val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION + contentResolver?.takePersistableUriPermission(selectedUri, takeFlags) + } + } + + val lazyListState = rememberLazyGridState() + val scope = rememberCoroutineScope() + + fun scrollToLastItem() { + val lastIndex = selectedImages.size - 1 + if (lastIndex >= 0) { + scope.launch { + lazyListState.animateScrollToItem(lastIndex) + } + } + } + LaunchedEffect(selectedImages.size) { + scrollToLastItem() + } + LazyVerticalGrid( + columns = GridCells.Fixed(2), + state = lazyListState, + modifier = Modifier + .fillMaxSize() + .padding( + bottom = padding.calculateBottomPadding(), top = 16.dp, start = 8.dp, end = 8.dp + ) + ) { + items(selectedImages.size + 1) { index -> + if (index < selectedImages.size) { + GalleryItem(selectedImages[index], + onRemoveClick = { selectedImages.removeAt(index) }, + onPlayClick = { onPlayClick(selectedImages[index]) }, + onImageClick = { onImageClick(selectedImages[index]) }) + } else { + if (data == GALLERY_ITEM && selectedImages.size < maxFileLimit) { + AddMedia(photoPickerLauncher) + } else if (selectedImages.size < maxFileLimit) { + AddDocument(documentPickerLauncher) + } + } + } + } +} + +@SuppressLint("SuspiciousIndentation") +@Composable +fun GalleryItem( + fileUri: Uri, + onRemoveClick: () -> Unit, + onPlayClick: (fileUri:Uri) -> Unit = {}, + onImageClick: (fileUri:Uri) -> Unit = {} +) { + + val context = LocalContext.current + val mimeType = getMimeType(context, Uri.parse(fileUri.toString())) + val allowedMimeTypes = listOf( + MIME_TYPE_PDF, + MIME_TYPE_DOC, + MIME_TYPE_DOCX, + ) + Box( + modifier = Modifier + .padding(14.dp) + .background(Color.White) + .height(138.dp) + .border(1.dp, NaviChatColor.ctaPrimary, RoundedCornerShape(4.dp)), + ) { + if (mimeType != null) { + if (mimeType.startsWith(MIME_TYPE_IMAGE)) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current).data(fileUri).allowHardware(false).build(), + contentDescription = "Image preview", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clickable(interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onImageClick(fileUri) }), + ) + } + if (allowedMimeTypes.any { mimeType.startsWith(it) }) { + Box(modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + content = { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_doc_preview), + contentDescription = "Document preview", + ) + Spacer(modifier = Modifier.size(10.dp)) + val fileName = getFileName(context.contentResolver, fileUri) + Text( + text = fileName, + fontSize = 12.sp, + lineHeight = 18.sp, + fontFamily = composeFontFamily, + color = Color.Black, + modifier = Modifier.padding(horizontal = 10.dp), + textAlign = TextAlign.Center, + maxLines = 2, + ) + } + }) + } + + if (mimeType.startsWith(MIME_TYPE_VIDEO)) { + val imageLoader = ImageLoader.Builder(context).components { + add(VideoFrameDecoder.Factory()) + }.crossfade(true).build() + + val painter = rememberAsyncImagePainter( + model = fileUri, + imageLoader = imageLoader + ) + Box( + modifier= Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onPlayClick(fileUri) }) + ) { + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + Image( + painter = painterResource(id = R.drawable.ic_play_video_button), + contentDescription = "Play video", + modifier = Modifier.align(Alignment.Center) + ) + + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, fileUri) + val videoDurationString = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + val videoDuration = videoDurationString?.toLong() + retriever.release() + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 115.dp, end = 2.dp), + contentAlignment = Alignment.BottomEnd + ) { + val videoDurationInSeconds = + timeInString((videoDuration?.div(1000))?.toInt() ?: 0) + Text( + text = videoDurationInSeconds, + color = Color.White, + fontSize = 12.sp, + modifier = Modifier.padding(4.dp) + ) + } + } + } + } + } + + Box( + modifier = Modifier + .offset(8.dp, (-12).dp) + .fillMaxSize(), + contentAlignment = Alignment.TopEnd + ) { + IconButton( + onClick = onRemoveClick + ) { + Image( + painter = painterResource(id = R.drawable.ic_remove_image), + contentDescription = "Remove File", + ) + } + } +} + +@Composable +fun AddMedia(photoPickerLauncher: ManagedActivityResultLauncher, Uri?>) +{ + Box(modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, indication = null + ) { + photoPickerLauncher.launch( + arrayOf( + IMAGE_TYPE, + VIDEO_TYPE, + ) + ) + } + .padding(14.dp) + .border( + width = 1.dp, color = NaviChatColor.ctaPrimary, shape = RoundedCornerShape(size = 4.dp) + ) + .height(138.dp), contentAlignment = Alignment.Center, content = { + Column( + modifier = Modifier.padding(start = 15.dp, end = 15.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_image), + contentDescription = stringResource(id = R.string.add_media) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(id = R.string.add_media), + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = ttComposeFontFamily, + fontWeight = FontWeight(600), + color = NaviChatColor.textSecondary, + ) + } + }) +} + +@Composable +fun AddDocument(documentPickerLauncher: ManagedActivityResultLauncher, Uri?>) +{ + Box(modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, indication = null + ) { + documentPickerLauncher.launch( + arrayOf( + MIME_TYPE_PDF, + MIME_TYPE_DOC, + MIME_TYPE_DOCX, + ) + ) + } + .padding(14.dp) + .border( + width = 1.dp, color = NaviChatColor.ctaPrimary, shape = RoundedCornerShape(size = 4.dp) + ) + .height(138.dp), contentAlignment = Alignment.Center, content = { + Column( + modifier = Modifier.padding(start = 15.dp, end = 15.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_image), + contentDescription = stringResource(id = R.string.add_attachment), + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(id = R.string.add_attachment), + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = ttComposeFontFamily, + fontWeight = FontWeight(600), + color = NaviChatColor.textSecondary, + textAlign = TextAlign.Center + ) + } + }) +} diff --git a/navi-chat/src/main/java/com/navi/chat/ui/compose/NaviChatCommonViews.kt b/navi-chat/src/main/java/com/navi/chat/ui/compose/NaviChatCommonViews.kt new file mode 100644 index 0000000000..9adf3c9f75 --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/ui/compose/NaviChatCommonViews.kt @@ -0,0 +1,171 @@ +package com.navi.chat.ui.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.chat.R +import com.navi.chat.ui.theme.NaviChatColor +import com.navi.common.utils.ClickDebounce +import com.navi.common.utils.get +import com.navi.design.font.FontWeightEnum +import com.navi.design.theme.getFontWeight +import com.navi.design.theme.ttComposeFontFamily + + +@Composable +fun NaviChatHeader( + title: String, + navigationIcon: Int = R.drawable.ic_chat_back_arrow, + onNavigationIconClick: (() -> Unit), + modifier: Modifier, + backgroundColor: Color = Color.White +) { + TopAppBar(title = { + Text( + text = title, + fontSize = 14.sp, + fontFamily = ttComposeFontFamily, + fontWeight = getFontWeight(FontWeightEnum.TT_REGULAR), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(end = 60.dp) + ) + }, navigationIcon = { + IconButton(onClick = { onNavigationIconClick.invoke() }) { + Image(painter = painterResource(navigationIcon), contentDescription = null) + } + }, modifier = modifier.height(48.dp), + elevation = 1.dp, + backgroundColor = backgroundColor + ) +} + +@Composable +fun NaviChatFooter( + secondaryButtonText: String, + primaryButtonText: String, + onSecondaryButtonClicked: () -> Unit, + onPrimaryButtonClicked: () -> Unit, + primaryButtonEnabled: Boolean = true +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp) + .background(Color.White), + ) { + + Spacer(modifier = Modifier.height(24.dp)) + + Row { + SecondaryRoundedButton( + modifier = Modifier.weight(1f), + text = secondaryButtonText + ) { + onSecondaryButtonClicked.invoke() + } + + Spacer(modifier = Modifier.width(16.dp)) + + ThemeRoundedButton( + modifier = Modifier.weight(1f), + text = primaryButtonText, + enabled = primaryButtonEnabled + ) { + onPrimaryButtonClicked.invoke() + } + + } + } +} + +@Composable +fun ThemeRoundedButton( + modifier: Modifier = Modifier, + text: String, + cornerRadius: Dp = 4.dp, + paddingValues: PaddingValues = PaddingValues(vertical = 14.dp, horizontal = 10.dp), + fontSize: TextUnit = 14.sp, + enabled: Boolean = true, + onClick: () -> Unit, +) { + + val clickDebounce = remember { ClickDebounce.get() } + + Button( + elevation = ButtonDefaults.elevation(defaultElevation = 0.dp), + onClick = { clickDebounce.processClick { onClick() } }, + modifier = modifier, + shape = RoundedCornerShape(cornerRadius), + contentPadding = paddingValues, + enabled = enabled, + colors = ButtonDefaults.buttonColors( + backgroundColor = NaviChatColor.ctaPrimary, + disabledBackgroundColor = NaviChatColor.ctaDisabled + ) + ) { + Text( + text = text, + fontSize = fontSize, + fontFamily = ttComposeFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviChatColor.textWhite, + letterSpacing = 0.sp + ) + } +} + +@Composable +fun SecondaryRoundedButton( + modifier: Modifier = Modifier, + text: String, + cornerRadius: Dp = 4.dp, + paddingValues: PaddingValues = PaddingValues(vertical = 14.dp, horizontal = 10.dp), + onClick: () -> Unit +) { + val clickDebounce = remember { ClickDebounce.get() } + + Button( + elevation = ButtonDefaults.elevation(defaultElevation = 0.dp), + onClick = { clickDebounce.processClick { onClick() } }, + modifier = modifier, + shape = RoundedCornerShape(cornerRadius), + colors = ButtonDefaults.buttonColors( + backgroundColor = NaviChatColor.ctaSecondary + ), + contentPadding = paddingValues + ) { + Text( + text = text, + fontSize = 14.sp, + fontFamily = ttComposeFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + color = NaviChatColor.ctaPrimary, + letterSpacing = 0.sp + ) + } +} diff --git a/navi-chat/src/main/java/com/navi/chat/ui/fragments/ChatAttachmentBottomSheet.kt b/navi-chat/src/main/java/com/navi/chat/ui/fragments/ChatAttachmentBottomSheet.kt index b9afc87654..4e4dd27e81 100644 --- a/navi-chat/src/main/java/com/navi/chat/ui/fragments/ChatAttachmentBottomSheet.kt +++ b/navi-chat/src/main/java/com/navi/chat/ui/fragments/ChatAttachmentBottomSheet.kt @@ -9,19 +9,17 @@ package com.navi.chat.ui.fragments import android.Manifest -import android.annotation.SuppressLint import android.app.Activity +import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Environment -import android.provider.OpenableColumns import android.view.View import android.view.ViewStub -import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.CallSuper import androidx.appcompat.content.res.AppCompatResources import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -35,6 +33,7 @@ import com.navi.chat.databinding.ChatAttachmentBottomSheetBinding import com.navi.chat.di.components.DaggerNaviChatComponent import com.navi.chat.di.dependencies.NaviChatModuleDependencies import com.navi.chat.di.modules.NaviChatModule +import com.navi.chat.interfaces.AttachmentOptionClickListener import com.navi.chat.interfaces.ChatAttachmentOptionsInterface import com.navi.chat.models.ChatAttachmentData import com.navi.chat.utils.FILE_MANAGER @@ -43,7 +42,6 @@ import com.navi.chat.viewmodels.AttachmentPickerViewModel import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.launch import timber.log.Timber -import java.io.File class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterface { @@ -53,11 +51,15 @@ class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterf private val attachmentPickerViewModel by lazy { ViewModelProvider(requireActivity())[AttachmentPickerViewModel::class.java] } - + private var attachmentOptionClickListener: AttachmentOptionClickListener? = null override val screenName: String get() = TAG + override fun onAttach(context: Context) { + super.onAttach(context) + attachmentOptionClickListener = context as? AttachmentOptionClickListener + } override fun setContainerView(viewStub: ViewStub) { viewStub.layoutResource = R.layout.chat_attachment_bottom_sheet binding = DataBindingUtil.getBinding(viewStub.inflate())!! @@ -97,7 +99,7 @@ class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterf private fun createAttachmentOptionList(): ArrayList { val attachmentDataArrayList = ArrayList() - AppCompatResources.getDrawable(requireContext(), R.drawable.ic_gallery_icon)?.let { + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_gallery_icon_new)?.let { attachmentDataArrayList.add( ChatAttachmentData( it, @@ -106,7 +108,7 @@ class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterf ) ) } - AppCompatResources.getDrawable(requireContext(), R.drawable.ic_file_manager_icon)?.let { + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_file_manager_icon_new)?.let { attachmentDataArrayList.add( ChatAttachmentData( it, @@ -122,44 +124,15 @@ class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterf if (isLegacyExternalStoragePermissionRequired()) { requestLegacyWriteExternalStoragePermission() } else { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "image/*" - intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*")) - resultLauncher.launch(Intent.createChooser(intent, "Select image or video")) + attachmentOptionClickListener?.onGalleryAttachmentOptionClicked() } } - private var resultLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { - attachmentPickerViewModel.setImagePickerData( - result.data, result.data?.data?.let { getFileName(it) } - ) - } - } - private fun openFile() { if (isLegacyExternalStoragePermissionRequired()) { requestLegacyWriteExternalStoragePermission() } else { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/*" - putExtra( - Intent.EXTRA_MIME_TYPES, - arrayOf( - DOC, - DOCX, - PDF - ) - ) - putExtra( - Intent.ACTION_OPEN_DOCUMENT, - requireContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) - ) - } - resultLauncher1.launch(intent) + attachmentOptionClickListener?.onFileAttachmentOptionClicked() } } @@ -183,36 +156,6 @@ class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterf } } - private var resultLauncher1 = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { - attachmentPickerViewModel.setPdfPickerData( - result.data, result.data?.data?.let { getFileName(it) } - ) - } - } - - @SuppressLint("Range") - private fun getFileName(uri: Uri): String { - val uriString = uri.toString() - var displayName = "" - if (uriString.startsWith("content://")) { - var cursor: Cursor? = null - try { - cursor = activity?.contentResolver?.query(uri, null, null, null, null) - if (cursor != null && cursor.moveToFirst()) { - displayName = - cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) - } - } finally { - cursor?.close() - } - } else if (uriString.startsWith("file://")) { - displayName = File(uriString).name - } - return displayName - } - override fun onClickChatAttachmentOption(attachmentsType: String) { when (attachmentsType) { GALLERY -> { @@ -233,4 +176,4 @@ class ChatAttachmentBottomSheet : BaseBottomSheet(), ChatAttachmentOptionsInterf const val DOC = "application/msword" const val DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" } -} +} \ No newline at end of file diff --git a/navi-chat/src/main/java/com/navi/chat/ui/fragments/NaviChatFragment.kt b/navi-chat/src/main/java/com/navi/chat/ui/fragments/NaviChatFragment.kt index 4e106893ed..8acc011911 100644 --- a/navi-chat/src/main/java/com/navi/chat/ui/fragments/NaviChatFragment.kt +++ b/navi-chat/src/main/java/com/navi/chat/ui/fragments/NaviChatFragment.kt @@ -380,6 +380,7 @@ class NaviChatFragment : ChatBaseFragment(), WidgetCallback, MessageOperation, T fileSizes.clear() it?.let { fileSizes.putAll(it.fileSizeInKB.orEmpty()) + it.fileSizeInKB?.let { it1 -> attachmentPickerViewModel.setFileSizes(it1) } it.botTypingCuesDelayInMs?.let { botDelayInMs -> delayForTypingCuesForBot = botDelayInMs Timber.d("Updated delayForTypingCuesForBot to $botDelayInMs") diff --git a/navi-chat/src/main/java/com/navi/chat/ui/theme/NaviChatColor.kt b/navi-chat/src/main/java/com/navi/chat/ui/theme/NaviChatColor.kt new file mode 100644 index 0000000000..04723c1cde --- /dev/null +++ b/navi-chat/src/main/java/com/navi/chat/ui/theme/NaviChatColor.kt @@ -0,0 +1,11 @@ +package com.navi.chat.ui.theme + +import androidx.compose.ui.graphics.Color + +object NaviChatColor { + val textSecondary = Color(0xFF444444) + val textWhite = Color(0xFFFFFFFF) + val ctaPrimary = Color(0xFF1F002A) + val ctaSecondary = Color(0xFFF5F5F5) + val ctaDisabled = Color(0xFFB5ACB9) +} \ No newline at end of file diff --git a/navi-chat/src/main/java/com/navi/chat/utils/Constants.kt b/navi-chat/src/main/java/com/navi/chat/utils/Constants.kt index cc1befa9ca..84292c6013 100644 --- a/navi-chat/src/main/java/com/navi/chat/utils/Constants.kt +++ b/navi-chat/src/main/java/com/navi/chat/utils/Constants.kt @@ -50,6 +50,20 @@ const val BOT = "BOT" const val DIGITAL_GOLD = "DIGITAL_GOLD" const val FILENAME = "filename" const val NODE_UUID_PARAM = "nodeUuid" +const val GALLERY_ITEM = "image" +const val DOCUMENT_ITEM = "document" +const val URI_LIST = "uriList" +const val FILE_SIZES = "fileSizes" +const val FILE_URI = "fileUri" +const val IMAGE_TYPE = "image/*" +const val VIDEO_TYPE = "video/*" +const val MIME_TYPE_IMAGE = "image/" +const val MIME_TYPE_VIDEO = "video/" +const val MIME_TYPE_PDF = "application/pdf" +const val MIME_TYPE_DOC = "application/msword" +const val MIME_TYPE_DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" +const val maxFileLimit = 6 +const val maxFileSizeInKb = 15000 /* Video Player Instance Save Constant */ const val CURRENT_VIDEO_POSITION_IN_MS = "currentVideoPositionInMs" @@ -59,3 +73,4 @@ const val TIME_FORMAT_TO_DISPLAY = "%02d:%02d" /* Screen names */ const val NAVI_CHAT_VIEW_VIDEO_ACTIVITY = "naviChatViewVideoActivity" +const val NAVI_CHAT_ATTACHMENT_CONFIRMATION_ACTIVITY = "naviChatAttachmentConfirmationActivity" diff --git a/navi-chat/src/main/java/com/navi/chat/viewmodels/AttachmentPickerViewModel.kt b/navi-chat/src/main/java/com/navi/chat/viewmodels/AttachmentPickerViewModel.kt index c46b718941..dbcaa379d3 100644 --- a/navi-chat/src/main/java/com/navi/chat/viewmodels/AttachmentPickerViewModel.kt +++ b/navi-chat/src/main/java/com/navi/chat/viewmodels/AttachmentPickerViewModel.kt @@ -12,6 +12,7 @@ import android.app.Application import android.content.Intent import android.net.Uri import android.widget.Toast +import androidx.core.net.toUri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.navi.chat.R @@ -39,6 +40,7 @@ class AttachmentPickerViewModel(application: Application) : AndroidViewModel(app private val defaultMaxFileSizeInMb = 15 var maxFileSizeInMb = defaultMaxFileSizeInMb + private var fileSizes = HashMap() val dispatcher = CoroutineDispatcherProvider() val fileHelper = ChatFileHelper(dispatcher) private var isToastVisible = false @@ -116,14 +118,21 @@ class AttachmentPickerViewModel(application: Application) : AndroidViewModel(app } } - fun setImagePickerData(data: Intent?, fileName: String?) { + fun setImagePickerData(data: String?, fileName: String?) { viewModelScope.launch(dispatcher.io) { - val imageData = data?.data - imageData?.let { emitFileData(imageData, fileName, FileHelper.JPG_EXTENSION) } + data?.let { emitFileData(data.toUri(), fileName, FileHelper.JPG_EXTENSION) } dismissDialog() } } + fun setFileSizes(sizes: HashMap) { + fileSizes = sizes + } + + fun getFileSizes(): HashMap { + return fileSizes + } + private fun dismissDialog() { viewModelScope.launch(dispatcher.main) { _dismissBottomSheetDialog.emit(true) } } diff --git a/navi-chat/src/main/res/drawable/ic_add_image.xml b/navi-chat/src/main/res/drawable/ic_add_image.xml new file mode 100644 index 0000000000..0aee6b29b1 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_add_image.xml @@ -0,0 +1,9 @@ + + + diff --git a/navi-chat/src/main/res/drawable/ic_doc_preview.xml b/navi-chat/src/main/res/drawable/ic_doc_preview.xml new file mode 100644 index 0000000000..71da414b10 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_doc_preview.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/navi-chat/src/main/res/drawable/ic_file_manager_icon_new.xml b/navi-chat/src/main/res/drawable/ic_file_manager_icon_new.xml new file mode 100644 index 0000000000..ec0b658af3 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_file_manager_icon_new.xml @@ -0,0 +1,9 @@ + + + diff --git a/navi-chat/src/main/res/drawable/ic_gallery_icon_new.xml b/navi-chat/src/main/res/drawable/ic_gallery_icon_new.xml new file mode 100644 index 0000000000..b3440ad62e --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_gallery_icon_new.xml @@ -0,0 +1,12 @@ + + + + diff --git a/navi-chat/src/main/res/drawable/ic_pause_icon_purple.xml b/navi-chat/src/main/res/drawable/ic_pause_icon_purple.xml new file mode 100644 index 0000000000..f0ea6d943b --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_pause_icon_purple.xml @@ -0,0 +1,15 @@ + + + + diff --git a/navi-chat/src/main/res/drawable/ic_play_button.xml b/navi-chat/src/main/res/drawable/ic_play_button.xml new file mode 100644 index 0000000000..3679523a43 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_play_button.xml @@ -0,0 +1,10 @@ + + + diff --git a/navi-chat/src/main/res/drawable/ic_play_video_button.xml b/navi-chat/src/main/res/drawable/ic_play_video_button.xml new file mode 100644 index 0000000000..13924ef4aa --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_play_video_button.xml @@ -0,0 +1,13 @@ + + + + diff --git a/navi-chat/src/main/res/drawable/ic_play_video_purple.xml b/navi-chat/src/main/res/drawable/ic_play_video_purple.xml new file mode 100644 index 0000000000..ee5d7679e3 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_play_video_purple.xml @@ -0,0 +1,15 @@ + + + + diff --git a/navi-chat/src/main/res/drawable/ic_remove_image.xml b/navi-chat/src/main/res/drawable/ic_remove_image.xml new file mode 100644 index 0000000000..2f6044cc07 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_remove_image.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/navi-chat/src/main/res/drawable/ic_right_arrow_purple_white_bg.xml b/navi-chat/src/main/res/drawable/ic_right_arrow_purple_white_bg.xml new file mode 100644 index 0000000000..b958eeb29f --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_right_arrow_purple_white_bg.xml @@ -0,0 +1,12 @@ + + + + diff --git a/navi-chat/src/main/res/drawable/ic_tooltip_slider_purple.xml b/navi-chat/src/main/res/drawable/ic_tooltip_slider_purple.xml new file mode 100644 index 0000000000..d09efb7105 --- /dev/null +++ b/navi-chat/src/main/res/drawable/ic_tooltip_slider_purple.xml @@ -0,0 +1,11 @@ + + + diff --git a/navi-chat/src/main/res/layout/activity_navi_chat_view_video.xml b/navi-chat/src/main/res/layout/activity_navi_chat_view_video.xml index 3a1cea9b01..fddae5af46 100644 --- a/navi-chat/src/main/res/layout/activity_navi_chat_view_video.xml +++ b/navi-chat/src/main/res/layout/activity_navi_chat_view_video.xml @@ -57,7 +57,7 @@ android:id="@+id/ivPlayPauseVideo" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_pause_icon" + android:src="@drawable/ic_pause_icon_purple" android:visibility="gone" tools:visibility="visible" app:layout_constraintStart_toStartOf="parent" @@ -84,8 +84,8 @@ app:layout_constraintEnd_toStartOf="@id/totalDuration" app:layout_constraintBottom_toBottomOf="parent" android:layout_marginBottom="@dimen/dp_34" - android:progressTint="@color/outrageous_orange" - android:thumb="@drawable/ic_tooltip_slider_thumb" + android:progressTint="@color/dark_purple" + android:thumb="@drawable/ic_tooltip_slider_purple" app:layout_constraintHorizontal_bias="0.0" tools:progress="50" android:progressBackgroundTint="@color/white" diff --git a/navi-chat/src/main/res/layout/chat_attachment_bottom_sheet.xml b/navi-chat/src/main/res/layout/chat_attachment_bottom_sheet.xml index 5ec5286a8d..d6731f4cc2 100644 --- a/navi-chat/src/main/res/layout/chat_attachment_bottom_sheet.xml +++ b/navi-chat/src/main/res/layout/chat_attachment_bottom_sheet.xml @@ -23,7 +23,7 @@ android:layout_marginStart="@dimen/layout_dp_16" android:layout_marginTop="@dimen/layout_dp_16" android:textColor="@color/dark_gray" - android:fontFamily="@font/tt_regular" + android:fontFamily="@font/tt_medium" android:textSize="@dimen/sp_18" android:text="@string/add_files" app:layout_constraintEnd_toEndOf="parent" diff --git a/navi-chat/src/main/res/layout/fragment_common_navi_chat.xml b/navi-chat/src/main/res/layout/fragment_common_navi_chat.xml index 6f0d159299..4a4ee72e72 100644 --- a/navi-chat/src/main/res/layout/fragment_common_navi_chat.xml +++ b/navi-chat/src/main/res/layout/fragment_common_navi_chat.xml @@ -26,6 +26,7 @@ + tools:src="@drawable/ic_gallery_icon_new" /> Error occurred while playing video The link you are trying to access has expired. %d/%d characters left + Cancel + Send + Add Attachment + Add Media + File size is too large 00:00 \ No newline at end of file diff --git a/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatReceivedMessageWithAttachmentLayout.kt b/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatReceivedMessageWithAttachmentLayout.kt index 12a7d6e7bd..987c24d1db 100644 --- a/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatReceivedMessageWithAttachmentLayout.kt +++ b/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatReceivedMessageWithAttachmentLayout.kt @@ -84,7 +84,7 @@ class NaviChatReceivedMessageWithAttachmentLayout @JvmOverloads constructor( when (state) { VIEW_STATE -> { - tvReceivedMessage.progressImage.setBackgroundResource(R.drawable.ic_preview) + tvReceivedMessage.progressImage.setBackgroundResource(R.drawable.ic_preview_purple) tvReceivedMessage.progressState.progress = ProgressBar.GONE tvReceivedMessage.progressImage.setOnClickListener { naviChatFileAttachmentWidget.widgetData?.fileName?.let { uri -> @@ -95,7 +95,7 @@ class NaviChatReceivedMessageWithAttachmentLayout @JvmOverloads constructor( } } DOWNLOAD_STATE, FAILED_STATE -> { - tvReceivedMessage.progressImage.setBackgroundResource(R.drawable.ic_download) + tvReceivedMessage.progressImage.setBackgroundResource(R.drawable.ic_download_purple) tvReceivedMessage.progressState.progress = ProgressBar.GONE tvReceivedMessage.progressImage.setOnClickListener { naviChatFileAttachmentWidget.widgetData.let { widgetData -> @@ -134,9 +134,9 @@ class NaviChatReceivedMessageWithAttachmentLayout @JvmOverloads constructor( private fun getBackgroundDrawable(fileType: String?): Int { return when (fileType) { - AWSFileType.MP4.value -> R.drawable.ic_video_preview + AWSFileType.MP4.value -> R.drawable.ic_video_preview_purple AWSFileType.JPG.value, AWSFileType.JPEG.value, - AWSFileType.PNG.value -> R.drawable.ic_image_preview + AWSFileType.PNG.value -> R.drawable.ic_image_preview_purple else -> R.drawable.ic_document_preview } } diff --git a/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatSentMessageWithAttachmentLayout.kt b/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatSentMessageWithAttachmentLayout.kt index c3ba6f4995..d7909d8a66 100644 --- a/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatSentMessageWithAttachmentLayout.kt +++ b/navi-widgets/src/main/java/com/navi/naviwidgets/widgets/NaviChatSentMessageWithAttachmentLayout.kt @@ -86,7 +86,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 when (state) { VIEW_STATE -> { - tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_preview) + if (naviChatFileAttachmentWidget.widgetData?.fileType == AWSFileType.MP4.value) { + tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_play_preview_purple) + } else { + tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_preview_purple) + } tvSentMessage.progressState.progress = ProgressBar.GONE tvSentMessage.progressImage.setOnClickListener { naviChatFileAttachmentWidget.id?.let { @@ -97,7 +101,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } } FAILURE_AND_RETRY_STATE -> { - tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_retry) + tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_retry_new) tvSentMessage.progressState.progress = ProgressBar.GONE tvSentMessage.progressImage.setOnClickListener { naviChatFileAttachmentWidget.id?.let { @@ -106,7 +110,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } } UPLOADING_STATE -> { - tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_cancel) + tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_cancel_purple) tvSentMessage.progressState.progress = ProgressBar.VISIBLE tvSentMessage.progressImage.setOnClickListener { naviChatFileAttachmentWidget.id?.let { @@ -116,7 +120,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } DOWNLOAD_STATE, FAILED_STATE -> { - tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_download) + tvSentMessage.progressImage.setBackgroundResource(R.drawable.ic_download_purple) tvSentMessage.progressState.progress = ProgressBar.GONE tvSentMessage.progressImage.setOnClickListener { naviChatFileAttachmentWidget.widgetData?.fileUuid?.let { @@ -163,10 +167,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 private fun getBackgroundDrawable(fileType: String?): Int { return when (fileType) { - AWSFileType.MP4.value -> R.drawable.ic_video_preview + AWSFileType.MP4.value -> R.drawable.ic_video_preview_purple AWSFileType.JPG.value, AWSFileType.JPEG.value, - AWSFileType.PNG.value -> R.drawable.ic_image_preview + AWSFileType.PNG.value -> R.drawable.ic_image_preview_purple else -> R.drawable.ic_document_preview } } diff --git a/navi-widgets/src/main/res/drawable/ic_cancel_purple.xml b/navi-widgets/src/main/res/drawable/ic_cancel_purple.xml new file mode 100644 index 0000000000..8d57750be3 --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_cancel_purple.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/navi-widgets/src/main/res/drawable/ic_document_preview.xml b/navi-widgets/src/main/res/drawable/ic_document_preview.xml index 629965b4af..6603486e94 100644 --- a/navi-widgets/src/main/res/drawable/ic_document_preview.xml +++ b/navi-widgets/src/main/res/drawable/ic_document_preview.xml @@ -1,27 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> - - - + android:pathData="M6,2C4.906,2 4,2.906 4,4V20C4,21.094 4.906,22 6,22H18C19.094,22 20,21.094 20,20V6.586L15.414,2H6ZM6,4H14V8H18V20H6V4ZM8,10V12H16V10H8ZM8,13V15H16V13H8ZM8,16V18H13V16H8Z" + android:fillColor="#1F002A"/> diff --git a/navi-widgets/src/main/res/drawable/ic_download_purple.xml b/navi-widgets/src/main/res/drawable/ic_download_purple.xml new file mode 100644 index 0000000000..cc5c9324fd --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_download_purple.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/navi-widgets/src/main/res/drawable/ic_image_preview_purple.xml b/navi-widgets/src/main/res/drawable/ic_image_preview_purple.xml new file mode 100644 index 0000000000..f76ca5a2b3 --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_image_preview_purple.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/navi-widgets/src/main/res/drawable/ic_navi_chat_sent_message_bg.xml b/navi-widgets/src/main/res/drawable/ic_navi_chat_sent_message_bg.xml index c2aee9e40c..d49e56ec74 100644 --- a/navi-widgets/src/main/res/drawable/ic_navi_chat_sent_message_bg.xml +++ b/navi-widgets/src/main/res/drawable/ic_navi_chat_sent_message_bg.xml @@ -1,9 +1,14 @@ + + - + android:bottomLeftRadius="@dimen/dp_20" + android:bottomRightRadius="@dimen/dp_4" + android:topLeftRadius="@dimen/dp_20" + android:topRightRadius="@dimen/dp_20" /> + \ No newline at end of file diff --git a/navi-widgets/src/main/res/drawable/ic_play_preview_purple.xml b/navi-widgets/src/main/res/drawable/ic_play_preview_purple.xml new file mode 100644 index 0000000000..f485e33630 --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_play_preview_purple.xml @@ -0,0 +1,15 @@ + + + + diff --git a/navi-widgets/src/main/res/drawable/ic_preview_purple.xml b/navi-widgets/src/main/res/drawable/ic_preview_purple.xml new file mode 100644 index 0000000000..ced0b4c906 --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_preview_purple.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/navi-widgets/src/main/res/drawable/ic_retry_new.xml b/navi-widgets/src/main/res/drawable/ic_retry_new.xml new file mode 100644 index 0000000000..aeb0ecded5 --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_retry_new.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/navi-widgets/src/main/res/drawable/ic_video_preview_purple.xml b/navi-widgets/src/main/res/drawable/ic_video_preview_purple.xml new file mode 100644 index 0000000000..9a92dd7d11 --- /dev/null +++ b/navi-widgets/src/main/res/drawable/ic_video_preview_purple.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/navi-widgets/src/main/res/layout/layout_navi_chat_item_message_with_attachment.xml b/navi-widgets/src/main/res/layout/layout_navi_chat_item_message_with_attachment.xml index bc7c54a3fd..1d769b54e7 100644 --- a/navi-widgets/src/main/res/layout/layout_navi_chat_item_message_with_attachment.xml +++ b/navi-widgets/src/main/res/layout/layout_navi_chat_item_message_with_attachment.xml @@ -18,8 +18,8 @@ app:layout_constraintEnd_toStartOf="@id/tvMessage" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - android:src="@drawable/ic_image_preview" - tools:src="@drawable/ic_image_preview" /> + android:src="@drawable/ic_image_preview_purple" + tools:src="@drawable/ic_image_preview_purple" /> + tools:src="@drawable/ic_cancel_purple" /> \ No newline at end of file diff --git a/navi-widgets/src/main/res/layout/layout_navi_chat_sent_message_with_attachment.xml b/navi-widgets/src/main/res/layout/layout_navi_chat_sent_message_with_attachment.xml index 4550cc7a6d..f15d2dfa55 100644 --- a/navi-widgets/src/main/res/layout/layout_navi_chat_sent_message_with_attachment.xml +++ b/navi-widgets/src/main/res/layout/layout_navi_chat_sent_message_with_attachment.xml @@ -54,7 +54,7 @@ android:layout_gravity="end" android:paddingLeft="@dimen/dp_4" app:layout_constraintBottom_toBottomOf="@id/tvTimestamp" - app:layout_constraintTop_toTopOf="@id/tvTimestamp" + app:layout_constraintTop_toBottomOf="@id/tvSentMessage" app:layout_constraintEnd_toEndOf="@id/tvSentMessage" android:visibility="gone" tools:visibility="visible"