diff --git a/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt b/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt index 0ca29ac..0b7be48 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt @@ -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>> + + /** + * 创建群聊 + * @param name 群聊名称 + * @param userIds 用户ID列表 + * @param promptIds AI智能体ID列表 + */ + suspend fun createGroupChat(name: String, userIds: List, promptIds: List): retrofit2.Response> } 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>> { + return ApiClient.api.getAgent(page, pageSize) + } + + override suspend fun createGroupChat(name: String, userIds: List, promptIds: List): retrofit2.Response> { + val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody( + name = name, + userIds = userIds, + promptIds = promptIds + ) + return ApiClient.api.createGroupChat(requestBody) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt b/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt index d600147..c973673 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt @@ -29,6 +29,7 @@ data class Agent( val updatedAt: String, @SerializedName("useCount") val useCount: Int + ) { fun toAgentEntity(): AgentEntity { return AgentEntity( diff --git a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt index be98f05..bc55bd3 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt @@ -56,6 +56,15 @@ data class SendChatAiRequestBody( ) +data class CreateGroupChatRequestBody( + @SerializedName("name") + val name: String, + @SerializedName("userIds") + val userIds: List, + @SerializedName("promptIds") + val promptIds: List, + ) + 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> + @POST("outside/rooms") + suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response> + diff --git a/app/src/main/java/com/aiosman/ravenow/entity/Account.kt b/app/src/main/java/com/aiosman/ravenow/entity/Account.kt index 0f2dbfa..0dd763b 100644 --- a/app/src/main/java/com/aiosman/ravenow/entity/Account.kt +++ b/app/src/main/java/com/aiosman/ravenow/entity/Account.kt @@ -63,7 +63,9 @@ data class AccountProfileEntity( val trtcUserId: String, val aiAccount: Boolean, - val rawAvatar: String + val rawAvatar: String, + + val chatAIId: String, ) /** diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt index 94f2fce..049ba19 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt @@ -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>(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 + ) } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListViewModel.kt new file mode 100644 index 0000000..4df597a --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListViewModel.kt @@ -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>(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(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 = 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 { + return if (searchText.isEmpty()) { + aiAgents + } else { + aiAgents.filter { it.name.contains(searchText, ignoreCase = true) } + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt index bd138b9..d9c76a1 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt @@ -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) { diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt index f2767a7..2b40487 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt @@ -79,19 +79,25 @@ object CreateGroupChatViewModel : ViewModel() { // 创建群聊 suspend fun createGroupChat( groupName: String, - memberIds: List, + selectedMembers: List, 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}" diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt index ded192a..15ec82b 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt @@ -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>(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 + ) } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListViewModel.kt new file mode 100644 index 0000000..5ad7f59 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListViewModel.kt @@ -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>(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(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 = 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 { + return if (searchText.isEmpty()) { + friends + } else { + friends.filter { it.name.contains(searchText, ignoreCase = true) } + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt index e41484d..23ef449 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt @@ -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 -> { diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/FriendChatListViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/FriendChatListViewModel.kt index 52c51c7..2358cb4 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/FriendChatListViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/FriendChatListViewModel.kt @@ -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 -> diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt new file mode 100644 index 0000000..7a9325a --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt @@ -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 + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListViewModel.kt new file mode 100644 index 0000000..efb424a --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListViewModel.kt @@ -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>(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(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 { + 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) + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt index d9ac943..ca793bc 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt @@ -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() } diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8cbca05..d1683e9 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -121,6 +121,7 @@ 输入密码以确认 版本 %1$s 探索 + 动态 关注 热门 首页 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b99f5ae..410cc06 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,6 +119,7 @@ Please enter your password to confirm Version %1$s Worldwide + Dynamic Following Hot Home