UI调整,群聊天室开发

This commit is contained in:
weber
2025-08-18 19:02:11 +08:00
parent 2de8127882
commit 791b24b2fb
8 changed files with 1108 additions and 110 deletions

View File

@@ -35,6 +35,7 @@ import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.crop.ImageCropScreen
@@ -94,6 +95,7 @@ sealed class NavigationRoute(
data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}")
data object ChatAi : NavigationRoute("ChatAi/{id}")
data object ChatGroup : NavigationRoute("ChatGroup/{id}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop")
data object AccountSetting : NavigationRoute("AccountSetting")
@@ -385,6 +387,20 @@ fun NavigationController(
}
}
composable(
route = NavigationRoute.ChatGroup.route,
arguments = listOf(navArgument("id") { type = NavType.StringType })
) {
val encodedId = it.arguments?.getString("id")
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
GroupChatScreen(decodedId?:"")
}
}
composable(route = NavigationRoute.CommentNoticeScreen.route) {
CompositionLocalProvider(
@@ -494,6 +510,14 @@ fun NavHostController.navigateToChatAi(id: String) {
)
}
fun NavHostController.navigateToGroupChat(id: String) {
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
navigate(
route = NavigationRoute.ChatGroup.route
.replace("{id}", encodedId)
)
}
fun NavHostController.goTo(

View File

@@ -0,0 +1,683 @@
package com.aiosman.ravenow.ui.chat
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
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
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
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
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DropdownMenu
import com.aiosman.ravenow.ui.composables.MenuItem
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.tencent.imsdk.v2.V2TIMMessage
import kotlinx.coroutines.launch
@Composable
fun GroupChatScreen(groupId: String) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current
val context = LocalNavController.current.context
val AppColors = LocalAppTheme.current
var goToNewCount by remember { mutableStateOf(0) }
val viewModel = viewModel<GroupChatViewModel>(
key = "GroupChatViewModel_$groupId",
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GroupChatViewModel(groupId) as T
}
}
)
LaunchedEffect(Unit) {
viewModel.init(context = context)
}
DisposableEffect(Unit) {
onDispose {
viewModel.UnRegistListener()
viewModel.clearUnRead()
}
}
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
var inBottom by remember { mutableStateOf(true) }
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
.collect { index ->
if (index == listState.layoutInfo.totalItemsCount - 1) {
coroutineScope.launch {
viewModel.onLoadMore(context)
}
}
}
}
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index }
.collect { index ->
inBottom = index == 0
if (index == 0) {
goToNewCount = 0
}
}
}
LaunchedEffect(viewModel.goToNew) {
if (viewModel.goToNew) {
if (inBottom) {
listState.scrollToItem(0)
} else {
goToNewCount++
}
viewModel.goToNew = false
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
) {
StatusBarSpacer()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
modifier = Modifier
.size(28.dp)
.noRippleClickable {
navController.navigateUp()
},
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(16.dp))
if (viewModel.groupAvatar.isNotEmpty()) {
CustomAsyncImage(
imageUrl = viewModel.groupAvatar,
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp)),
contentDescription = "群聊头像"
)
} else {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.decentBackground),
contentAlignment = Alignment.Center
) {
Text(
text = viewModel.groupName.take(1),
style = TextStyle(
color = AppColors.text,
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = viewModel.groupName,
style = TextStyle(
color = AppColors.text,
fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
)
Text(
text = "${viewModel.memberCount}",
style = TextStyle(
color = AppColors.secondaryText,
fontSize = 14.sp
)
)
}
Spacer(modifier = Modifier.weight(1f))
Box {
Image(
painter = painterResource(R.drawable.rider_pro_more_horizon),
modifier = Modifier
.size(28.dp)
.noRippleClickable {
isMenuExpanded = true
},
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
menuItems = listOf(
MenuItem(
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
) {
isMenuExpanded = false
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
}
}
},
MenuItem(
title = "群成员",
icon = R.drawable.rider_pro_more_horizon,
) {
isMenuExpanded = false
viewModel.getGroupMembers()
}
),
)
}
}
}
},
bottomBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.imePadding()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(AppColors.decentBackground)
)
Spacer(modifier = Modifier.height(8.dp))
GroupChatInput(
onSendImage = { uri ->
uri?.let {
viewModel.sendImageMessage(it, context)
}
},
) { message ->
viewModel.sendMessage(message, context)
}
}
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.decentBackground)
.padding(paddingValues)
) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
reverseLayout = true,
verticalArrangement = Arrangement.Top
) {
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
val item = chatList[index]
if (item.showTimeDivider) {
val calendar = java.util.Calendar.getInstance()
calendar.timeInMillis = item.timestamp
Text(
text = calendar.time.formatChatTime(context),
style = TextStyle(
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center
),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
}
GroupChatItem(item = item, viewModel.myProfile?.trtcUserId!!)
}
}
if (goToNewCount > 0) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 16.dp, end = 16.dp)
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(AppColors.background)
.padding(8.dp)
.noRippleClickable {
coroutineScope.launch {
listState.scrollToItem(0)
}
},
) {
Text(
text = "${goToNewCount} 条新消息",
style = TextStyle(
color = AppColors.text,
fontSize = 16.sp,
),
)
}
}
}
}
}
@Composable
fun GroupChatSelfItem(item: ChatItem) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
Column(
horizontalAlignment = androidx.compose.ui.Alignment.End,
) {
Text(
text = item.nickname,
style = TextStyle(
color = Color.Gray,
fontSize = 12.sp,
),
modifier = Modifier.padding(bottom = 2.dp)
)
Box(
modifier = Modifier
.widthIn(
min = 20.dp,
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF000000))
.padding(
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
)
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
) {
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 = "不支持的消息类型",
style = TextStyle(
color = Color.White,
fontSize = 16.sp,
)
)
}
}
}
}
Spacer(modifier = Modifier.width(12.dp))
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(40.dp))
) {
CustomAsyncImage(
imageUrl = item.avatar,
modifier = Modifier.fillMaxSize(),
contentDescription = "avatar"
)
}
}
}
}
@Composable
fun GroupChatOtherItem(item: ChatItem) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(40.dp))
) {
CustomAsyncImage(
imageUrl = item.avatar,
modifier = Modifier.fillMaxSize(),
contentDescription = "avatar"
)
}
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = item.nickname,
style = TextStyle(
color = AppColors.secondaryText,
fontSize = 12.sp,
),
modifier = Modifier.padding(bottom = 2.dp)
)
Box(
modifier = Modifier
.widthIn(
min = 20.dp,
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.background)
.padding(
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
)
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
) {
when (item.messageType) {
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
Text(
text = item.message,
style = TextStyle(
color = AppColors.text,
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 = "不支持的消息类型",
style = TextStyle(
color = AppColors.text,
fontSize = 16.sp,
)
)
}
}
}
}
}
}
}
@Composable
fun GroupChatItem(item: ChatItem, currentUserId: String) {
val isCurrentUser = item.userId == currentUserId
if (isCurrentUser) {
GroupChatSelfItem(item)
} else {
GroupChatOtherItem(item)
}
}
@Composable
fun GroupChatInput(
onSendImage: (Uri?) -> Unit = {},
onSend: (String) -> Unit = {},
) {
val navigationBarHeight = with(LocalDensity.current) {
WindowInsets.navigationBars.getBottom(this).toDp()
}
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
var isKeyboardOpen by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("") }
val appColors = LocalAppTheme.current
val inputBarHeight by animateDpAsState(
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
animationSpec = tween(
durationMillis = 300,
easing = androidx.compose.animation.core.LinearEasing
), label = ""
)
LaunchedEffect(isKeyboardOpen) {
inputBarHeight
}
val focusManager = LocalFocusManager.current
val windowInsets = WindowInsets.ime
val density = LocalDensity.current
val softwareKeyboardController = LocalSoftwareKeyboardController.current
val currentDensity by rememberUpdatedState(density)
LaunchedEffect(windowInsets.getBottom(currentDensity)) {
if (windowInsets.getBottom(currentDensity) <= 0) {
focusManager.clearFocus()
}
}
val imagePickUpLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
val uri = it.data?.data
onSendImage(uri)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = inputBarHeight)
) {
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(16.dp))
.background(appColors.background)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterStart,
) {
BasicTextField(
value = text,
onValueChange = {
text = it
},
textStyle = TextStyle(
color = appColors.text,
fontSize = 16.sp
),
cursorBrush = SolidColor(appColors.text),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.onFocusChanged { focusState ->
isKeyboardOpen = focusState.isFocused
}
.pointerInput(Unit) {
awaitPointerEventScope {
keyboardController = softwareKeyboardController
awaitFirstDown().also {
keyboardController?.show()
}
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
}
)
)
}
Spacer(modifier = Modifier.width(16.dp))
Icon(
painter = painterResource(id = R.drawable.rider_pro_camera),
contentDescription = "发送图片",
modifier = Modifier
.size(30.dp)
.noRippleClickable {
imagePickUpLauncher.launch(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
},
"选择图片"
)
)
},
tint = appColors.chatActionColor
)
Spacer(modifier = Modifier.width(8.dp))
Crossfade(
targetState = text.isNotEmpty(), animationSpec = tween(500),
label = ""
) { isNotEmpty ->
Icon(
painter = painterResource(id = R.drawable.rider_pro_video_share),
contentDescription = "发送消息",
modifier = Modifier
.size(32.dp)
.noRippleClickable {
if (text.isNotEmpty()) {
onSend(text)
text = ""
}
},
tint = if (isNotEmpty) appColors.main else appColors.chatActionColor
)
}
}
}
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: GroupChatViewModel): List<ChatItem> {
for (i in chatList.indices) {
if (i == 0) {
viewModel.showTimestampMap[chatList[i].msgId] = false
chatList[i].showTimeDivider = false
continue
}
val currentMessage = chatList[i]
val timeDiff = currentMessage.timestamp - chatList[i - 1].timestamp
if (-timeDiff > 30 * 60 * 1000) {
viewModel.showTimestampMap[currentMessage.msgId] = true
currentMessage.showTimeDivider = true
}
}
return chatList
}

