diff --git a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt index 0ff1c21..723a020 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt @@ -1,5 +1,10 @@ package com.aiosman.riderpro.ui.chat +import android.app.Activity +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween @@ -29,6 +34,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -66,6 +72,7 @@ import com.aiosman.riderpro.exp.formatChatTime import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.modifiers.noRippleClickable +import com.tencent.imsdk.v2.V2TIMMessage import kotlinx.coroutines.launch @@ -158,7 +165,13 @@ fun ChatScreen(userId: String) { .background(Color(0xfff7f7f7)) ) Spacer(modifier = Modifier.height(8.dp)) - ChatInput() { + ChatInput( + onSendImage = { + it?.let { + viewModel.sendImageMessage(it, context) + } + }, + ) { viewModel.sendMessage(it, context) } } @@ -230,17 +243,35 @@ fun ChatSelfItem(item: ChatItem) { .padding(vertical = 8.dp, horizontal = 16.dp) .padding(bottom = 3.dp) ) { - Text( - text = item.message, - style = TextStyle( - color = Color.White, - fontSize = 16.sp, - ), - textAlign = TextAlign.Start, - - ) + when (item.messageType) { + V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> { + Text( + text = item.message, + style = TextStyle( + color = Color.White, + fontSize = 16.sp, + ), + textAlign = TextAlign.Start + ) + } + V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> { + CustomAsyncImage( + imageUrl = item.imageList[1].url, + modifier = Modifier.fillMaxSize(), + contentDescription = "image" + ) + } + else -> { + Text( + text = "Unsupported message type", + style = TextStyle( + color = Color.White, + fontSize = 16.sp, + ) + ) + } + } } - } Spacer(modifier = Modifier.width(12.dp)) Box( @@ -308,13 +339,34 @@ fun ChatOtherItem(item: ChatItem) { .padding(vertical = 8.dp, horizontal = 16.dp) .padding(bottom = 3.dp) ) { - Text( - text = item.message, - style = TextStyle( - color = Color.Black, - fontSize = 16.sp - ) - ) + when (item.messageType) { + V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> { + Text( + text = item.message, + style = TextStyle( + color = Color.Black, + fontSize = 16.sp, + ), + textAlign = TextAlign.Start + ) + } + V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> { + CustomAsyncImage( + imageUrl = item.imageList[1].url, + modifier = Modifier.fillMaxSize(), + contentDescription = "image" + ) + } + else -> { + Text( + text = "Unsupported message type", + style = TextStyle( + color = Color.White, + fontSize = 16.sp, + ) + ) + } + } } } @@ -335,7 +387,8 @@ fun ChatItem(item: ChatItem, currentUserId: String) { @Composable fun ChatInput( - onSend: (String) -> Unit = {} + onSendImage: (Uri?) -> Unit = {}, + onSend: (String) -> Unit = {}, ) { val navigationBarHeight = with(LocalDensity.current) { WindowInsets.navigationBars.getBottom(this).toDp() @@ -367,6 +420,15 @@ fun ChatInput( } } + val imagePickUpLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + val uri = it.data?.data + onSendImage(uri) + } + } + Row( modifier = Modifier .fillMaxWidth() @@ -413,6 +475,24 @@ fun ChatInput( ) } Spacer(modifier = Modifier.width(16.dp)) + Icon( + painter = painterResource(id = R.drawable.rider_pro_images), + contentDescription = "Emoji", + modifier = Modifier + .size(32.dp) + .noRippleClickable { + imagePickUpLauncher.launch( + Intent.createChooser( + Intent(Intent.ACTION_GET_CONTENT).apply { + type = "image/*" + }, + "Select Image" + ) + ) + }, + tint = Color(0xff000000) + ) + Spacer(modifier = Modifier.width(8.dp)) Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty -> Image( painter = rememberUpdatedState( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt index 11d877c..b9c6797 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt @@ -2,7 +2,10 @@ package com.aiosman.riderpro.ui.chat import android.content.Context import android.icu.util.Calendar +import android.net.Uri +import android.provider.MediaStore import android.util.Log +import android.webkit.MimeTypeMap import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -16,11 +19,15 @@ import com.aiosman.riderpro.entity.AccountProfileEntity import com.aiosman.riderpro.exp.formatChatTime import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener import com.tencent.imsdk.v2.V2TIMCallback +import com.tencent.imsdk.v2.V2TIMImageElem import com.tencent.imsdk.v2.V2TIMManager import com.tencent.imsdk.v2.V2TIMMessage import com.tencent.imsdk.v2.V2TIMSendCallback import com.tencent.imsdk.v2.V2TIMValueCallback import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream data class ChatItem( @@ -30,7 +37,10 @@ data class ChatItem( val userId: String, val nickname: String, val timeCategory: String = "", - val timestamp: Long = 0 + val timestamp: Long = 0, + val imageList: MutableList = emptyList().toMutableList(), + val messageType : Int = 0, + val textDisplay : String = "" ) class ChatViewModel( @@ -44,7 +54,7 @@ class ChatViewModel( var textMessageListener: V2TIMAdvancedMsgListener? = null var hasMore by mutableStateOf(true) var isLoading by mutableStateOf(false) - var lastMessage : V2TIMMessage? = null + var lastMessage: V2TIMMessage? = null fun init(context: Context) { // 获取用户信息 viewModelScope.launch { @@ -61,7 +71,12 @@ class ChatViewModel( textMessageListener = object : V2TIMAdvancedMsgListener() { override fun onRecvNewMessage(msg: V2TIMMessage?) { super.onRecvNewMessage(msg) - chatData = listOf(convertToChatItem(msg!!, context)) + chatData + msg?.let { + val chatItem = convertToChatItem(msg, context) + chatItem?.let { + chatData = listOf(it) + chatData + } + } } } V2TIMManager.getMessageManager().addAdvancedMsgListener(textMessageListener); @@ -70,7 +85,8 @@ class ChatViewModel( fun UnRegistListener() { V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener); } - fun clearUnRead(){ + + fun clearUnRead() { val conversationID = "c2c_${userProfile?.trtcUserId}" V2TIMManager.getConversationManager() .cleanConversationUnreadMessageCount(conversationID, 0, 0, object : V2TIMCallback { @@ -83,7 +99,8 @@ class ChatViewModel( } }) } - fun convertToChatItem(message: V2TIMMessage, context: Context): ChatItem { + + fun convertToChatItem(message: V2TIMMessage, context: Context): ChatItem? { val avatar = if (message.sender == userProfile?.trtcUserId) { userProfile?.avatar ?: "" } else { @@ -97,15 +114,50 @@ class ChatViewModel( val timestamp = message.timestamp val calendar = Calendar.getInstance() calendar.timeInMillis = timestamp * 1000 + val imageElm = message.imageElem?.imageList + when (message.elemType) { + V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> { + val imageElm = message.imageElem?.imageList?.all { + it.size == 0 + } + if (imageElm != true) { + return ChatItem( + message = "Image", + avatar = avatar, + time = calendar.time.formatChatTime(context), + userId = message.sender, + nickname = nickname, + timestamp = timestamp * 1000, + imageList = message.imageElem?.imageList + ?: emptyList().toMutableList(), + messageType = V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE, + textDisplay = "Image" + ) + } + return null - return ChatItem( - message = message.textElem.text, - avatar = avatar, - time = calendar.time.formatChatTime(context), - userId = message.sender, - nickname = nickname, - timestamp = timestamp * 1000 - ) + } + + V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> { + return ChatItem( + message = message.textElem?.text ?: "Unsupported message type", + avatar = avatar, + time = calendar.time.formatChatTime(context), + userId = message.sender, + nickname = nickname, + timestamp = timestamp * 1000, + imageList = imageElm?.toMutableList() + ?: emptyList().toMutableList(), + messageType = V2TIMMessage.V2TIM_ELEM_TYPE_TEXT, + textDisplay = message.textElem?.text ?: "Unsupported message type" + ) + + } + else -> { + return null + } + + } } fun onLoadMore(context: Context) { @@ -122,7 +174,7 @@ class ChatViewModel( override fun onSuccess(p0: List?) { chatData = chatData + (p0 ?: emptyList()).map { convertToChatItem(it, context) - } + }.filterNotNull() if ((p0?.size ?: 0) < 20) { hasMore = false } @@ -130,6 +182,7 @@ class ChatViewModel( isLoading = false Log.d("ChatViewModel", "fetch history message success") } + override fun onError(p0: Int, p1: String?) { Log.e("ChatViewModel", "fetch history message error: $p1") isLoading = false @@ -147,18 +200,85 @@ class ChatViewModel( override fun onProgress(p0: Int) { } + override fun onError(p0: Int, p1: String?) { Log.e("ChatViewModel", "send message error: $p1") } override fun onSuccess(p0: V2TIMMessage?) { Log.d("ChatViewModel", "send message success") - chatData = listOf(convertToChatItem(p0!!, context)) + chatData + val chatItem = convertToChatItem(p0!!, context) + chatItem?.let { + chatData = listOf(it) + chatData + } } } ) } + fun sendImageMessage(imageUri:Uri, context: Context) { + val tempFile = createTempFile(context, imageUri) + val imagePath = tempFile?.path + if (imagePath != null) { + val v2TIMMessage = V2TIMManager.getMessageManager().createImageMessage(imagePath) + V2TIMManager.getMessageManager().sendMessage( + v2TIMMessage, + userProfile?.trtcUserId!!, + null, + V2TIMMessage.V2TIM_PRIORITY_NORMAL, + false, + null, + object : V2TIMSendCallback { + override fun onProgress(p0: Int) { + Log.d("ChatViewModel", "send image message progress: $p0") + } + + override fun onError(p0: Int, p1: String?) { + Log.e("ChatViewModel", "send image message error: $p1") + } + + override fun onSuccess(p0: V2TIMMessage?) { + Log.d("ChatViewModel", "send image message success") + val chatItem = convertToChatItem(p0!!, context) + chatItem?.let { + chatData = listOf(it) + chatData + } + } + } + ) + } + + } + fun createTempFile(context: Context, uri: Uri): File? { + return try { + val projection = arrayOf(MediaStore.Images.Media.DATA) + val cursor = context.contentResolver.query(uri, projection, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + val filePath = it.getString(columnIndex) + val inputStream: InputStream? = context.contentResolver.openInputStream(uri) + val mimeType = context.contentResolver.getType(uri) + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + val tempFile = File.createTempFile("temp_image", ".$extension", context.cacheDir) + val outputStream = FileOutputStream(tempFile) + + inputStream?.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + tempFile + } else { + null + } + } + } catch (e: Exception) { + e.printStackTrace() + null + } + } + fun fetchHistoryMessage(context: Context) { V2TIMManager.getMessageManager().getC2CHistoryMessageList( userProfile?.trtcUserId!!, @@ -168,7 +288,7 @@ class ChatViewModel( override fun onSuccess(p0: List?) { chatData = (p0 ?: emptyList()).map { convertToChatItem(it, context) - } + }.filterNotNull() if ((p0?.size ?: 0) < 20) { hasMore = false } @@ -182,6 +302,7 @@ class ChatViewModel( } ) } + fun getDisplayChatList(): List { return chatData } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt index 07e168f..11a40af 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt @@ -1,4 +1,3 @@ -import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -21,7 +20,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.Text -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -46,7 +44,7 @@ import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.aiosman.riderpro.utils.File.saveImageToGallery +import com.aiosman.riderpro.utils.FileUtil.saveImageToGallery import kotlinx.coroutines.launch import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt index c78ecdc..40ed75c 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt @@ -304,7 +304,7 @@ fun ChatMessageList( Spacer(modifier = Modifier.height(6.dp)) Row { Text( - text = item.lastMessage, + text = "${if (item.isSelf) "Me: " else ""}${item.displayText}", fontSize = 14.sp, maxLines = 1, color = Color(0x99000000), diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt index 71fd14a..45f6d7b 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt @@ -9,32 +9,25 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import androidx.navigation.NavHostController -import androidx.paging.Pager -import androidx.paging.PagingConfig import androidx.paging.PagingData -import androidx.paging.cachedIn import androidx.paging.map import com.aiosman.riderpro.data.AccountNotice import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.entity.CommentEntity -import com.aiosman.riderpro.entity.CommentPagingSource -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.CommentService import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.CommentServiceImpl import com.aiosman.riderpro.data.UserService import com.aiosman.riderpro.data.UserServiceImpl import com.aiosman.riderpro.exp.formatChatTime import com.aiosman.riderpro.ui.NavigationRoute +import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.riderpro.ui.navigateToChat import com.aiosman.riderpro.utils.TrtcHelper import com.tencent.imsdk.v2.V2TIMConversation import com.tencent.imsdk.v2.V2TIMConversationResult import com.tencent.imsdk.v2.V2TIMManager +import com.tencent.imsdk.v2.V2TIMMessage import com.tencent.imsdk.v2.V2TIMValueCallback import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlin.coroutines.suspendCoroutine @@ -44,7 +37,9 @@ data class Conversation( val lastMessage: String, val lastMessageTime: String, val avatar: String = "", - val unreadCount: Int = 0 + val unreadCount: Int = 0, + val displayText: String, + val isSelf: Boolean ) object MessageListViewModel : ViewModel() { @@ -142,13 +137,24 @@ object MessageListViewModel : ViewModel() { timeInMillis = msg.lastMessage?.timestamp ?: 0 timeInMillis *= 1000 } + var displayText = "" + when (msg.lastMessage?.elemType) { + V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> { + displayText = msg.lastMessage?.textElem?.text ?: "" + } + V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> { + displayText = "[图片]" + } + } Conversation( nickname = msg.showName, lastMessage = msg.lastMessage?.textElem?.text ?: "", lastMessageTime = lastMessage.time.formatChatTime(context), avatar = msg.faceUrl, unreadCount = msg.unreadCount, - trtcUserId = msg.userID + trtcUserId = msg.userID, + displayText = displayText, + isSelf = msg.lastMessage.sender == MyProfileViewModel.profile?.trtcUserId ) } ?: emptyList() } diff --git a/app/src/main/java/com/aiosman/riderpro/utils/File.kt b/app/src/main/java/com/aiosman/riderpro/utils/FileUtil.kt similarity index 86% rename from app/src/main/java/com/aiosman/riderpro/utils/File.kt rename to app/src/main/java/com/aiosman/riderpro/utils/FileUtil.kt index 73be24a..4895e88 100644 --- a/app/src/main/java/com/aiosman/riderpro/utils/File.kt +++ b/app/src/main/java/com/aiosman/riderpro/utils/FileUtil.kt @@ -2,6 +2,7 @@ package com.aiosman.riderpro.utils import android.content.ContentValues import android.content.Context +import android.database.Cursor import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.net.Uri @@ -9,7 +10,6 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import android.widget.Toast -import coil.ImageLoader import coil.request.ImageRequest import coil.request.SuccessResult import com.aiosman.riderpro.utils.Utils.getImageLoader @@ -18,7 +18,7 @@ import kotlinx.coroutines.withContext import java.io.FileNotFoundException import java.io.OutputStream -object File { +object FileUtil { suspend fun saveImageToGallery(context: Context, url: String) { val loader = getImageLoader(context) @@ -89,4 +89,17 @@ object File { } } + fun getRealPathFromUri(context: Context, uri: Uri): String? { + var realPath: String? = null + val projection = arrayOf(MediaStore.Images.Media.DATA) + val cursor: Cursor? = context.contentResolver.query(uri, projection, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + realPath = it.getString(columnIndex) + } + } + return realPath + } + } \ No newline at end of file