预留首页-探索,完善群组功能
This commit is contained in:
@@ -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 -> {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user