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

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