UI调整,群聊开发

This commit is contained in:
weber
2025-08-20 19:19:14 +08:00
parent 791b24b2fb
commit 8f8c2ff2e9
27 changed files with 709 additions and 513 deletions

View File

@@ -33,6 +33,7 @@ open class AppThemeData(
var tabUnselectedBackground: Color,
var tabSelectedText: Color,
var tabUnselectedText: Color,
var bubbleBackground: Color,
)
class LightThemeColors : AppThemeData(
@@ -60,10 +61,10 @@ class LightThemeColors : AppThemeData(
chatActionColor = Color(0xffe0e0e0),
brandColorsColor = Color(0xffD80264),
tabSelectedBackground = Color(0xff110C13),
tabUnselectedBackground = Color(0xff7C7480),
tabUnselectedBackground = Color(0x147C7480),
tabSelectedText = Color(0xffffffff),
tabUnselectedText = Color(0xff000000),
bubbleBackground = Color(0xfff5f5f5)
)
class DarkThemeColors : AppThemeData(
@@ -94,4 +95,5 @@ class DarkThemeColors : AppThemeData(
tabUnselectedBackground = Color(0xff7C7480),
tabSelectedText = Color(0xff000000),
tabUnselectedText = Color(0xffffffff),
bubbleBackground = Color(0xfff2d2c2e)
)

View File

@@ -44,19 +44,27 @@ data class SingleChatRequestBody(
val generateText: String,
)
data class GroupChatRequestBody(
@SerializedName("trtcGroupId")
val trtcGroupId: String,
)
data class SendChatAiRequestBody(
@SerializedName("trtcGroupId")
val trtcGroupId: String? = null,
@SerializedName("fromTrtcUserId")
val fromTrtcUserId: String,
val fromTrtcUserId: String? = null,
@SerializedName("toTrtcUserId")
val toTrtcUserId: String,
val toTrtcUserId: String? = null,
@SerializedName("message")
val message: String,
@SerializedName("skipTrtc")
val skipTrtc: Boolean,
val skipTrtc: Boolean? = true,
)
data class CreateGroupChatRequestBody(
@SerializedName("name")
val name: String,
@@ -420,6 +428,8 @@ interface RaveNowAPI {
@Query("nickname") search: String? = null,
@Query("followerId") followerId: Int? = null,
@Query("followingId") followingId: Int? = null,
@Query("includeAI") includeAI: Boolean? = false,
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
): Response<ListContainer<AccountProfile>>
@POST("register/google")
@@ -533,6 +543,9 @@ interface RaveNowAPI {
@POST("generate/postText")
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
@GET("outside/rooms/open")
suspend fun createGroupChatAi(@Query("trtcGroupId") trtcGroupId: String): Response<DataContainer<Unit>>
@POST("outside/rooms/create-single-chat")
suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>>

View File

@@ -95,7 +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 ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop")
data object AccountSetting : NavigationRoute("AccountSetting")
@@ -389,15 +389,18 @@ fun NavigationController(
composable(
route = NavigationRoute.ChatGroup.route,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType },
navArgument("name") { type = NavType.StringType },
navArgument("avatar") { type = NavType.StringType })
) {
val encodedId = it.arguments?.getString("id")
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
val name = it.arguments?.getString("name")
val avatar = it.arguments?.getString("avatar")
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
GroupChatScreen(decodedId?:"")
GroupChatScreen(decodedId?:"",name?:"",avatar?:"")
}
}
@@ -432,24 +435,12 @@ fun NavigationController(
}
composable(
route = NavigationRoute.AddAgent.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
},
exitTransition = {
fadeOut(animationSpec = tween(durationMillis = 0))
}
) {
AddAgentScreen()
}
composable(
route = NavigationRoute.CreateGroupChat.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
},
exitTransition = {
fadeOut(animationSpec = tween(durationMillis = 0))
}
) {
CreateGroupChatScreen()
}
@@ -500,6 +491,7 @@ fun NavHostController.navigateToChat(id: String) {
navigate(
route = NavigationRoute.Chat.route
.replace("{id}", id)
)
}
@@ -510,11 +502,15 @@ fun NavHostController.navigateToChatAi(id: String) {
)
}
fun NavHostController.navigateToGroupChat(id: String) {
fun NavHostController.navigateToGroupChat(id: String,name:String,avatar:String) {
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
val encodedName = java.net.URLEncoder.encode(name, "UTF-8")
val encodedAvator = java.net.URLEncoder.encode(avatar, "UTF-8")
navigate(
route = NavigationRoute.ChatGroup.route
.replace("{id}", encodedId)
.replace("{name}", encodedName)
.replace("{avatar}", encodedAvator)
)
}

View File

@@ -64,7 +64,7 @@ fun AddAgentScreen() {
var errorMessage by remember { mutableStateOf<String?>(null) }
fun onNameChange(value: String) {
model.name = value
model.name = value.trim()
agnetNameError = when {
else -> null
}
@@ -73,7 +73,7 @@ fun AddAgentScreen() {
val appColors = LocalAppTheme.current
fun onDescChange(value: String) {
model.desc = value
model.desc = value.trim()
agnetDescError = when {
value.length > 100 -> "简介长度不能大于100"
else -> null
@@ -232,6 +232,7 @@ fun AddAgentScreen() {
// 创建成功,关闭页面
model.name = ""
model.desc = ""
model.isFromAddAgent = false // 重置标志
navController.popBackStack()
}
} catch (e: Exception) {

View File

@@ -58,6 +58,10 @@ object AddAgentViewModel : ViewModel() {
)
println("AddAgentViewModel: Agent created successfully with ID: ${result.id}")
// 通知相关ViewModel更新列表
notifyAgentCreated(result)
return result
} catch (e: Exception) {
println("AddAgentViewModel: Error creating agent: ${e.message}")
@@ -66,6 +70,11 @@ object AddAgentViewModel : ViewModel() {
isUpdating = false
}
}
private fun notifyAgentCreated(agent: AgentEntity) {
// 通知我的智能体列表更新
com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.addAgentToList(agent)
}
fun validate(): String? {
return when {

View File

@@ -83,6 +83,7 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.tencent.imsdk.v2.V2TIMMessage
import kotlinx.coroutines.launch
import java.util.UUID
@Composable
@@ -280,7 +281,7 @@ fun ChatAiScreen(userId: String) {
verticalArrangement = Arrangement.Top
) {
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
items(chatList.size, key = { index -> chatList[index].msgId + UUID.randomUUID().toString()}) { index ->
val item = chatList[index]
if (item.showTimeDivider) {
val calendar = java.util.Calendar.getInstance()

View File

@@ -68,11 +68,22 @@ class ChatAiViewModel(
textMessageListener = object : V2TIMAdvancedMsgListener() {
override fun onRecvNewMessage(msg: V2TIMMessage?) {
super.onRecvNewMessage(msg)
msg?.let {
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = userProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
msg?.let { message ->
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
if ((message.userID == currentChatUserId || message.userID == currentUserId) &&
message.sender != currentUserId) {
val chatItem = ChatItem.convertToChatItem(message, context, avatar = userProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
android.util.Log.i("ChatAiViewModel", "收到来自 ${message.sender} 的消息更新AI聊天列表")
}
}
}
}
}
@@ -260,7 +271,7 @@ class ChatAiViewModel(
message: String,
) {
viewModelScope.launch {
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(fromTrtcUserId = fromTrtcUserId,toTrtcUserId = toTrtcUserId,message = message,skipTrtc = true))
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(fromTrtcUserId = fromTrtcUserId,toTrtcUserId = toTrtcUserId,message = message))
}
}

View File

@@ -83,6 +83,7 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.tencent.imsdk.v2.V2TIMMessage
import kotlinx.coroutines.launch
import java.util.UUID
@Composable
@@ -280,7 +281,7 @@ fun ChatScreen(userId: String) {
verticalArrangement = Arrangement.Top
) {
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
items(chatList.size, key = { index -> chatList[index].msgId + UUID.randomUUID().toString()}) { index ->
val item = chatList[index]
if (item.showTimeDivider) {
val calendar = java.util.Calendar.getInstance()

View File

@@ -65,11 +65,22 @@ class ChatViewModel(
textMessageListener = object : V2TIMAdvancedMsgListener() {
override fun onRecvNewMessage(msg: V2TIMMessage?) {
super.onRecvNewMessage(msg)
msg?.let {
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = userProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
msg?.let { message ->
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
if ((message.userID == currentChatUserId || message.userID == currentUserId) &&
message.sender != currentUserId) {
val chatItem = ChatItem.convertToChatItem(message, context, avatar = userProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
android.util.Log.i("ChatViewModel", "收到来自 ${message.sender} 的消息,更新聊天列表")
}
}
}
}
}

View File

@@ -65,6 +65,7 @@ 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
@@ -83,9 +84,10 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.tencent.imsdk.v2.V2TIMMessage
import kotlinx.coroutines.launch
import java.util.UUID
@Composable
fun GroupChatScreen(groupId: String) {
fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current
val context = LocalNavController.current.context
@@ -96,7 +98,7 @@ fun GroupChatScreen(groupId: String) {
key = "GroupChatViewModel_$groupId",
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GroupChatViewModel(groupId) as T
return GroupChatViewModel(groupId,name,avatar) as T
}
}
)
@@ -167,7 +169,7 @@ fun GroupChatScreen(groupId: String) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
modifier = Modifier
.size(28.dp)
.size(24.dp)
.noRippleClickable {
navController.navigateUp()
},
@@ -180,7 +182,7 @@ fun GroupChatScreen(groupId: String) {
CustomAsyncImage(
imageUrl = viewModel.groupAvatar,
modifier = Modifier
.size(40.dp)
.size(32.dp)
.clip(RoundedCornerShape(8.dp)),
contentDescription = "群聊头像"
)
@@ -193,12 +195,16 @@ fun GroupChatScreen(groupId: String) {
contentAlignment = Alignment.Center
) {
Text(
text = viewModel.groupName.take(1),
text = viewModel.groupName,
style = TextStyle(
color = AppColors.text,
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@@ -213,13 +219,7 @@ fun GroupChatScreen(groupId: String) {
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))
@@ -304,7 +304,7 @@ fun GroupChatScreen(groupId: String) {
verticalArrangement = Arrangement.Top
) {
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
items(chatList.size, key = { index -> chatList[index].msgId + UUID.randomUUID().toString()}) { index ->
val item = chatList[index]
if (item.showTimeDivider) {
val calendar = java.util.Calendar.getInstance()
@@ -369,7 +369,7 @@ fun GroupChatSelfItem(item: ChatItem) {
Column(
horizontalAlignment = androidx.compose.ui.Alignment.End,
) {
Text(
/* Text(
text = item.nickname,
style = TextStyle(
color = Color.Gray,
@@ -377,15 +377,15 @@ fun GroupChatSelfItem(item: ChatItem) {
),
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))
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFF6246FF))
.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)
@@ -398,7 +398,7 @@ fun GroupChatSelfItem(item: ChatItem) {
text = item.message,
style = TextStyle(
color = Color.White,
fontSize = 16.sp,
fontSize = 14.sp,
),
textAlign = TextAlign.Start
)
@@ -417,25 +417,25 @@ fun GroupChatSelfItem(item: ChatItem) {
text = "不支持的消息类型",
style = TextStyle(
color = Color.White,
fontSize = 16.sp,
fontSize = 14.sp,
)
)
}
}
}
}
Spacer(modifier = Modifier.width(12.dp))
/*Spacer(modifier = Modifier.width(12.dp))
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(40.dp))
.size(24.dp)
.clip(RoundedCornerShape(24.dp))
) {
CustomAsyncImage(
imageUrl = item.avatar,
modifier = Modifier.fillMaxSize(),
contentDescription = "avatar"
)
}
}*/
}
}
}
@@ -455,11 +455,11 @@ fun GroupChatOtherItem(item: ChatItem) {
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(40.dp))
.size(24.dp)
.clip(RoundedCornerShape(24.dp))
) {
CustomAsyncImage(
imageUrl = item.avatar,
imageUrl = item.avatar.replace("storage/avatars/", "/avatar/"),
modifier = Modifier.fillMaxSize(),
contentDescription = "avatar"
)
@@ -481,8 +481,8 @@ fun GroupChatOtherItem(item: ChatItem) {
min = 20.dp,
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.background)
.clip(RoundedCornerShape(20.dp))
.background(AppColors.bubbleBackground)
.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)
@@ -495,7 +495,7 @@ fun GroupChatOtherItem(item: ChatItem) {
text = item.message,
style = TextStyle(
color = AppColors.text,
fontSize = 16.sp,
fontSize = 14.sp,
),
textAlign = TextAlign.Start
)
@@ -528,10 +528,47 @@ fun GroupChatOtherItem(item: ChatItem) {
@Composable
fun GroupChatItem(item: ChatItem, currentUserId: String) {
val isCurrentUser = item.userId == currentUserId
if (isCurrentUser) {
GroupChatSelfItem(item)
} else {
GroupChatOtherItem(item)
// 管理员消息显示特殊布局
if (item.userId == "administrator") {
GroupChatAdminItem(item)
return
}
// 根据是否是当前用户显示不同样式
when (item.userId) {
currentUserId -> GroupChatSelfItem(item)
else -> GroupChatOtherItem(item)
}
}
@Composable
fun GroupChatAdminItem(item: ChatItem) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 50.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(vertical = 8.dp, horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = item.message,
style = TextStyle(
color = AppColors.secondaryText,
fontSize = 12.sp,
textAlign = TextAlign.Center
),
maxLines = Int.MAX_VALUE
)
}
}
}

View File

@@ -15,6 +15,10 @@ 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.data.api.ApiClient
import com.aiosman.ravenow.data.api.GroupChatRequestBody
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.entity.ChatNotification
@@ -31,6 +35,8 @@ import java.io.InputStream
class GroupChatViewModel(
val groupId: String,
val name: String,
val avatar: String,
) : ViewModel() {
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
var groupInfo by mutableStateOf<GroupInfo?>(null)
@@ -77,8 +83,8 @@ class GroupChatViewModel(
// 简化群组信息获取,使用默认信息
groupInfo = GroupInfo(
groupId = groupId,
groupName = "群聊 $groupId",
groupAvatar = "",
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
@@ -156,8 +162,8 @@ class GroupChatViewModel(
val v2TIMMessage = V2TIMManager.getMessageManager().createTextMessage(message)
V2TIMManager.getMessageManager().sendMessage(
v2TIMMessage,
groupId,
null,
groupId,
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
false,
null,
@@ -167,6 +173,7 @@ class GroupChatViewModel(
Log.e("GroupChatViewModel", "发送群聊消息失败: $p1")
}
override fun onSuccess(p0: V2TIMMessage?) {
sendChatAiMessage(message = message, trtcGroupId = groupId)
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
@@ -177,6 +184,18 @@ class GroupChatViewModel(
)
}
fun sendChatAiMessage(
trtcGroupId: String,
message: String,
) {
viewModelScope.launch {
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(trtcGroupId = trtcGroupId,message = message))
}
}
fun sendImageMessage(imageUri: Uri, context: Context) {
val tempFile = createTempFile(context, imageUri)
val imagePath = tempFile?.path

View File

@@ -59,15 +59,20 @@ fun AgentCard(
Row(
modifier = Modifier
) {
CustomAsyncImage(
context,
agentEntity.avatar,
contentDescription = "",
// 使用remember基于agentEntity.id来缓存图片避免滑动时重复加载
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(40.dp)),
contentScale = ContentScale.Crop
)
.clip(RoundedCornerShape(40.dp))
) {
CustomAsyncImage(
LocalContext.current,
agentEntity.avatar,
contentDescription = "",
modifier = Modifier.size(40.dp),
contentScale = ContentScale.Crop
)
}
Column(
modifier = Modifier
.weight(1f)

View File

@@ -40,42 +40,6 @@ object CreateGroupChatViewModel : ViewModel() {
var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null)
// 获取AI智能体列表
suspend fun getAiAgents(): List<GroupMember> {
return try {
// TODO: 从API获取AI智能体列表
listOf(
GroupMember("1", "AI助手", "https://example.com/avatar1.jpg"),
GroupMember("2", "智能客服", "https://example.com/avatar2.jpg"),
GroupMember("3", "翻译助手", "https://example.com/avatar3.jpg"),
GroupMember("4", "写作助手", "https://example.com/avatar4.jpg"),
GroupMember("5", "编程助手", "https://example.com/avatar5.jpg"),
GroupMember("6", "设计助手", "https://example.com/avatar6.jpg")
)
} catch (e: Exception) {
errorMessage = "获取AI智能体列表失败: ${e.message}"
emptyList()
}
}
// 获取朋友列表
suspend fun getFriends(): List<GroupMember> {
return try {
// TODO: 从API获取朋友列表
listOf(
GroupMember("7", "张三", "https://example.com/avatar7.jpg"),
GroupMember("8", "李四", "https://example.com/avatar8.jpg"),
GroupMember("9", "王五", "https://example.com/avatar9.jpg"),
GroupMember("10", "赵六", "https://example.com/avatar10.jpg"),
GroupMember("11", "钱七", "https://example.com/avatar11.jpg"),
GroupMember("12", "孙八", "https://example.com/avatar12.jpg")
)
} catch (e: Exception) {
errorMessage = "获取朋友列表失败: ${e.message}"
emptyList()
}
}
// 创建群聊
suspend fun createGroupChat(
groupName: String,

View File

@@ -269,79 +269,68 @@ fun IndexScreen() {
targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text,
animationSpec = tween(durationMillis = 250), label = ""
)
NavigationBarItem(
modifier = Modifier.padding(top = 2.dp),
selected = isSelected,
onClick = {
if (it.route === NavigationItem.Add.route) {
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
return@NavigationBarItem
}
coroutineScope.launch {
pagerState.scrollToPage(idx)
}
model.tabIndex = idx
},
colors = NavigationBarItemColors(
selectedIconColor = Color.Transparent,
selectedTextColor = Color.Transparent,
selectedIndicatorColor = Color.Transparent,
unselectedIconColor = Color.Transparent,
unselectedTextColor = Color.Transparent,
disabledIconColor = Color.Transparent,
disabledTextColor = Color.Transparent
),
icon = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (it.route == NavigationItem.Add.route) {
// Add按钮只显示大图标
Box(
modifier = Modifier
.weight(1f)
.padding(top = 2.dp)
.noRippleClickable {
if (it.route === NavigationItem.Add.route) {
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
return@noRippleClickable
}
coroutineScope.launch {
pagerState.scrollToPage(idx)
}
model.tabIndex = idx
},
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (it.route == NavigationItem.Add.route) {
// Add按钮只显示大图标
Image(
painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
contentDescription = it.label(),
modifier = Modifier.size(32.dp),
colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
)
} else {
// 其他按钮:图标+文字
Box(
modifier = Modifier
.width(48.dp)
.height(32.dp)
.background(
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
contentDescription = it.label(),
modifier = Modifier.size(32.dp),
modifier = Modifier.size(24.dp),
colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
)
} else {
// 其他按钮:图标+文字
Box(
modifier = Modifier
.width(48.dp)
.height(32.dp)
.background(
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
contentDescription = it.label(),
modifier = Modifier.size(24.dp),
colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
)
}
// 文字标签,可控制间距
Spacer(modifier = Modifier.height(1.dp))
Text(
text = it.label(),
fontSize = 10.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
)
}
// 文字标签,可控制间距
Spacer(modifier = Modifier.height(1.dp))
Text(
text = it.label(),
fontSize = 10.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
)
}
},
label = {
// 不显示默认标签
}
)
}
}
}

View File

@@ -104,7 +104,8 @@ fun Agent() {
modifier = Modifier
.size(36.dp)
.noRippleClickable {
//
// 设置标志,表示新增智能体后不需要刷新
com.aiosman.ravenow.ui.agent.AddAgentViewModel.isFromAddAgent = true
navController.navigate(
NavigationRoute.AddAgent.route
)
@@ -168,7 +169,8 @@ fun Agent() {
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.weight(1f),
beyondBoundsPageCount = 1 // 预加载相邻页面,避免切换时重新加载
) {
when (it) {
0 -> {

View File

@@ -25,9 +25,11 @@ open class BaseAgentModel :ViewModel(){
var refreshing by mutableStateOf(false)
var isFirstLoad = true
var agentList by mutableStateOf<List<AgentEntity>>(listOf())
open fun extraArgs(): AgentLoaderExtraArgs {
return AgentLoaderExtraArgs()
}
fun refreshPager(pullRefresh: Boolean = false) {
if (!isFirstLoad && !pullRefresh) {
return
@@ -46,6 +48,10 @@ open class BaseAgentModel :ViewModel(){
}
}
// 添加智能体到列表顶部,避免重新加载
fun addAgentToList(agent: AgentEntity) {
agentList = listOf(agent) + agentList
}
fun ResetModel() {
agentLoader.clear()

View File

@@ -23,6 +23,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -60,10 +62,23 @@ fun HotAgent() {
}
}
// 只在首次加载时刷新避免从AddAgent返回时重复刷新
LaunchedEffect(Unit) {
model.refreshPager()
if (model.agentList.isEmpty() && !model.isLoading) {
model.refreshPager()
}
}
val context = LocalContext.current
// 当智能体列表加载完成后,预加载图片
LaunchedEffect(agentList) {
if (agentList.isNotEmpty()) {
model.preloadImages(context)
}
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -107,7 +122,7 @@ fun HotAgent() {
) {
items(
agentList.size,
key = { idx -> idx }
key = { idx -> agentList[idx].id } // 使用智能体ID作为key避免重新创建
) { idx ->
val agentItem = agentList[idx]
AgentCard(

View File

@@ -22,10 +22,14 @@ object HotAgentViewModel : ViewModel() {
var currentPage by mutableStateOf(1)
var error by mutableStateOf<String?>(null)
// 记录已预加载的图片ID避免重复加载
private val preloadedImageIds = mutableSetOf<Int>()
private val pageSize = 20
init {
refreshPager()
// 延迟初始化,避免在页面切换时立即加载
// refreshPager()
}
fun refreshPager(pullRefresh: Boolean = false) {
@@ -37,6 +41,11 @@ object HotAgentViewModel : ViewModel() {
refreshing = pullRefresh
error = null
// 清除预加载记录,强制重新加载图片
if (pullRefresh) {
clearPreloadedImages()
}
val response = ApiClient.api.getAgent(
page = 1,
pageSize = pageSize
@@ -45,7 +54,17 @@ object HotAgentViewModel : ViewModel() {
val body = response.body()
if (body != null) {
val newAgents = body.data.list.map { it.toAgentEntity() }
agentList = newAgents
// 只有在列表为空或者是下拉刷新时才替换整个列表
if (agentList.isEmpty() || pullRefresh) {
agentList = newAgents
} else {
// 否则只添加新的智能体
val existingIds = agentList.map { it.id }.toSet()
val newAgentsToAdd = newAgents.filter { it.id !in existingIds }
if (newAgentsToAdd.isNotEmpty()) {
agentList = agentList + newAgentsToAdd
}
}
currentPage = 1
hasNext = newAgents.size == pageSize
} else {
@@ -108,4 +127,32 @@ object HotAgentViewModel : ViewModel() {
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id)
}
}
// 预加载图片,避免滑动时重复加载
fun preloadImages(context: android.content.Context) {
viewModelScope.launch {
agentList.forEach { agent ->
if (agent.id !in preloadedImageIds && agent.avatar.isNotEmpty()) {
try {
// 预加载头像图片到缓存
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
.data(agent.avatar)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.build()
)
preloadedImageIds.add(agent.id)
} catch (e: Exception) {
// 忽略预加载错误
}
}
}
}
}
// 清除预加载记录(在刷新时调用)
fun clearPreloadedImages() {
preloadedImageIds.clear()
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -60,10 +61,23 @@ fun MineAgent() {
}
}
// 只在首次加载时刷新避免从AddAgent返回时重复刷新
LaunchedEffect(Unit) {
model.refreshPager()
if (model.agentList.isEmpty() && !model.isLoading) {
model.refreshPager()
}
}
val context = LocalContext.current
// 当智能体列表加载完成后,预加载图片
LaunchedEffect(agentList) {
if (agentList.isNotEmpty()) {
model.preloadImages(context)
}
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -107,7 +121,7 @@ fun MineAgent() {
) {
items(
agentList.size,
key = { idx -> idx }
key = { idx -> agentList[idx].id } // 使用智能体ID作为key避免重新创建
) { idx ->
val agentItem = agentList[idx]
AgentCard(

View File

@@ -25,10 +25,14 @@ object MineAgentViewModel : ViewModel() {
var currentPage by mutableStateOf(1)
var error by mutableStateOf<String?>(null)
// 记录已预加载的图片ID避免重复加载
private val preloadedImageIds = mutableSetOf<Int>()
private val pageSize = 20
init {
refreshPager()
// 延迟初始化,避免在页面切换时立即加载
// refreshPager()
}
fun refreshPager(pullRefresh: Boolean = false) {
@@ -40,6 +44,11 @@ object MineAgentViewModel : ViewModel() {
refreshing = pullRefresh
error = null
// 清除预加载记录,强制重新加载图片
if (pullRefresh) {
clearPreloadedImages()
}
val response = ApiClient.api.getMyAgent(
page = 1,
pageSize = pageSize
@@ -48,7 +57,17 @@ object MineAgentViewModel : ViewModel() {
val body = response.body()
if (body != null) {
val newAgents = body.list.map { it.toAgentEntity() }
agentList = newAgents
// 只有在列表为空或者是下拉刷新时才替换整个列表
if (agentList.isEmpty() || pullRefresh) {
agentList = newAgents
} else {
// 否则只添加新的智能体
val existingIds = agentList.map { it.id }.toSet()
val newAgentsToAdd = newAgents.filter { it.id !in existingIds }
if (newAgentsToAdd.isNotEmpty()) {
agentList = agentList + newAgentsToAdd
}
}
currentPage = 1
hasNext = newAgents.size == pageSize
} else {
@@ -138,4 +157,37 @@ object MineAgentViewModel : ViewModel() {
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id)
}
}
// 添加新创建的智能体到列表顶部
fun addAgentToList(agent: AgentEntity) {
agentList = listOf(agent) + agentList
}
// 预加载图片,避免滑动时重复加载
fun preloadImages(context: android.content.Context) {
viewModelScope.launch {
agentList.forEach { agent ->
if (agent.id !in preloadedImageIds && agent.avatar.isNotEmpty()) {
try {
// 预加载头像图片到缓存
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
.data(agent.avatar)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.build()
)
preloadedImageIds.add(agent.id)
} catch (e: Exception) {
// 忽略预加载错误
}
}
}
}
}
// 清除预加载记录(在刷新时调用)
fun clearPreloadedImages() {
preloadedImageIds.clear()
}
}

View File

@@ -117,7 +117,7 @@ fun NotificationsScreen() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 8.dp),
.padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
@@ -197,7 +197,7 @@ fun NotificationsScreen() {
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp),
.padding(start = 16.dp,bottom = 16.dp),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
@@ -251,43 +251,9 @@ fun NotificationsScreen() {
2 -> {
FriendChatListScreen()
}
}
}
/* Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
,
contentAlignment = Alignment.Center
) {
if (AppState.enableChat){
ChatMessageList(
MessageListViewModel.chatList,
onUserAvatarClick = { conv ->
MessageListViewModel.goToUserDetail(conv, navController)
},
) { conv ->
MessageListViewModel.goToChat(conv, navController)
}
}else{
// center text
Text(
text = "Chat service is under maintenance",
color = AppColors.text,
fontSize = 16.sp
)
}
}
}
PullRefreshIndicator(
MessageListViewModel.isLoading,
state,
Modifier.align(Alignment.TopCenter)
)*/
}
}
@@ -360,131 +326,3 @@ fun NotificationIndicator(
}
@Composable
fun NotificationCounterItem(count: Int) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var clickCount by remember { mutableStateOf(0) }
Row(
modifier = Modifier.padding(vertical = 16.dp, horizontal = 32.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_notification),
contentDescription = "",
modifier = Modifier
.size(24.dp).noRippleClickable {
clickCount++
if (clickCount > 5) {
clickCount = 0
AppStore.saveDarkMode(!AppState.darkMode)
Toast.makeText(context, "Dark mode: ${AppState.darkMode},please restart app", Toast.LENGTH_SHORT).show()
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(24.dp))
Text(stringResource(R.string.notifications_upper), fontSize = 18.sp, color = AppColors.text)
Spacer(modifier = Modifier.weight(1f))
if (count > 0) {
Box(
modifier = Modifier
.background(AppColors.main, RoundedCornerShape(16.dp))
.padding(horizontal = 8.dp, vertical = 2.dp)
) {
Text(
text = count.toString(),
color = AppColors.mainText,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
)
}
}
}
}
@Composable
fun ChatMessageList(
items: List<Conversation>,
onUserAvatarClick: (Conversation) -> Unit = {},
onChatClick: (Conversation) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(items.size) { index ->
val item = items[index]
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
) {
Box {
CustomAsyncImage(
context = LocalContext.current,
imageUrl = item.avatar,
contentDescription = item.nickname,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(48.dp))
.noRippleClickable {
onUserAvatarClick(item)
}
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.noRippleClickable {
onChatClick(item)
}
) {
Row {
Text(
text = item.nickname,
fontSize = 16.sp,
modifier = Modifier,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = item.lastMessageTime,
fontSize = 14.sp,
color = AppColors.secondaryText,
)
}
Spacer(modifier = Modifier.height(6.dp))
Row {
Text(
text = "${if (item.isSelf) "Me: " else ""}${item.displayText}",
fontSize = 14.sp,
maxLines = 1,
color = AppColors.secondaryText,
modifier = Modifier.weight(1f),
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(4.dp))
if (item.unreadCount > 0) {
Box(
modifier = Modifier
.background(AppColors.main, CircleShape)
.padding(horizontal = 8.dp, vertical = 2.dp)
) {
Text(
text = item.unreadCount.toString(),
color = AppColors.mainText,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Center)
)
}
Spacer(modifier = Modifier.width(8.dp))
}
}
}
}
}
}
}

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
@@ -27,6 +28,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -113,13 +115,7 @@ fun AgentChatListScreen() {
model.goToChatAi(conv.trtcUserId,navController)
}
)
if (index < AgentChatListViewModel.agentChatList.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
}
// 加载更多指示器
@@ -174,16 +170,16 @@ fun AgentChatItem(
onChatClick: (AgentConversation) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
onChatClick(conversation)
}
},
verticalAlignment = Alignment.CenterVertically
) {
// 头像
Box {
CustomAsyncImage(
context = LocalContext.current,
@@ -191,18 +187,18 @@ fun AgentChatItem(
contentDescription = conversation.nickname,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.clip(RoundedCornerShape(48.dp))
.noRippleClickable {
onUserAvatarClick(conversation)
}
)
}
// 聊天信息
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -210,53 +206,52 @@ fun AgentChatItem(
) {
Text(
text = conversation.nickname,
fontSize = 16.sp,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = conversation.lastMessageTime,
fontSize = 12.sp,
fontSize = 11.sp,
color = AppColors.secondaryText
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${if (conversation.isSelf) stringResource(R.string.agent_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp,
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
// 未读消息数量
Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background(
color = AppColors.main,
color = Color(0xFFFF3B30),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
color = Color.White,
fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
fontWeight = FontWeight.Bold
)
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
@@ -17,6 +18,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -97,12 +99,6 @@ fun FriendChatListScreen() {
}
)
if (index < FriendChatListViewModel.friendChatList.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
}
if (FriendChatListViewModel.isLoading && FriendChatListViewModel.friendChatList.isNotEmpty()) {
@@ -158,10 +154,11 @@ fun FriendChatItem(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
onChatClick(conversation)
}
},
verticalAlignment = Alignment.CenterVertically
) {
Box {
CustomAsyncImage(
@@ -170,17 +167,18 @@ fun FriendChatItem(
contentDescription = conversation.nickname,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.clip(RoundedCornerShape(48.dp))
.noRippleClickable {
onUserAvatarClick(conversation)
}
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -188,52 +186,52 @@ fun FriendChatItem(
) {
Text(
text = conversation.nickname,
fontSize = 16.sp,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = conversation.lastMessageTime,
fontSize = 12.sp,
fontSize = 11.sp,
color = AppColors.secondaryText
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background(
color = AppColors.main,
color = Color(0xFFFF3B30),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
color = Color.White,
fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
fontWeight = FontWeight.Bold
)
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
@@ -13,10 +14,12 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -45,7 +48,17 @@ fun GroupChatListScreen() {
LaunchedEffect(Unit) {
GroupChatListViewModel.refreshPager(context = context)
// 初始化消息监听器
GroupChatListViewModel.initMessageListener(context)
}
// 在组件销毁时清理监听器
DisposableEffect(Unit) {
onDispose {
GroupChatListViewModel.removeMessageListener()
}
}
Column(
modifier = Modifier
@@ -96,12 +109,6 @@ fun GroupChatListScreen() {
}
)
if (index < GroupChatListViewModel.groupChatList.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
}
if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
@@ -157,10 +164,11 @@ fun GroupChatItem(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
onChatClick(conversation)
}
},
verticalAlignment = Alignment.CenterVertically
) {
Box {
CustomAsyncImage(
@@ -169,7 +177,7 @@ fun GroupChatItem(
contentDescription = conversation.groupName,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.clip(RoundedCornerShape(12.dp))
.noRippleClickable {
onGroupAvatarClick(conversation)
}
@@ -179,7 +187,8 @@ fun GroupChatItem(
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -187,22 +196,22 @@ fun GroupChatItem(
) {
Text(
text = conversation.groupName,
fontSize = 16.sp,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = conversation.lastMessageTime,
fontSize = 12.sp,
fontSize = 11.sp,
color = AppColors.secondaryText
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -210,30 +219,29 @@ fun GroupChatItem(
) {
Text(
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background(
color = AppColors.main,
color = Color(0xFFFF3B30),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
fontWeight = FontWeight.Bold
color = Color.White,
fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
)
}
}

View File

@@ -9,9 +9,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.GroupChatRequestBody
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.navigateToChat
@@ -22,6 +25,8 @@ 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 com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
import com.tencent.imsdk.v2.V2TIMConversationListener
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
@@ -70,7 +75,16 @@ data class GroupConversation(
groupName = msg.showName,
lastMessage = msg.lastMessage?.textElem?.text ?: "",
lastMessageTime = lastMessage.time.formatChatTime(context),
avatar = "${ConstVars.BASE_SERVER}${msg.faceUrl}",
avatar = if (msg.faceUrl.isNullOrEmpty()) {
// 将 groupId 转换为 Base64
val groupIdBase64 = android.util.Base64.encodeToString(
msg.groupID.toByteArray(),
android.util.Base64.NO_WRAP
)
"${ApiClient.RETROFIT_URL+"group/avatar?groupIdBase64="}${groupIdBase64}"+"&token="+"${AppStore.token}"
} else {
"${ApiClient.BASE_API_URL+"/outside/rooms/avatar/"}${msg.faceUrl}"+"?token="+"${AppStore.token}"
},
unreadCount = msg.unreadCount,
displayText = displayText,
isSelf = msg.lastMessage?.sender == AppState.profile?.trtcUserId,
@@ -90,6 +104,10 @@ object GroupChatListViewModel : ViewModel() {
var error by mutableStateOf<String?>(null)
private val pageSize = 20
// 消息监听器
private var messageListener: V2TIMAdvancedMsgListener? = null
private var conversationListener: V2TIMConversationListener? = null
fun refreshPager(pullRefresh: Boolean = false, context: Context? = null) {
if (isLoading && !pullRefresh) return
@@ -162,14 +180,24 @@ object GroupChatListViewModel : ViewModel() {
}
}
fun createGroupChat(
trtcGroupId: String,
) {
viewModelScope.launch {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId)
}
}
fun goToChat(
conversation: GroupConversation,
navController: NavHostController
) {
viewModelScope.launch {
try {
createGroupChat(trtcGroupId = conversation.groupId)
// 群聊直接使用群ID进行导航
navController.navigateToGroupChat(conversation.groupId)
navController.navigateToGroupChat(conversation.groupId, conversation.groupName, conversation.avatar)
} catch (e: Exception) {
error = ""
e.printStackTrace()
@@ -184,7 +212,7 @@ object GroupChatListViewModel : ViewModel() {
viewModelScope.launch {
try {
// 可以导航到群详情页面,这里暂时使用群聊页面
navController.navigateToChat(conversation.groupId)
//
} catch (e: Exception) {
error = ""
e.printStackTrace()
@@ -197,4 +225,77 @@ object GroupChatListViewModel : ViewModel() {
loadGroupChatList(context)
}
}
// 初始化消息监听器
fun initMessageListener(context: Context) {
// 消息监听器 - 监听新消息
messageListener = object : V2TIMAdvancedMsgListener() {
override fun onRecvNewMessage(msg: V2TIMMessage?) {
super.onRecvNewMessage(msg)
msg?.let { message ->
if (message.groupID != null && message.groupID.isNotEmpty()) {
// 收到群聊消息,刷新群聊列表
android.util.Log.i("GroupChatList", "收到群聊消息,刷新列表")
refreshGroupChatList(context)
}
}
}
}
// 会话监听器 - 监听会话变化
conversationListener = object : V2TIMConversationListener() {
override fun onConversationChanged(conversationList: MutableList<V2TIMConversation>?) {
super.onConversationChanged(conversationList)
// 会话发生变化,刷新群聊列表
conversationList?.let { conversations ->
val hasGroupConversation = conversations.any { it.type == V2TIMConversation.V2TIM_GROUP }
if (hasGroupConversation) {
android.util.Log.i("GroupChatList", "群聊会话发生变化,刷新列表")
refreshGroupChatList(context)
}
}
}
override fun onNewConversation(conversationList: MutableList<V2TIMConversation>?) {
super.onNewConversation(conversationList)
// 新增会话,刷新群聊列表
conversationList?.let { conversations ->
val hasGroupConversation = conversations.any { it.type == V2TIMConversation.V2TIM_GROUP }
if (hasGroupConversation) {
android.util.Log.i("GroupChatList", "新增群聊会话,刷新列表")
refreshGroupChatList(context)
}
}
}
}
// 注册监听器
V2TIMManager.getMessageManager().addAdvancedMsgListener(messageListener)
V2TIMManager.getConversationManager().addConversationListener(conversationListener)
}
// 移除消息监听器
fun removeMessageListener() {
messageListener?.let {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(it)
}
conversationListener?.let {
V2TIMManager.getConversationManager().removeConversationListener(it)
}
messageListener = null
conversationListener = null
}
// 刷新群聊列表
private fun refreshGroupChatList(context: Context) {
viewModelScope.launch {
try {
loadGroupChatList(context)
} catch (e: Exception) {
android.util.Log.e("GroupChatList", "刷新群聊列表失败: ${e.message}")
}
}
}
}

View File

@@ -48,6 +48,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.graphicsLayer
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.AppStore
@@ -232,9 +233,9 @@ fun Explore() {
}
@Composable
fun AgentPage(agentItems: List<AgentItem>, page: Int) {
fun AgentPage(agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier) {
Column(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.padding(horizontal = 0.dp)
) {
@@ -268,11 +269,27 @@ fun Explore() {
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 4.dp),
pageSpacing = 0.dp
) { page ->
// 计算当前页面的偏移量
val pageOffset = (
(pagerState.currentPage - page) + pagerState
.currentPageOffsetFraction
).coerceIn(-1f, 1f)
// 根据偏移量计算缩放比例
val scale = 1f - (0.1f * kotlin.math.abs(pageOffset))
AgentPage(
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
page = page
page = page,
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
}
)
}
}
@@ -410,12 +427,12 @@ fun Explore() {
@Composable
fun BannerCard(bannerItem: BannerItem) {
fun BannerCard(bannerItem: BannerItem, modifier: Modifier = Modifier) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
Card(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.padding(horizontal = 0.dp),
shape = RoundedCornerShape(20.dp),
@@ -595,15 +612,15 @@ fun Explore() {
) {
// 可以添加更多不同高度的内容项
// 第一块区域
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceEvenly
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween
) {
// 第一个
// 第一个 - 靠左显示
Column(
modifier = Modifier
.clickable {
@@ -633,69 +650,75 @@ fun Explore() {
)
}
// 第二个
Column(
modifier = Modifier
.clickable {
navController.navigate(
NavigationRoute.AddAgent.route)
},
horizontalAlignment = Alignment.CenterHorizontally
// 中间两个 - 平均分布
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceEvenly
) {
Box(
// 第二个
Column(
modifier = Modifier
.size(64.dp)
.background(Color(0xFF94f9f2), RoundedCornerShape(24.dp)),
contentAlignment = Alignment.Center
.clickable {
navController.navigate(
NavigationRoute.AddAgent.route)
},
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "创建智能体",
modifier = Modifier.size(24.dp),
)
}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "创建Agent",
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
// 第三个
Column(
modifier = Modifier
.clickable {
NewPostViewModel.asNewPost()
navController.navigate("NewPost")
},
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(64.dp)
.background(Color(0xFFfafd5d), RoundedCornerShape(24.dp)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.mipmap.rider_pro_release),
contentDescription = "发布动态",
modifier = Modifier.size(24.dp),
Box(
modifier = Modifier
.size(64.dp)
.background(Color(0xFF94f9f2), RoundedCornerShape(24.dp)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "创建智能体",
modifier = Modifier.size(24.dp),
)
}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "创建Agent",
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
// 第三个
Column(
modifier = Modifier
.clickable {
NewPostViewModel.asNewPost()
navController.navigate("NewPost")
},
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(64.dp)
.background(Color(0xFFfafd5d), RoundedCornerShape(24.dp)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.mipmap.rider_pro_release),
contentDescription = "发布动态",
modifier = Modifier.size(24.dp),
)
}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "发布动态",
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "发布动态",
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
// 第四个
// 第四个 - 靠右显示
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
@@ -719,9 +742,9 @@ fun Explore() {
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -767,10 +790,28 @@ fun BannerSection(bannerItems: List<BannerItem>) {
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 4.dp),
) { page ->
val bannerItem = bannerItems[page]
BannerCard(bannerItem = bannerItem)
// 计算当前页面的偏移量
val pageOffset = (
(pagerState.currentPage - page) + pagerState
.currentPageOffsetFraction
).coerceIn(-1f, 1f)
// 根据偏移量计算缩放比例
val scale = 1f - (0.1f * kotlin.math.abs(pageOffset))
BannerCard(
bannerItem = bannerItem,
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
}
)
}
}