预留首页-探索,完善群组功能

This commit is contained in:
weber
2025-08-13 18:57:03 +08:00
parent 2d518cbd68
commit bc7a897cec
17 changed files with 1045 additions and 128 deletions

View File

@@ -64,6 +64,7 @@ import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListScreen
import com.aiosman.ravenow.ui.index.tabs.message.tab.FriendChatListScreen
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListScreen
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -244,7 +245,7 @@ fun NotificationsScreen() {
}
1 -> {
GroupChatListScreen()
}
2 -> {

View File

@@ -145,7 +145,7 @@ object FriendChatListViewModel : ViewModel() {
// 过滤掉ai_group的会话只保留朋友聊天
val filteredConversations = result?.conversationList?.filter { conversation ->
// 排除ai_group的会话
!conversation.conversationGroupList.contains("ai_group")
!conversation.conversationGroupList.contains("ai_group")&& conversation.type == V2TIMConversation.V2TIM_C2C
} ?: emptyList()
friendChatList = filteredConversations.map { msg: V2TIMConversation ->

View File

@@ -0,0 +1,243 @@
package com.aiosman.ravenow.ui.index.tabs.message.tab
import androidx.compose.foundation.background
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.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun GroupChatListScreen() {
val context = LocalContext.current
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
val state = rememberPullRefreshState(
refreshing = GroupChatListViewModel.refreshing,
onRefresh = {
GroupChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
LaunchedEffect(Unit) {
GroupChatListViewModel.refreshPager(context = context)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state)
) {
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "暂无群聊",
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "您还没有加入任何群聊",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(
items = GroupChatListViewModel.groupChatList,
key = { _, item -> item.id }
) { index, item ->
GroupChatItem(
conversation = item,
onGroupAvatarClick = { conv ->
GroupChatListViewModel.goToGroupDetail(conv, navController)
},
onChatClick = { conv ->
GroupChatListViewModel.goToChat(conv, navController)
}
)
if (index < GroupChatListViewModel.groupChatList.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
}
if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main
)
}
}
}
}
}
PullRefreshIndicator(
refreshing = GroupChatListViewModel.refreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
GroupChatListViewModel.error?.let { error ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = error,
color = AppColors.error,
fontSize = 14.sp
)
}
}
}
}
@Composable
fun GroupChatItem(
conversation: GroupConversation,
onGroupAvatarClick: (GroupConversation) -> Unit = {},
onChatClick: (GroupConversation) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.noRippleClickable {
onChatClick(conversation)
}
) {
Box {
CustomAsyncImage(
context = LocalContext.current,
imageUrl = conversation.avatar,
contentDescription = conversation.groupName,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable {
onGroupAvatarClick(conversation)
}
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = conversation.groupName,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = conversation.lastMessageTime,
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
Spacer(modifier = Modifier.height(4.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,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background(
color = AppColors.main,
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
)
}
}
}
}
}
}

View File

