预留首页-探索,完善群组功能
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,24 +31,49 @@ 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 pullRefreshState = rememberPullRefreshState(
|
||||||
|
refreshing = viewModel.isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 上拉加载更多
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredAgents = if (searchText.isEmpty()) {
|
// 显示错误信息
|
||||||
aiAgents
|
viewModel.errorMessage?.let { error ->
|
||||||
} else {
|
LaunchedEffect(error) {
|
||||||
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
|
// 可以在这里显示错误提示
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pullRefresh(pullRefreshState)
|
||||||
|
) {
|
||||||
|
if (viewModel.isLoading && filteredAgents.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -54,6 +86,7 @@ fun AiAgentListScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -65,6 +98,34 @@ fun AiAgentListScreen(
|
|||||||
onSelect = { onMemberSelect(agent) }
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,19 +244,24 @@ 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 ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.width(48.dp)
|
||||||
|
) {
|
||||||
Box {
|
Box {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -277,6 +296,23 @@ fun CreateGroupChatScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 名称显示
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
|
val response = accountService.createGroupChat(groupName, userIds, promptIds)
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
true
|
true
|
||||||
|
} else {
|
||||||
|
isLoading = false
|
||||||
|
errorMessage = "创建群聊失败: ${response.message()}"
|
||||||
|
false
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
errorMessage = "创建群聊失败: ${e.message}"
|
errorMessage = "创建群聊失败: ${e.message}"
|
||||||
|
|||||||
@@ -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,24 +31,49 @@ 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 pullRefreshState = rememberPullRefreshState(
|
||||||
|
refreshing = viewModel.isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 上拉加载更多
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredFriends = if (searchText.isEmpty()) {
|
// 显示错误信息
|
||||||
friends
|
viewModel.errorMessage?.let { error ->
|
||||||
} else {
|
LaunchedEffect(error) {
|
||||||
friends.filter { it.name.contains(searchText, ignoreCase = true) }
|
// 可以在这里显示错误提示
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pullRefresh(pullRefreshState)
|
||||||
|
) {
|
||||||
|
if (viewModel.isLoading && filteredFriends.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -54,6 +86,7 @@ fun FriendListScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -65,6 +98,34 @@ fun FriendListScreen(
|
|||||||
onSelect = { onMemberSelect(friend) }
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 -> {
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.message.tab
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun GroupChatListScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
val state = rememberPullRefreshState(
|
||||||
|
refreshing = GroupChatListViewModel.refreshing,
|
||||||
|
onRefresh = {
|
||||||
|
GroupChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
GroupChatListViewModel.refreshPager(context = context)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pullRefresh(state)
|
||||||
|
) {
|
||||||
|
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "暂无群聊",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "您还没有加入任何群聊",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = GroupChatListViewModel.groupChatList,
|
||||||
|
key = { _, item -> item.id }
|
||||||
|
) { index, item ->
|
||||||
|
GroupChatItem(
|
||||||
|
conversation = item,
|
||||||
|
onGroupAvatarClick = { conv ->
|
||||||
|
GroupChatListViewModel.goToGroupDetail(conv, navController)
|
||||||
|
},
|
||||||
|
onChatClick = { conv ->
|
||||||
|
GroupChatListViewModel.goToChat(conv, navController)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index < GroupChatListViewModel.groupChatList.size - 1) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
color = AppColors.divider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = AppColors.main
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = GroupChatListViewModel.refreshing,
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupChatListViewModel.error?.let { error ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = AppColors.error,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatItem(
|
||||||
|
conversation: GroupConversation,
|
||||||
|
onGroupAvatarClick: (GroupConversation) -> Unit = {},
|
||||||
|
onChatClick: (GroupConversation) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 12.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onChatClick(conversation)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = LocalContext.current,
|
||||||
|
imageUrl = conversation.avatar,
|
||||||
|
contentDescription = conversation.groupName,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.noRippleClickable {
|
||||||
|
onGroupAvatarClick(conversation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = conversation.groupName,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = conversation.lastMessageTime,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = AppColors.secondaryText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
if (conversation.unreadCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
|
||||||
|
.background(
|
||||||
|
color = AppColors.main,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
|
||||||
|
color = AppColors.mainText,
|
||||||
|
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.message.tab
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.icu.util.Calendar
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.navigateToChat
|
||||||
|
import com.tencent.imsdk.v2.V2TIMConversation
|
||||||
|
import com.tencent.imsdk.v2.V2TIMConversationResult
|
||||||
|
import com.tencent.imsdk.v2.V2TIMManager
|
||||||
|
import com.tencent.imsdk.v2.V2TIMMessage
|
||||||
|
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
data class GroupConversation(
|
||||||
|
val id: String,
|
||||||
|
val groupId: String,
|
||||||
|
val groupName: String,
|
||||||
|
val lastMessage: String,
|
||||||
|
val lastMessageTime: String,
|
||||||
|
val avatar: String = "",
|
||||||
|
val unreadCount: Int = 0,
|
||||||
|
val displayText: String,
|
||||||
|
val isSelf: Boolean,
|
||||||
|
val memberCount: Int = 0
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun convertToGroupConversation(msg: V2TIMConversation, context: Context): GroupConversation {
|
||||||
|
val lastMessage = Calendar.getInstance().apply {
|
||||||
|
timeInMillis = msg.lastMessage?.timestamp ?: 0
|
||||||
|
timeInMillis *= 1000
|
||||||
|
}
|
||||||
|
var displayText = ""
|
||||||
|
when (msg.lastMessage?.elemType) {
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
||||||
|
displayText = msg.lastMessage?.textElem?.text ?: ""
|
||||||
|
}
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
||||||
|
displayText = "[图片]"
|
||||||
|
}
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_SOUND -> {
|
||||||
|
displayText = "[语音]"
|
||||||
|
}
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_VIDEO -> {
|
||||||
|
displayText = "[视频]"
|
||||||
|
}
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_FILE -> {
|
||||||
|
displayText = "[文件]"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
displayText = "[消息]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GroupConversation(
|
||||||
|
id = msg.conversationID,
|
||||||
|
groupId = msg.groupID,
|
||||||
|
groupName = msg.showName,
|
||||||
|
lastMessage = msg.lastMessage?.textElem?.text ?: "",
|
||||||
|
lastMessageTime = lastMessage.time.formatChatTime(context),
|
||||||
|
avatar = "${ConstVars.BASE_SERVER}${msg.faceUrl}",
|
||||||
|
unreadCount = msg.unreadCount,
|
||||||
|
displayText = displayText,
|
||||||
|
isSelf = msg.lastMessage?.sender == AppState.profile?.trtcUserId,
|
||||||
|
memberCount = msg.groupAtInfoList?.size ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object GroupChatListViewModel : ViewModel() {
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
|
var groupChatList by mutableStateOf<List<GroupConversation>>(emptyList())
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var refreshing by mutableStateOf(false)
|
||||||
|
var hasNext by mutableStateOf(true)
|
||||||
|
var currentPage by mutableStateOf(1)
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
private val pageSize = 20
|
||||||
|
|
||||||
|
fun refreshPager(pullRefresh: Boolean = false, context: Context? = null) {
|
||||||
|
if (isLoading && !pullRefresh) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
refreshing = pullRefresh
|
||||||
|
error = null
|
||||||
|
context?.let { loadGroupChatList(it) }
|
||||||
|
currentPage = 1
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = ""
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
refreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMore() {
|
||||||
|
if (isLoading || !hasNext) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
// 腾讯IM的会话列表是一次性获取的,这里模拟分页
|
||||||
|
// 实际项目中可能需要根据时间戳或其他方式实现真正的分页
|
||||||
|
hasNext = false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = ""
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadGroupChatList(context: Context) {
|
||||||
|
val result = suspendCoroutine { continuation ->
|
||||||
|
// 获取全部会话列表
|
||||||
|
V2TIMManager.getConversationManager().getConversationList(
|
||||||
|
0,
|
||||||
|
Int.MAX_VALUE,
|
||||||
|
object : V2TIMValueCallback<V2TIMConversationResult> {
|
||||||
|
override fun onSuccess(t: V2TIMConversationResult?) {
|
||||||
|
continuation.resumeWith(Result.success(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, desc: String?) {
|
||||||
|
continuation.resumeWith(Result.failure(Exception("Error $code: $desc")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只保留群聊会话,过滤掉单聊会话
|
||||||
|
val filteredConversations = result?.conversationList?.filter { conversation ->
|
||||||
|
// 只保留群聊会话(conversationType为2表示群聊)
|
||||||
|
conversation.type == V2TIMConversation.V2TIM_GROUP
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
groupChatList = filteredConversations.map { msg: V2TIMConversation ->
|
||||||
|
val conversation = GroupConversation.convertToGroupConversation(msg, context)
|
||||||
|
println("GroupChatList: Conversation ${conversation.groupName} has ${conversation.unreadCount} unread messages")
|
||||||
|
conversation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToChat(
|
||||||
|
conversation: GroupConversation,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// 群聊直接使用群ID进行导航
|
||||||
|
navController.navigateToChat(conversation.groupId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = ""
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToGroupDetail(
|
||||||
|
conversation: GroupConversation,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// 可以导航到群详情页面,这里暂时使用群聊页面
|
||||||
|
navController.navigateToChat(conversation.groupId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = ""
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshConversation(context: Context, groupId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadGroupChatList(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ fun MomentsList() {
|
|||||||
val navigationBarPaddings =
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user