View File

@@ -0,0 +1,283 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
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
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.entity.ChatNotification
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
import com.tencent.imsdk.v2.V2TIMCallback
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
class GroupChatViewModel(
val groupId: String,
) : ViewModel() {
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
var groupInfo by mutableStateOf<GroupInfo?>(null)
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
val userService: UserService = UserServiceImpl()
val accountService: AccountService = AccountServiceImpl()
var textMessageListener: V2TIMAdvancedMsgListener? = null
var hasMore by mutableStateOf(true)
var isLoading by mutableStateOf(false)
var lastMessage: V2TIMMessage? = null
val showTimestampMap = mutableMapOf<String, Boolean>()
var chatNotification by mutableStateOf<ChatNotification?>(null)
var goToNew by mutableStateOf(false)
// 群聊特有属性
var memberCount by mutableStateOf(0)
var groupName by mutableStateOf("")
var groupAvatar by mutableStateOf("")
data class GroupInfo(
val groupId: String,
val groupName: String,
val groupAvatar: String,
val memberCount: Int,
val ownerId: String
)
fun init(context: Context) {
viewModelScope.launch {
try {
getGroupInfo()
myProfile = accountService.getMyAccountProfile()
RegistListener(context)
fetchHistoryMessage(context)
val notiStrategy = ChatState.getStrategyByTargetTrtcId(groupId)
chatNotification = notiStrategy
} catch (e: Exception) {
Log.e("GroupChatViewModel", "初始化失败: ${e.message}")
}
}
}
private suspend fun getGroupInfo() {
// 简化群组信息获取,使用默认信息
groupInfo = GroupInfo(
groupId = groupId,
groupName = "群聊 $groupId",
groupAvatar = "",
memberCount = 0,
ownerId = ""
)
groupName = groupInfo?.groupName ?: ""
groupAvatar = groupInfo?.groupAvatar ?: ""
memberCount = groupInfo?.memberCount ?: 0
}
fun RegistListener(context: Context) {
textMessageListener = object : V2TIMAdvancedMsgListener() {
override fun onRecvNewMessage(msg: V2TIMMessage?) {
super.onRecvNewMessage(msg)
msg?.let {
if (it.groupID == groupId) {
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = null)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
}
}
V2TIMManager.getMessageManager().addAdvancedMsgListener(textMessageListener)
}
fun UnRegistListener() {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener)
}
fun clearUnRead() {
val conversationID = "group_${groupId}"
V2TIMManager.getConversationManager()
.cleanConversationUnreadMessageCount(conversationID, 0, 0, object : V2TIMCallback {
override fun onSuccess() {
Log.i("imsdk", "清除群聊未读消息成功")
}
override fun onError(code: Int, desc: String) {
Log.i("imsdk", "清除群聊未读消息失败, code:$code, desc:$desc")
}
})
}
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) return
isLoading = true
viewModelScope.launch {
V2TIMManager.getMessageManager().getGroupHistoryMessageList(
groupId,
20,
lastMessage,
object : V2TIMValueCallback<List<V2TIMMessage>> {
override fun onSuccess(p0: List<V2TIMMessage>?) {
chatData = chatData + (p0 ?: emptyList()).map {
ChatItem.convertToChatItem(it, context, avatar = null)
}.filterNotNull()
if ((p0?.size ?: 0) < 20) {
hasMore = false
}
lastMessage = p0?.lastOrNull()
isLoading = false
}
override fun onError(p0: Int, p1: String?) {
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $p1")
isLoading = false
}
}
)
}
}
fun sendMessage(message: String, context: Context) {
val v2TIMMessage = V2TIMManager.getMessageManager().createTextMessage(message)
V2TIMManager.getMessageManager().sendMessage(
v2TIMMessage,
groupId,
null,
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
false,
null,
object : V2TIMSendCallback<V2TIMMessage> {
override fun onProgress(p0: Int) {}
override fun onError(p0: Int, p1: String?) {
Log.e("GroupChatViewModel", "发送群聊消息失败: $p1")
}
override fun onSuccess(p0: V2TIMMessage?) {
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
)
}
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,
groupId,
null,
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
false,
null,
object : V2TIMSendCallback<V2TIMMessage> {
override fun onProgress(p0: Int) {}
override fun onError(p0: Int, p1: String?) {
Log.e("GroupChatViewModel", "发送群聊图片消息失败: $p1")
}
override fun onSuccess(p0: V2TIMMessage?) {
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
)
}
}
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().getGroupHistoryMessageList(
groupId,
20,
null,
object : V2TIMValueCallback<List<V2TIMMessage>> {
override fun onSuccess(p0: List<V2TIMMessage>?) {
chatData = (p0 ?: emptyList()).mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = null)
}
if ((p0?.size ?: 0) < 20) {
hasMore = false
}
lastMessage = p0?.lastOrNull()
}
override fun onError(p0: Int, p1: String?) {
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $p1")
}
}
)
}
fun getDisplayChatList(): List<ChatItem> {
val list = chatData
for (item in list) {
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
}
return list
}
suspend fun updateNotificationStrategy(strategy: String) {
val result = ChatState.updateChatNotification(groupId.hashCode(), strategy)
chatNotification = result
}
val notificationStrategy get() = chatNotification?.strategy ?: "default"
// 群聊特有功能
fun getGroupMembers() {
// 简化群成员获取,暂时只记录日志
Log.d("GroupChatViewModel", "获取群成员功能待实现")
}
}