@@ -0,0 +1,195 @@
package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.content.Context
import android.icu.util.Calendar
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.UserService
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.tencent.imsdk.v2.V2TIMConversation
import com.tencent.imsdk.v2.V2TIMConversationResult
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMMessage
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
data class GroupConversation(
val id: String,
val groupId: String,
val groupName: String,
val lastMessage: String,
val lastMessageTime: String,
val avatar: String = "",
val unreadCount: Int = 0,
val displayText: String,
val isSelf: Boolean,
val memberCount: Int = 0
) {
companion object {
fun convertToGroupConversation(msg: V2TIMConversation, context: Context): GroupConversation {
val lastMessage = Calendar.getInstance().apply {
timeInMillis = msg.lastMessage?.timestamp ?: 0
timeInMillis *= 1000
}
var displayText = ""
when (msg.lastMessage?.elemType) {
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
displayText = msg.lastMessage?.textElem?.text ?: ""
}
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
displayText = "[图片]"
}
V2TIMMessage.V2TIM_ELEM_TYPE_SOUND -> {
displayText = "[语音]"
}
V2TIMMessage.V2TIM_ELEM_TYPE_VIDEO -> {
displayText = "[视频]"
}
V2TIMMessage.V2TIM_ELEM_TYPE_FILE -> {
displayText = "[文件]"
}
else -> {
displayText = "[消息]"
}
}
return GroupConversation(
id = msg.conversationID,
groupId = msg.groupID,
groupName = msg.showName,
lastMessage = msg.lastMessage?.textElem?.text ?: "",
lastMessageTime = lastMessage.time.formatChatTime(context),
avatar = "${ConstVars.BASE_SERVER}${msg.faceUrl}",
unreadCount = msg.unreadCount,
displayText = displayText,
isSelf = msg.lastMessage?.sender == AppState.profile?.trtcUserId,
memberCount = msg.groupAtInfoList?.size ?: 0
)
}
}
}
object GroupChatListViewModel : ViewModel() {
val userService: UserService = UserServiceImpl()
var groupChatList by mutableStateOf<List<GroupConversation>>(emptyList())
var isLoading by mutableStateOf(false)
var refreshing by mutableStateOf(false)
var hasNext by mutableStateOf(true)
var currentPage by mutableStateOf(1)
var error by mutableStateOf<String?>(null)
private val pageSize = 20
fun refreshPager(pullRefresh: Boolean = false, context: Context? = null) {
if (isLoading && !pullRefresh) return
viewModelScope.launch {
try {
isLoading = true
refreshing = pullRefresh
error = null
context?.let { loadGroupChatList(it) }
currentPage = 1
} catch (e: Exception) {
error = ""
e.printStackTrace()
} finally {
isLoading = false
refreshing = false
}
}
}
fun loadMore() {
if (isLoading || !hasNext) return
viewModelScope.launch {
try {
isLoading = true
error = null
// 腾讯IM的会话列表是一次性获取的这里模拟分页
// 实际项目中可能需要根据时间戳或其他方式实现真正的分页
hasNext = false
} catch (e: Exception) {
error = ""
e.printStackTrace()
} finally {
isLoading = false
}
}
}
private suspend fun loadGroupChatList(context: Context) {
val result = suspendCoroutine { continuation ->
// 获取全部会话列表
V2TIMManager.getConversationManager().getConversationList(
0,
Int.MAX_VALUE,
object : V2TIMValueCallback<V2TIMConversationResult> {
override fun onSuccess(t: V2TIMConversationResult?) {
continuation.resumeWith(Result.success(t))
}
override fun onError(code: Int, desc: String?) {
continuation.resumeWith(Result.failure(Exception("Error $code: $desc")))
}
}
)
}
// 只保留群聊会话,过滤掉单聊会话
val filteredConversations = result?.conversationList?.filter { conversation ->
// 只保留群聊会话conversationType为2表示群聊
conversation.type == V2TIMConversation.V2TIM_GROUP
} ?: emptyList()
groupChatList = filteredConversations.map { msg: V2TIMConversation ->
val conversation = GroupConversation.convertToGroupConversation(msg, context)
println("GroupChatList: Conversation ${conversation.groupName} has ${conversation.unreadCount} unread messages")
conversation
}
}
fun goToChat(
conversation: GroupConversation,
navController: NavHostController
) {
viewModelScope.launch {
try {
// 群聊直接使用群ID进行导航
navController.navigateToChat(conversation.groupId)
} catch (e: Exception) {
error = ""
e.printStackTrace()
}
}
}
fun goToGroupDetail(
conversation: GroupConversation,
navController: NavHostController
) {
viewModelScope.launch {
try {
// 可以导航到群详情页面,这里暂时使用群聊页面
navController.navigateToChat(conversation.groupId)
} catch (e: Exception) {
error = ""
e.printStackTrace()
}
}
}
fun refreshConversation(context: Context, groupId: String) {
viewModelScope.launch {
loadGroupChatList(context)
}
}
}

View File

@@ -60,7 +60,7 @@ fun MomentsList() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var pagerState = rememberPagerState { 3 }
var pagerState = rememberPagerState { 4 }
var scope = rememberCoroutineScope()
Column(
modifier = Modifier
@@ -99,7 +99,7 @@ fun MomentsList() {
Image(
painter = painterResource(
if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
@@ -121,7 +121,7 @@ fun MomentsList() {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.index_following),
text = stringResource(R.string.index_dynamic),
fontSize = 16.sp,
color = if (pagerState.currentPage == 1) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
@@ -129,7 +129,37 @@ fun MomentsList() {
Image(
painter = painterResource(
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(2)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.index_following),
fontSize = 16.sp,
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Image(
painter = painterResource(
if (pagerState.currentPage == 2) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
@@ -145,7 +175,7 @@ fun MomentsList() {
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(2)
pagerState.animateScrollToPage(3)
}
},
verticalArrangement = Arrangement.Center,
@@ -154,13 +184,13 @@ fun MomentsList() {
Text(
text = stringResource(R.string.index_hot),
fontSize = 16.sp,
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText,
color = if (pagerState.currentPage == 3) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Image(
painter = painterResource(
if (pagerState.currentPage == 2) R.mipmap.tab_indicator_selected
if (pagerState.currentPage == 3) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
@@ -197,14 +227,17 @@ fun MomentsList() {
) {
when (it) {
0 -> {
//ExploreMomentsList()
}
1 -> {
ExploreMomentsList()
}
1 -> {
2 -> {
TimelineMomentsList()
}
2 -> {
3 -> {
HotMomentsList()
}