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

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

@@ -15,6 +15,8 @@ import com.aiosman.ravenow.data.api.ResetPasswordRequestBody
import com.aiosman.ravenow.data.api.TrtcSignResponseBody
import com.aiosman.ravenow.data.api.UnRegisterMessageChannelRequestBody
import com.aiosman.ravenow.data.api.UpdateNoticeRequestBody
import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.api.UpdateUserLangRequestBody
import com.aiosman.ravenow.entity.AccountFavouriteEntity
import com.aiosman.ravenow.entity.AccountLikeEntity
@@ -55,7 +57,9 @@ data class AccountProfile(
// trtcUserId
val trtcUserId: String,
// aiAccount true:ai false:普通用户
val aiAccount: Boolean
val aiAccount: Boolean,
val chatAIId: String,
) {
/**
* 转换为Entity
@@ -79,7 +83,8 @@ data class AccountProfile(
},
trtcUserId = trtcUserId,
aiAccount = aiAccount,
rawAvatar = avatar
rawAvatar = avatar,
chatAIId = chatAIId
)
}
}
@@ -394,6 +399,21 @@ interface AccountService {
suspend fun getAppConfig(): AppConfig
suspend fun removeAccount(password: String)
/**
* 获取AI智能体列表
* @param page 页码
* @param pageSize 每页数量
*/
suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>>
/**
* 创建群聊
* @param name 群聊名称
* @param userIds 用户ID列表
* @param promptIds AI智能体ID列表
*/
suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>>
}
class AccountServiceImpl : AccountService {
@@ -574,4 +594,17 @@ class AccountServiceImpl : AccountService {
throw ServiceException("Failed to remove account")
}
}
override suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>> {
return ApiClient.api.getAgent(page, pageSize)
}
override suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>> {
val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody(
name = name,
userIds = userIds,
promptIds = promptIds
)
return ApiClient.api.createGroupChat(requestBody)
}
}

View File

@@ -29,6 +29,7 @@ data class Agent(
val updatedAt: String,
@SerializedName("useCount")
val useCount: Int
) {
fun toAgentEntity(): AgentEntity {
return AgentEntity(

View File

@@ -56,6 +56,15 @@ data class SendChatAiRequestBody(
)
data class CreateGroupChatRequestBody(
@SerializedName("name")
val name: String,
@SerializedName("userIds")
val userIds: List<String>,
@SerializedName("promptIds")
val promptIds: List<String>,
)
data class LoginUserRequestBody(
@SerializedName("username")
val username: String? = null,
@@ -529,6 +538,9 @@ interface RaveNowAPI {
@POST("outside/rooms/message")
suspend fun sendChatAiMessage(@Body body: SendChatAiRequestBody): Response<DataContainer<Unit>>
@POST("outside/rooms")
suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>>

View File

@@ -63,7 +63,9 @@ data class AccountProfileEntity(
val trtcUserId: String,
val aiAccount: Boolean,
val rawAvatar: String
val rawAvatar: String,
val chatAIId: String,
)
/**

View File

@@ -3,6 +3,11 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@@ -15,7 +20,9 @@ import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AiAgentListScreen(
searchText: String,
@@ -24,47 +31,101 @@ fun AiAgentListScreen(
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
var aiAgents by remember { mutableStateOf<List<GroupMember>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
// 使用ViewModel
val viewModel = remember { AiAgentListViewModel() }
// 加载AI智能体数据
LaunchedEffect(Unit) {
isLoading = true
aiAgents = CreateGroupChatViewModel.getAiAgents()
isLoading = false
}
// 获取过滤后的数据
val filteredAgents = viewModel.getFilteredAgents(searchText)
val filteredAgents = if (searchText.isEmpty()) {
aiAgents
} else {
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
// 下拉刷新状态
val pullRefreshState = rememberPullRefreshState(
refreshing = viewModel.isRefreshing,
onRefresh = {
viewModel.refresh()
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredAgents) { agent ->
MemberItem(
member = agent,
isSelected = selectedMemberIds.contains(agent.id),
onSelect = { onMemberSelect(agent) }
)
// 上拉加载更多
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
.collect { visibleItems ->
if (visibleItems.isNotEmpty()) {
val lastVisibleItem = visibleItems.last()
if (lastVisibleItem.index >= filteredAgents.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
viewModel.loadMore()
}
}
}
}
// 显示错误信息
viewModel.errorMessage?.let { error ->
LaunchedEffect(error) {
// 可以在这里显示错误提示
}
}
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
if (viewModel.isLoading && filteredAgents.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredAgents) { agent ->
MemberItem(
member = agent,
isSelected = selectedMemberIds.contains(agent.id),
onSelect = { onMemberSelect(agent) }
)
}
// 加载更多指示器
if (viewModel.isLoadingMore) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "加载更多...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
}
}
}
}
// 下拉刷新指示器
PullRefreshIndicator(
refreshing = viewModel.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = AppColors.background,
contentColor = AppColors.main
)
}
}

View File

@@ -0,0 +1,118 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.api.ApiClient
import kotlinx.coroutines.launch
class AiAgentListViewModel : ViewModel() {
private val accountService: AccountService = AccountServiceImpl()
// 状态管理
var aiAgents by mutableStateOf<List<GroupMember>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
var isRefreshing by mutableStateOf(false)
private set
var isLoadingMore by mutableStateOf(false)
private set
var currentPage by mutableStateOf(1)
private set
var hasMoreData by mutableStateOf(true)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
private val pageSize = 20
init {
loadAgents(1)
}
// 加载AI智能体数据
fun loadAgents(page: Int, isRefresh: Boolean = false) {
viewModelScope.launch {
try {
if (isRefresh) {
isRefreshing = true
} else if (page == 1) {
isLoading = true
} else {
isLoadingMore = true
}
errorMessage = null
val response = accountService.getAgent(page, pageSize)
if (response.isSuccessful && response.body() != null) {
val agentData = response.body()!!
val newAgents: List<GroupMember> = agentData.data.list.map { agent ->
GroupMember(
id = agent.openId,
name = agent.title,
avatar = "${ApiClient.BASE_API_URL+"/outside"}${agent.avatar}"+"?token="+"${AppStore.token}",
isAi = true
)
}
if (isRefresh || page == 1) {
aiAgents = newAgents
currentPage = 1
} else {
aiAgents = aiAgents + newAgents
currentPage = page
}
hasMoreData = newAgents.size >= pageSize
} else {
errorMessage = "获取AI智能体列表失败: ${response.message()}"
}
} catch (e: Exception) {
errorMessage = "获取AI智能体列表失败: ${e.message}"
} finally {
isLoading = false
isRefreshing = false
isLoadingMore = false
}
}
}
// 刷新数据
fun refresh() {
loadAgents(1, true)
}
// 加载更多数据
fun loadMore() {
if (hasMoreData && !isLoadingMore && !isLoading) {
loadAgents(currentPage + 1)
}
}
// 清除错误信息
fun clearError() {
errorMessage = null
}
// 搜索过滤
fun getFilteredAgents(searchText: String): List<GroupMember> {
return if (searchText.isEmpty()) {
aiAgents
} else {
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
}
}
}

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
@@ -42,7 +43,8 @@ import kotlinx.coroutines.launch
data class GroupMember(
val id: String,
val name: String,
val avatar: String
val avatar: String,
val isAi:Boolean = false
)
@OptIn(ExperimentalFoundationApi::class)
@@ -61,6 +63,9 @@ fun CreateGroupChatScreen() {
var pagerState = rememberPagerState(pageCount = { 2 })
var scope = rememberCoroutineScope()
// LazyRow状态管理
val lazyRowState = rememberLazyListState()
// 清除错误信息
LaunchedEffect(groupName.text, searchText.text) {
if (CreateGroupChatViewModel.errorMessage != null) {
@@ -74,6 +79,15 @@ fun CreateGroupChatScreen() {
// 这样用户可以在AI智能体和朋友之间切换选中的状态会保持
}
// 监听selectedMembers变化当有新成员添加时自动滚动到最后一个
LaunchedEffect(selectedMembers.size) {
if (selectedMembers.isNotEmpty()) {
// 延迟一点时间确保LazyRow已经更新了布局
kotlinx.coroutines.delay(100)
lazyRowState.animateScrollToItem(selectedMembers.size - 1)
}
}
val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
LaunchedEffect(Unit) {
@@ -230,52 +244,74 @@ fun CreateGroupChatScreen() {
// 已选成员列表
if (selectedMembers.isNotEmpty()) {
// 显示选中成员数量
Text(
/* Text(
text = "已选择 ${selectedMembers.size} 个成员",
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
)*/
LazyRow(
state = lazyRowState,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(selectedMembers) { member ->
Box {
CustomAsyncImage(
context = context,
imageUrl = member.avatar,
contentDescription = member.name,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
// 删除按钮
Box(
modifier = Modifier
.size(20.dp)
.background(AppColors.error, CircleShape)
.align(Alignment.TopEnd)
.noRippleClickable {
// 删除成员时同时更新选中状态
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.removeSelectedMember(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
},
contentAlignment = Alignment.Center
) {
Text(
text = "×",
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.width(48.dp)
) {
Box {
CustomAsyncImage(
context = context,
imageUrl = member.avatar,
contentDescription = member.name,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
// 删除按钮
Box(
modifier = Modifier
.size(20.dp)
.background(AppColors.error, CircleShape)
.align(Alignment.TopEnd)
.noRippleClickable {
// 删除成员时同时更新选中状态
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.removeSelectedMember(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
},
contentAlignment = Alignment.Center
) {
Text(
text = "×",
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
// 名称显示
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (member.name.length > 5) {
member.name.substring(0, 5) + "..."
} else {
member.name
},
fontSize = 12.sp,
color = AppColors.text,
maxLines = 1,
modifier = Modifier.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
}
@@ -354,11 +390,11 @@ fun CreateGroupChatScreen() {
Button(
onClick = {
// 创建群聊逻辑
if (groupName.text.isNotEmpty() && selectedMembers.isNotEmpty()) {
if (selectedMembers.isNotEmpty()) {
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
memberIds = selectedMembers.map { it.id },
selectedMembers = selectedMembers,
context = context
)
if (success) {
@@ -374,7 +410,7 @@ fun CreateGroupChatScreen() {
containerColor = AppColors.main,
contentColor = AppColors.mainText
),
shape = RoundedCornerShape(8.dp),
shape = RoundedCornerShape(24.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
) {
if (CreateGroupChatViewModel.isLoading) {

View File

@@ -79,19 +79,25 @@ object CreateGroupChatViewModel : ViewModel() {
// 创建群聊
suspend fun createGroupChat(
groupName: String,
memberIds: List<String>,
selectedMembers: List<GroupMember>,
context: Context
): Boolean {
return try {
isLoading = true
// TODO: 实现创建群聊的API调用
// 这里应该调用实际的API来创建群聊
// 模拟API调用延迟
kotlinx.coroutines.delay(1000)
// 根据isAi属性分别获取userIds和promptIds
val userIds = selectedMembers.filter { !it.isAi }.map { it.id }
val promptIds = selectedMembers.filter { it.isAi }.map { it.id }
isLoading = false
true
val response = accountService.createGroupChat(groupName, userIds, promptIds)
if (response.isSuccessful && response.body() != null) {
isLoading = false
true
} else {
isLoading = false
errorMessage = "创建群聊失败: ${response.message()}"
false
}
} catch (e: Exception) {
isLoading = false
errorMessage = "创建群聊失败: ${e.message}"

View File

@@ -3,6 +3,11 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@@ -15,7 +20,9 @@ import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FriendListScreen(
searchText: String,
@@ -24,47 +31,101 @@ fun FriendListScreen(
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
var friends by remember { mutableStateOf<List<GroupMember>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
// 使用ViewModel
val viewModel = remember { FriendListViewModel() }
// 加载朋友数据
LaunchedEffect(Unit) {
isLoading = true
friends = CreateGroupChatViewModel.getFriends()
isLoading = false
}
// 获取过滤后的数据
val filteredFriends = viewModel.getFilteredFriends(searchText)
val filteredFriends = if (searchText.isEmpty()) {
friends
} else {
friends.filter { it.name.contains(searchText, ignoreCase = true) }
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
// 下拉刷新状态
val pullRefreshState = rememberPullRefreshState(
refreshing = viewModel.isRefreshing,
onRefresh = {
viewModel.refresh()
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredFriends) { friend ->
MemberItem(
member = friend,
isSelected = selectedMemberIds.contains(friend.id),
onSelect = { onMemberSelect(friend) }
)
// 上拉加载更多
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
.collect { visibleItems ->
if (visibleItems.isNotEmpty()) {
val lastVisibleItem = visibleItems.last()
if (lastVisibleItem.index >= filteredFriends.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
viewModel.loadMore()
}
}
}
}
// 显示错误信息
viewModel.errorMessage?.let { error ->
LaunchedEffect(error) {
// 可以在这里显示错误提示
}
}
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
if (viewModel.isLoading && filteredFriends.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredFriends) { friend ->
MemberItem(
member = friend,
isSelected = selectedMemberIds.contains(friend.id),
onSelect = { onMemberSelect(friend) }
)
}
// 加载更多指示器
if (viewModel.isLoadingMore) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "加载更多...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
}
}
}
}
// 下拉刷新指示器
PullRefreshIndicator(
refreshing = viewModel.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = AppColors.background,
contentColor = AppColors.main
)
}
}

View File

@@ -0,0 +1,113 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.data.api.ApiClient
import kotlinx.coroutines.launch
class FriendListViewModel : ViewModel() {
private val userService: UserService = UserServiceImpl()
// 状态管理
var friends by mutableStateOf<List<GroupMember>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
var isRefreshing by mutableStateOf(false)
private set
var isLoadingMore by mutableStateOf(false)
private set
var currentPage by mutableStateOf(1)
private set
var hasMoreData by mutableStateOf(true)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
private val pageSize = 20
init {
loadFriends(1)
}
// 加载朋友数据
fun loadFriends(page: Int, isRefresh: Boolean = false) {
viewModelScope.launch {
try {
if (isRefresh) {
isRefreshing = true
} else if (page == 1) {
isLoading = true
} else {
isLoadingMore = true
}
errorMessage = null
val userData = userService.getUsers(pageSize, page)
val newFriends: List<GroupMember> = userData.list.map { user ->
GroupMember(
id = user.chatAIId,
name = user.nickName,
avatar = user.avatar,
isAi = false
)
}
if (isRefresh || page == 1) {
friends = newFriends
currentPage = 1
} else {
friends = friends + newFriends
currentPage = page
}
hasMoreData = newFriends.size >= pageSize
} catch (e: Exception) {
errorMessage = "获取朋友列表失败: ${e.message}"
} finally {
isLoading = false
isRefreshing = false
isLoadingMore = false
}
}
}
// 刷新数据
fun refresh() {
loadFriends(1, true)
}
// 加载更多数据
fun loadMore() {
if (hasMoreData && !isLoadingMore && !isLoading) {
loadFriends(currentPage + 1)
}
}
// 清除错误信息
fun clearError() {
errorMessage = null
}
// 搜索过滤
fun getFilteredFriends(searchText: String): List<GroupMember> {
return if (searchText.isEmpty()) {
friends
} else {
friends.filter { it.name.contains(searchText, ignoreCase = true) }
}
}
}

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

View File

@@ -121,6 +121,7 @@
<string name="remove_account_password_hint">输入密码以确认</string>
<string name="version_text">版本 %1$s</string>
<string name="index_worldwide">探索</string>
<string name="index_dynamic">动态</string>
<string name="index_following">关注</string>
<string name="index_hot">热门</string>
<string name="main_home">首页</string>

View File

@@ -119,6 +119,7 @@
<string name="remove_account_password_hint">Please enter your password to confirm</string>
<string name="version_text">Version %1$s</string>
<string name="index_worldwide">Worldwide</string>
<string name="index_dynamic">Dynamic</string>
<string name="index_following">Following</string>
<string name="index_hot">Hot</string>
<string name="main_home">Home</string>