新增图片发送

This commit is contained in:
2024-10-09 23:49:20 +08:00
parent ce606e090b
commit ea26d67b90
6 changed files with 270 additions and 52 deletions

View File

@@ -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)
) {
when (item.messageType) {
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
Text(
text = item.message,
style = TextStyle(
color = Color.White,
fontSize = 16.sp,
),
textAlign = TextAlign.Start,
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,14 +339,35 @@ fun ChatOtherItem(item: ChatItem) {
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 3.dp)
) {
when (item.messageType) {
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
Text(
text = item.message,
style = TextStyle(
color = Color.Black,
fontSize = 16.sp
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(

View File

@@ -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<V2TIMImageElem.V2TIMImage> = emptyList<V2TIMImageElem.V2TIMImage>().toMutableList(),
val messageType : Int = 0,
val textDisplay : String = ""
)
class ChatViewModel(
@@ -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,6 +85,7 @@ class ChatViewModel(
fun UnRegistListener() {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener);
}
fun clearUnRead() {
val conversationID = "c2c_${userProfile?.trtcUserId}"
V2TIMManager.getConversationManager()
@@ -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,16 +114,51 @@ 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 = message.textElem.text,
message = "Image",
avatar = avatar,
time = calendar.time.formatChatTime(context),
userId = message.sender,
nickname = nickname,
timestamp = timestamp * 1000
timestamp = timestamp * 1000,
imageList = message.imageElem?.imageList
?: emptyList<V2TIMImageElem.V2TIMImage>().toMutableList(),
messageType = V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE,
textDisplay = "Image"
)
}
return null
}
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<V2TIMImageElem.V2TIMImage>().toMutableList(),
messageType = V2TIMMessage.V2TIM_ELEM_TYPE_TEXT,
textDisplay = message.textElem?.text ?: "Unsupported message type"
)
}
else -> {
return null
}
}
}
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) {
@@ -122,7 +174,7 @@ class ChatViewModel(
override fun onSuccess(p0: List<V2TIMMessage>?) {
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<V2TIMMessage> {
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<V2TIMMessage>?) {
chatData = (p0 ?: emptyList()).map {
convertToChatItem(it, context)
}
}.filterNotNull()
if ((p0?.size ?: 0) < 20) {
hasMore = false
}
@@ -182,6 +302,7 @@ class ChatViewModel(
}
)
}
fun getDisplayChatList(): List<ChatItem> {
return chatData
}

View File

@@ -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

View File

@@ -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),

View File

@@ -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()
}

View File

@@ -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
}
}