View File

@@ -15,6 +15,7 @@ import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.navigateToChat
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.tencent.imsdk.v2.V2TIMConversation
import com.tencent.imsdk.v2.V2TIMConversationListFilter
import com.tencent.imsdk.v2.V2TIMConversationResult
@@ -168,7 +169,7 @@ object GroupChatListViewModel : ViewModel() {
viewModelScope.launch {
try {
// 群聊直接使用群ID进行导航
//navController.navigateToChat(conversation.groupId)
navController.navigateToGroupChat(conversation.groupId)
} catch (e: Exception) {
error = ""
e.printStackTrace()

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
@@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@@ -58,6 +60,7 @@ import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.AgentCard
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.post.NewPostViewModel
// Banner数据类
@@ -74,9 +77,9 @@ data class BannerItem(
fun fromRoom(room: Room): BannerItem {
return BannerItem(
id = room.id,
title = room.name ?: "聊天室",
subtitle = room.description ?: "欢迎加入我们的聊天室",
imageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.creator.profile.avatar}"+"?token="+"${AppStore.token}" ?: "",
title = room.name ,
subtitle = room.description ,
imageUrl = "${ApiClient.RETROFIT_URL}${room.creator.profile.avatar}"+"?token="+"${AppStore.token}" ?: "",
backgroundImageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.recommendBanner}"+"?token="+"${AppStore.token}" ?: "",
userCount = room.userCount,
agentName = room.creator.profile.nickname
@@ -138,6 +141,7 @@ fun Explore() {
}
)
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCard2(agentItem: AgentItem) {
val AppColors = LocalAppTheme.current
@@ -192,14 +196,14 @@ fun Explore() {
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
// 描述
Text(
text = agentItem.desc,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 2,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
@@ -316,17 +320,19 @@ fun Explore() {
verticalAlignment = Alignment.CenterVertically
) {
// 左侧图标
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.size(24.dp)
.padding(start = 16.dp),
.size(24.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.rider_pro_group_chat),
contentDescription = "chat",
modifier = Modifier.size(12.dp),
Icon(
painter = painterResource(id = R.drawable.rider_pro_group_chat),
contentDescription = null,
modifier = Modifier
.size(24.dp),
tint = AppColors.text
)
}
@@ -362,13 +368,13 @@ fun Explore() {
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp)
) {
repeat(3) { columnIndex ->
val itemIndex = rowIndex * 3 + columnIndex
val itemIndex = rowIndex * 2 + columnIndex
if (itemIndex < roomItems.size) {
val roomItem = roomItems[itemIndex]
HotChatRoomGridItem(roomItem = roomItem)
} else {
// 填充空白占位
Spacer(modifier = Modifier.width(80.dp))
//Spacer(modifier = Modifier.width(80.dp))
}
}
}
@@ -393,9 +399,9 @@ fun Explore() {
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 0.dp),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp)
) {
repeat(gridCount) { gridIndex ->
repeat(1) { gridIndex ->
item {
HotChatRoomGrid(roomItems = roomItems.drop(gridIndex * totalItems).take(totalItems))
HotChatRoomGrid(roomItems = viewModel.bannerItems.take(8))
}
}
}
@@ -473,6 +479,8 @@ fun Explore() {
Text(
text = bannerItem.title,
fontSize = 24.sp,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(start = 16.dp)
@@ -484,12 +492,13 @@ fun Explore() {
color = Color.White.copy(alpha = 0.9f),
maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.padding(start = 16.dp)
modifier = Modifier.padding(start = 16.dp, end = 16.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth().background(brush = androidx.compose.ui.graphics.Brush.verticalGradient(
modifier = Modifier.fillMaxWidth()
.background(brush = androidx.compose.ui.graphics.Brush.verticalGradient(
colors = listOf(
Color(0x00000000), // 底部颜色(透明)
Color(0x33000000), // 顶部颜色
@@ -499,60 +508,52 @@ fun Explore() {
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Agent信息
Box(
modifier = Modifier
.wrapContentWidth()
.padding(8.dp),
contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row( modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically){
// 左侧头像
Box(
modifier = Modifier
.size(24.dp)
.background(
Color.White.copy(alpha = 0.2f),
RoundedCornerShape(16.dp)
).blur(6.dp),
contentAlignment = Alignment.Center
)
) {
CustomAsyncImage(
context = context,
imageUrl = bannerItem.imageUrl,
contentDescription = "agent Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
)
modifier = Modifier.fillMaxSize()
.clip(RoundedCornerShape(24.dp)))
}
Spacer(modifier = Modifier.width(8.dp))
Column {
// 中间信息区域占比1
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Text(
text = bannerItem.agentName,
fontSize = 10.sp,
color = Color(0xfff5f5f5).copy(alpha = 0.6f),
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
// 新增的文字行
Text(
text = bannerItem.subtitle,
fontSize = 12.sp,
color = Color.White,
maxLines = 1,
fontWeight = androidx.compose.ui.text.font.FontWeight.W400,
modifier = Modifier.padding(top = 2.dp)
)
}
}
}
Column (
modifier = Modifier
.wrapContentWidth()
.padding(end = 16.dp)
){
// 进入按钮
// 右侧进入按钮
Box(
modifier = Modifier
.width(69.dp)
@@ -573,7 +574,9 @@ fun Explore() {
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
)
}
}
}
}
}
@@ -810,12 +813,11 @@ fun BannerSection(bannerItems: List<BannerItem>) {
painter = painterResource(R.mipmap.rider_pro_fire2),
contentDescription = "fire",
modifier = Modifier.size(28.dp),
colorFilter = ColorFilter.tint(Color(0xFFEF4444))
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "正在高能对话中",
fontSize = 20.sp,
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
@@ -854,7 +856,7 @@ fun BannerSection(bannerItems: List<BannerItem>) {
}
// Agent ViewPager
AgentViewPagerSection(agentItems = viewModel.agentItems)
AgentViewPagerSection(agentItems = viewModel.agentItems.take(9))
}
}
@@ -874,7 +876,6 @@ fun BannerSection(bannerItems: List<BannerItem>) {
painter = painterResource(R.mipmap.rider_pro_hot_room),
contentDescription = "chat room",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color(0xFFFFA500))
)
Spacer(modifier = Modifier.width(4.dp))
Text(

View File

@@ -69,6 +69,7 @@ fun HotMomentsList() {
modifier = Modifier
.fillMaxSize()
.pullRefresh(state)
) {
Column(
modifier = Modifier.fillMaxWidth().background(
@@ -80,6 +81,7 @@ fun HotMomentsList() {
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(2.dp)
) {
DiscoverView()
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
@@ -107,7 +109,6 @@ fun DiscoverView() {
.fillMaxWidth()
.aspectRatio(1f)
.padding(2.dp)
.clip(RoundedCornerShape(8.dp))
.noRippleClickable {
navController.navigateToPost(
id = momentItem.id,

View File

@@ -5,19 +5,24 @@
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:translateX="3"
android:translateY="4">
<path
android:fillType="evenOdd"
android:strokeColor="#110C13"
android:strokeWidth="1.432"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M8.804 5.458a3.58 3.58 0 0 1 3.074-1.046l5.454 0.797 c2.008 0.293 3.407 2.224 3.125 4.312l-1.022 7.56-1.167-0.17" />
android:pathData="M5.14899667,2.26161678 C5.81639662,1.36500291 6.86507369,0.791250947 8.04718047,0.797664676 L13.5589051,0.827875965 C15.588266,0.838979528 17.2423894,2.5558848 17.2534929,4.66269122 L17.2937025,12.2921122 L16.1141693,12.2856584" />
<path
android:fillColor="#110C13"
android:fillType="evenOdd"
android:pathData="M6.494 14.375 7.348-0.04-0.01 1.907-7.349 0.04 z" />
android:strokeWidth="1"
android:pathData="M 3.49352162 10.3748471 L 10.8424878 10.3346375 L 10.8324354 12.2419928 L 3.48346923 12.2822023 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#110C13"
android:strokeWidth="1.432"
android:pathData="M6.77 8.113 5.171-0.756a3.818 3.818 0 0 1 4.335 3.266l0.51 3.775a3.818 3.818 0 0 1-3.23 4.289L4.565 20 3.54 12.402a3.818 3.818 0 0 1 3.23-4.29z" />
android:strokeWidth="1.5"
android:pathData="M4.53948169,3.69210555 L9.7647742,3.66351559 C11.8731096,3.65197992 13.5916046,5.35177192 13.6031402,7.46010728 C13.603215,7.47377603 13.6032164,7.48744509 13.6031444,7.50111385 L13.5830711,11.3098388 C13.5720438,13.4021803 11.8788145,15.0957501 9.78647531,15.1071983 L0.702475953,15.156901 L0.702475953,15.156901 L0.742885864,7.48946504 C0.753913192,5.39712355 2.44714246,3.7035537 4.53948169,3.69210555 Z" />
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB