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

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.TrtcSignResponseBody
import com.aiosman.ravenow.data.api.UnRegisterMessageChannelRequestBody import com.aiosman.ravenow.data.api.UnRegisterMessageChannelRequestBody
import com.aiosman.ravenow.data.api.UpdateNoticeRequestBody 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.data.api.UpdateUserLangRequestBody
import com.aiosman.ravenow.entity.AccountFavouriteEntity import com.aiosman.ravenow.entity.AccountFavouriteEntity
import com.aiosman.ravenow.entity.AccountLikeEntity import com.aiosman.ravenow.entity.AccountLikeEntity
@@ -55,7 +57,9 @@ data class AccountProfile(
// trtcUserId // trtcUserId
val trtcUserId: String, val trtcUserId: String,
// aiAccount true:ai false:普通用户 // aiAccount true:ai false:普通用户
val aiAccount: Boolean val aiAccount: Boolean,
val chatAIId: String,
) { ) {
/** /**
* 转换为Entity * 转换为Entity
@@ -79,7 +83,8 @@ data class AccountProfile(
}, },
trtcUserId = trtcUserId, trtcUserId = trtcUserId,
aiAccount = aiAccount, aiAccount = aiAccount,
rawAvatar = avatar rawAvatar = avatar,
chatAIId = chatAIId
) )
} }
} }
@@ -394,6 +399,21 @@ interface AccountService {
suspend fun getAppConfig(): AppConfig suspend fun getAppConfig(): AppConfig
suspend fun removeAccount(password: String) 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 { class AccountServiceImpl : AccountService {
@@ -574,4 +594,17 @@ class AccountServiceImpl : AccountService {
throw ServiceException("Failed to remove account") 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, val updatedAt: String,
@SerializedName("useCount") @SerializedName("useCount")
val useCount: Int val useCount: Int
) { ) {
fun toAgentEntity(): AgentEntity { fun toAgentEntity(): AgentEntity {
return 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( data class LoginUserRequestBody(
@SerializedName("username") @SerializedName("username")
val username: String? = null, val username: String? = null,
@@ -529,6 +538,9 @@ interface RaveNowAPI {
@POST("outside/rooms/message") @POST("outside/rooms/message")
suspend fun sendChatAiMessage(@Body body: SendChatAiRequestBody): Response<DataContainer<Unit>> 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 trtcUserId: String,
val aiAccount: Boolean, 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.foundation.shape.CircleShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
@@ -15,7 +20,9 @@ import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun AiAgentListScreen( fun AiAgentListScreen(
searchText: String, searchText: String,
@@ -24,47 +31,101 @@ fun AiAgentListScreen(
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
var aiAgents by remember { mutableStateOf<List<GroupMember>>(emptyList()) } // 使用ViewModel
var isLoading by remember { mutableStateOf(false) } val viewModel = remember { AiAgentListViewModel() }
// 加载AI智能体数据 // 获取过滤后的数据
LaunchedEffect(Unit) { val filteredAgents = viewModel.getFilteredAgents(searchText)
isLoading = true
aiAgents = CreateGroupChatViewModel.getAiAgents()
isLoading = false
}
val filteredAgents = if (searchText.isEmpty()) { // 下拉刷新状态
aiAgents val pullRefreshState = rememberPullRefreshState(
} else { refreshing = viewModel.isRefreshing,
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) } onRefresh = {
} viewModel.refresh()
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
} }
} else { )
LazyColumn(
modifier = Modifier.fillMaxSize(), // 上拉加载更多
contentPadding = PaddingValues(16.dp), LaunchedEffect(listState) {
verticalArrangement = Arrangement.spacedBy(8.dp) snapshotFlow { listState.layoutInfo.visibleItemsInfo }
) { .collect { visibleItems ->
items(filteredAgents) { agent -> if (visibleItems.isNotEmpty()) {
MemberItem( val lastVisibleItem = visibleItems.last()
member = agent, if (lastVisibleItem.index >= filteredAgents.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
isSelected = selectedMemberIds.contains(agent.id), viewModel.loadMore()
onSelect = { onMemberSelect(agent) } }
}
}
}
// 显示错误信息
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.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -42,7 +43,8 @@ import kotlinx.coroutines.launch
data class GroupMember( data class GroupMember(
val id: String, val id: String,
val name: String, val name: String,
val avatar: String val avatar: String,
val isAi:Boolean = false
) )
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -61,6 +63,9 @@ fun CreateGroupChatScreen() {
var pagerState = rememberPagerState(pageCount = { 2 }) var pagerState = rememberPagerState(pageCount = { 2 })
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
// LazyRow状态管理
val lazyRowState = rememberLazyListState()
// 清除错误信息 // 清除错误信息
LaunchedEffect(groupName.text, searchText.text) { LaunchedEffect(groupName.text, searchText.text) {
if (CreateGroupChatViewModel.errorMessage != null) { if (CreateGroupChatViewModel.errorMessage != null) {
@@ -74,6 +79,15 @@ fun CreateGroupChatScreen() {
// 这样用户可以在AI智能体和朋友之间切换选中的状态会保持 // 这样用户可以在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 val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -230,52 +244,74 @@ fun CreateGroupChatScreen() {
// 已选成员列表 // 已选成员列表
if (selectedMembers.isNotEmpty()) { if (selectedMembers.isNotEmpty()) {
// 显示选中成员数量 // 显示选中成员数量
Text( /* Text(
text = "已选择 ${selectedMembers.size} 个成员", text = "已选择 ${selectedMembers.size} 个成员",
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
) )*/
LazyRow( LazyRow(
state = lazyRowState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(selectedMembers) { member -> items(selectedMembers) { member ->
Box { Column(
CustomAsyncImage( horizontalAlignment = Alignment.CenterHorizontally,
context = context, modifier = Modifier.width(48.dp)
imageUrl = member.avatar, ) {
contentDescription = member.name, Box {
modifier = Modifier CustomAsyncImage(
.size(48.dp) context = context,
.clip(CircleShape) imageUrl = member.avatar,
) contentDescription = member.name,
modifier = Modifier
// 删除按钮 .size(48.dp)
Box( .clip(CircleShape)
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
) )
// 删除按钮
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( Button(
onClick = { onClick = {
// 创建群聊逻辑 // 创建群聊逻辑
if (groupName.text.isNotEmpty() && selectedMembers.isNotEmpty()) { if (selectedMembers.isNotEmpty()) {
scope.launch { scope.launch {
val success = CreateGroupChatViewModel.createGroupChat( val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text, groupName = groupName.text,
memberIds = selectedMembers.map { it.id }, selectedMembers = selectedMembers,
context = context context = context
) )
if (success) { if (success) {
@@ -374,7 +410,7 @@ fun CreateGroupChatScreen() {
containerColor = AppColors.main, containerColor = AppColors.main,
contentColor = AppColors.mainText contentColor = AppColors.mainText
), ),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(24.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
) { ) {
if (CreateGroupChatViewModel.isLoading) { if (CreateGroupChatViewModel.isLoading) {

View File

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

View File

@@ -3,6 +3,11 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.foundation.shape.CircleShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
@@ -15,7 +20,9 @@ import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun FriendListScreen( fun FriendListScreen(
searchText: String, searchText: String,
@@ -24,47 +31,101 @@ fun FriendListScreen(
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
var friends by remember { mutableStateOf<List<GroupMember>>(emptyList()) } // 使用ViewModel
var isLoading by remember { mutableStateOf(false) } val viewModel = remember { FriendListViewModel() }
// 加载朋友数据 // 获取过滤后的数据
LaunchedEffect(Unit) { val filteredFriends = viewModel.getFilteredFriends(searchText)
isLoading = true
friends = CreateGroupChatViewModel.getFriends()
isLoading = false
}
val filteredFriends = if (searchText.isEmpty()) { // 下拉刷新状态
friends val pullRefreshState = rememberPullRefreshState(
} else { refreshing = viewModel.isRefreshing,
friends.filter { it.name.contains(searchText, ignoreCase = true) } onRefresh = {
} viewModel.refresh()
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
} }
} else { )
LazyColumn(
modifier = Modifier.fillMaxSize(), // 上拉加载更多
contentPadding = PaddingValues(16.dp), LaunchedEffect(listState) {
verticalArrangement = Arrangement.spacedBy(8.dp) snapshotFlow { listState.layoutInfo.visibleItemsInfo }
) { .collect { visibleItems ->
items(filteredFriends) { friend -> if (visibleItems.isNotEmpty()) {
MemberItem( val lastVisibleItem = visibleItems.last()
member = friend, if (lastVisibleItem.index >= filteredFriends.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
isSelected = selectedMemberIds.contains(friend.id), viewModel.loadMore()
onSelect = { onMemberSelect(friend) } }
}
}
}
// 显示错误信息
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.follower.FollowerNoticeViewModel
import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListScreen 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.FriendChatListScreen
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListScreen
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -244,7 +245,7 @@ fun NotificationsScreen() {
} }
1 -> { 1 -> {
GroupChatListScreen()
} }
2 -> { 2 -> {

View File

@@ -145,7 +145,7 @@ object FriendChatListViewModel : ViewModel() {
// 过滤掉ai_group的会话只保留朋友聊天 // 过滤掉ai_group的会话只保留朋友聊天
val filteredConversations = result?.conversationList?.filter { conversation -> val filteredConversations = result?.conversationList?.filter { conversation ->
// 排除ai_group的会话 // 排除ai_group的会话
!conversation.conversationGroupList.contains("ai_group") !conversation.conversationGroupList.contains("ai_group")&& conversation.type == V2TIMConversation.V2TIM_C2C
} ?: emptyList() } ?: emptyList()
friendChatList = filteredConversations.map { msg: V2TIMConversation -> 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 = val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var pagerState = rememberPagerState { 3 } var pagerState = rememberPagerState { 4 }
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
Column( Column(
modifier = Modifier modifier = Modifier
@@ -121,7 +121,7 @@ fun MomentsList() {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = stringResource(R.string.index_following), text = stringResource(R.string.index_dynamic),
fontSize = 16.sp, fontSize = 16.sp,
color = if (pagerState.currentPage == 1) AppColors.text else AppColors.nonActiveText, color = if (pagerState.currentPage == 1) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600) fontWeight = FontWeight.W600)
@@ -139,7 +139,6 @@ fun MomentsList() {
) )
} }
//热门tab
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column( Column(
modifier = Modifier modifier = Modifier
@@ -152,7 +151,7 @@ fun MomentsList() {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = stringResource(R.string.index_hot), text = stringResource(R.string.index_following),
fontSize = 16.sp, fontSize = 16.sp,
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText, color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600) fontWeight = FontWeight.W600)
@@ -169,6 +168,37 @@ fun MomentsList() {
.height(4.dp) .height(4.dp)
) )
}
//热门tab
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(3)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.index_hot),
fontSize = 16.sp,
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 == 3) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
} }
//搜索按钮 //搜索按钮
Column( Column(
@@ -197,14 +227,17 @@ fun MomentsList() {
) { ) {
when (it) { when (it) {
0 -> { 0 -> {
//ExploreMomentsList()
}
1 -> {
ExploreMomentsList() ExploreMomentsList()
} }
1 -> { 2 -> {
TimelineMomentsList() TimelineMomentsList()
} }
2 -> { 3 -> {
HotMomentsList() HotMomentsList()
} }

View File

@@ -121,6 +121,7 @@
<string name="remove_account_password_hint">输入密码以确认</string> <string name="remove_account_password_hint">输入密码以确认</string>
<string name="version_text">版本 %1$s</string> <string name="version_text">版本 %1$s</string>
<string name="index_worldwide">探索</string> <string name="index_worldwide">探索</string>
<string name="index_dynamic">动态</string>
<string name="index_following">关注</string> <string name="index_following">关注</string>
<string name="index_hot">热门</string> <string name="index_hot">热门</string>
<string name="main_home">首页</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="remove_account_password_hint">Please enter your password to confirm</string>
<string name="version_text">Version %1$s</string> <string name="version_text">Version %1$s</string>
<string name="index_worldwide">Worldwide</string> <string name="index_worldwide">Worldwide</string>
<string name="index_dynamic">Dynamic</string>
<string name="index_following">Following</string> <string name="index_following">Following</string>
<string name="index_hot">Hot</string> <string name="index_hot">Hot</string>
<string name="main_home">Home</string> <string name="main_home">Home</string>