智能体会话开发
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.aiosman.ravenow.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.data.api.AppConfig
|
import com.aiosman.ravenow.data.api.AppConfig
|
||||||
import com.aiosman.ravenow.data.api.CaptchaInfo
|
import com.aiosman.ravenow.data.api.CaptchaInfo
|
||||||
@@ -53,6 +54,8 @@ data class AccountProfile(
|
|||||||
val banner: String?,
|
val banner: String?,
|
||||||
// trtcUserId
|
// trtcUserId
|
||||||
val trtcUserId: String,
|
val trtcUserId: String,
|
||||||
|
// aiAccount true:ai false:普通用户
|
||||||
|
val aiAccount: Boolean
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 转换为Entity
|
* 转换为Entity
|
||||||
@@ -63,7 +66,13 @@ data class AccountProfile(
|
|||||||
followerCount = followerCount,
|
followerCount = followerCount,
|
||||||
followingCount = followingCount,
|
followingCount = followingCount,
|
||||||
nickName = nickname,
|
nickName = nickname,
|
||||||
avatar = "${ApiClient.BASE_SERVER}$avatar",
|
avatar = if (aiAccount) {
|
||||||
|
// 对于AI账户,直接使用原始头像URL,不添加服务器前缀
|
||||||
|
"${ApiClient.BASE_API_URL+"/outside"}$avatar"+"?token="+"Bearer ${AppStore.token}"
|
||||||
|
} else {
|
||||||
|
// 对于普通用户,添加服务器前缀
|
||||||
|
"${ApiClient.BASE_SERVER}$avatar"
|
||||||
|
},
|
||||||
bio = bio,
|
bio = bio,
|
||||||
country = "Worldwide",
|
country = "Worldwide",
|
||||||
isFollowing = isFollowing,
|
isFollowing = isFollowing,
|
||||||
@@ -74,6 +83,7 @@ data class AccountProfile(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
trtcUserId = trtcUserId,
|
trtcUserId = trtcUserId,
|
||||||
|
aiAccount = aiAccount,
|
||||||
rawAvatar = avatar
|
rawAvatar = avatar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,23 @@ interface UserService {
|
|||||||
followingId: Int? = null
|
followingId: Int? = null
|
||||||
): ListContainer<AccountProfileEntity>
|
): ListContainer<AccountProfileEntity>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param id 用户ID
|
||||||
|
* @return 用户信息
|
||||||
|
*/
|
||||||
|
|
||||||
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
|
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param id 用户ID
|
||||||
|
* @return 用户信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
suspend fun getUserProfileByOpenId(id: String):AccountProfileEntity
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserServiceImpl : UserService {
|
class UserServiceImpl : UserService {
|
||||||
@@ -97,4 +112,10 @@ class UserServiceImpl : UserService {
|
|||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return body.data.toAccountProfileEntity()
|
return body.data.toAccountProfileEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getUserProfileByOpenId(id: String): AccountProfileEntity {
|
||||||
|
val resp = ApiClient.api.getAccountProfileByOpenId(id)
|
||||||
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
|
return body.data.toAccountProfileEntity()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,12 @@ interface RaveNowAPI {
|
|||||||
@Path("id") id: String
|
@Path("id") id: String
|
||||||
): Response<DataContainer<AccountProfile>>
|
): Response<DataContainer<AccountProfile>>
|
||||||
|
|
||||||
|
|
||||||
|
@GET("profile/aichat/profile/{id}")
|
||||||
|
suspend fun getAccountProfileByOpenId(
|
||||||
|
@Path("id") id: String
|
||||||
|
): Response<DataContainer<AccountProfile>>
|
||||||
|
|
||||||
@POST("user/{id}/follow")
|
@POST("user/{id}/follow")
|
||||||
suspend fun followUser(
|
suspend fun followUser(
|
||||||
@Path("id") id: Int
|
@Path("id") id: Int
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ data class AccountProfileEntity(
|
|||||||
val banner: String?,
|
val banner: String?,
|
||||||
// trtcUserId
|
// trtcUserId
|
||||||
val trtcUserId: String,
|
val trtcUserId: String,
|
||||||
|
|
||||||
|
val aiAccount: Boolean,
|
||||||
val rawAvatar: String
|
val rawAvatar: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ data class AgentEntity(
|
|||||||
//val profile: ProfileEntity,
|
//val profile: ProfileEntity,
|
||||||
val title: String,
|
val title: String,
|
||||||
val updatedAt: String,
|
val updatedAt: String,
|
||||||
val useCount: Int
|
val useCount: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ProfileEntity(
|
data class ProfileEntity(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.account.AccountEditScreen2
|
|||||||
import com.aiosman.ravenow.ui.account.AccountSetting
|
import com.aiosman.ravenow.ui.account.AccountSetting
|
||||||
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
||||||
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
||||||
|
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatScreen
|
import com.aiosman.ravenow.ui.chat.ChatScreen
|
||||||
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
||||||
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
||||||
@@ -91,6 +92,7 @@ sealed class NavigationRoute(
|
|||||||
data object ResetPassword : NavigationRoute("ResetPassword")
|
data object ResetPassword : NavigationRoute("ResetPassword")
|
||||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||||
data object Chat : NavigationRoute("Chat/{id}")
|
data object Chat : NavigationRoute("Chat/{id}")
|
||||||
|
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
||||||
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
||||||
data object ImageCrop : NavigationRoute("ImageCrop")
|
data object ImageCrop : NavigationRoute("ImageCrop")
|
||||||
data object AccountSetting : NavigationRoute("AccountSetting")
|
data object AccountSetting : NavigationRoute("AccountSetting")
|
||||||
@@ -369,6 +371,19 @@ fun NavigationController(
|
|||||||
ChatScreen(it.arguments?.getString("id")!!)
|
ChatScreen(it.arguments?.getString("id")!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.ChatAi.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
ChatAiScreen(it.arguments?.getString("id")!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
composable(route = NavigationRoute.CommentNoticeScreen.route) {
|
composable(route = NavigationRoute.CommentNoticeScreen.route) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAnimatedContentScope provides this,
|
LocalAnimatedContentScope provides this,
|
||||||
@@ -457,6 +472,15 @@ fun NavHostController.navigateToChat(id: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToChatnavigateToChatAi(id: String) {
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.Chat.route
|
||||||
|
.replace("{id}", id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun NavHostController.goTo(
|
fun NavHostController.goTo(
|
||||||
route: NavigationRoute
|
route: NavigationRoute
|
||||||
) {
|
) {
|
||||||
|
|||||||
652
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiScreen.kt
Normal file
652
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiScreen.kt
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||||
|
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.tencent.imsdk.v2.V2TIMMessage
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiScreen(userId: String) {
|
||||||
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalNavController.current.context
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
var goToNewCount by remember { mutableStateOf(0) }
|
||||||
|
val viewModel = viewModel<ChatAiViewModel>(
|
||||||
|
key = "ChatViewModel_$userId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return ChatViewModel(userId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
var isLoadingMore by remember { mutableStateOf(false) } // Add a state for loading
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(context = context)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
viewModel.UnRegistListener()
|
||||||
|
viewModel.clearUnRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
|
}
|
||||||
|
var inBottom by remember { mutableStateOf(true) }
|
||||||
|
// 监听滚动状态,触发加载更多
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
Log.d("ChatScreen", "lastVisibleItemIndex: ${index}")
|
||||||
|
if (index == listState.layoutInfo.totalItemsCount - 1) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.onLoadMore(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 监听滚动状态,触发滚动到底部
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
inBottom = index == 0
|
||||||
|
if (index == 0) {
|
||||||
|
goToNewCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 监听是否需要滚动到最新消息
|
||||||
|
LaunchedEffect(viewModel.goToNew) {
|
||||||
|
if (viewModel.goToNew) {
|
||||||
|
if (inBottom) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
} else {
|
||||||
|
goToNewCount++
|
||||||
|
}
|
||||||
|
viewModel.goToNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp, horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = viewModel.userProfile?.avatar ?: "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp)),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = viewModel.userProfile?.nickName ?: "",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
isMenuExpanded = true
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
AppColors.text)
|
||||||
|
)
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isMenuExpanded,
|
||||||
|
onDismissRequest = {
|
||||||
|
isMenuExpanded = false
|
||||||
|
},
|
||||||
|
menuItems = listOf(
|
||||||
|
MenuItem(
|
||||||
|
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
|
||||||
|
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
||||||
|
) {
|
||||||
|
|
||||||
|
isMenuExpanded = false
|
||||||
|
viewModel.viewModelScope.launch {
|
||||||
|
if (viewModel.notificationStrategy == "mute") {
|
||||||
|
viewModel.updateNotificationStrategy("active")
|
||||||
|
} else {
|
||||||
|
viewModel.updateNotificationStrategy("mute")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(
|
||||||
|
AppColors.decentBackground)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
ChatInput(
|
||||||
|
onSendImage = {
|
||||||
|
it?.let {
|
||||||
|
viewModel.sendImageMessage(it, context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
viewModel.sendMessage(it, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
reverseLayout = true,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
|
||||||
|
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
|
||||||
|
val item = chatList[index]
|
||||||
|
if (item.showTimeDivider) {
|
||||||
|
val calendar = java.util.Calendar.getInstance()
|
||||||
|
calendar.timeInMillis = item.timestamp
|
||||||
|
Text(
|
||||||
|
text = calendar.time.formatChatTime(context), // Format the timestamp
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatItem(item = item, viewModel.myProfile?.trtcUserId!!)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// item {
|
||||||
|
// Spacer(modifier = Modifier.height(72.dp))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
if (goToNewCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(bottom = 16.dp, end = 16.dp)
|
||||||
|
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${goToNewCount} New Message",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiSelfItem(item: ChatItem) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color(0xFF000000))
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "Unsupported message type",
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiOtherItem(item: ChatItem) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "Unsupported message type",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiItem(item: ChatItem, currentUserId: String) {
|
||||||
|
val isCurrentUser = item.userId == currentUserId
|
||||||
|
if (isCurrentUser) {
|
||||||
|
ChatAiSelfItem(item)
|
||||||
|
} else {
|
||||||
|
ChatAiOtherItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatAiInput(
|
||||||
|
onSendImage: (Uri?) -> Unit = {},
|
||||||
|
onSend: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
|
}
|
||||||
|
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
|
||||||
|
var isKeyboardOpen by remember { mutableStateOf(false) }
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val inputBarHeight by animateDpAsState(
|
||||||
|
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = androidx.compose.animation.core.LinearEasing
|
||||||
|
), label = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在 isKeyboardOpen 变化时立即更新 inputBarHeight 的动画目标值
|
||||||
|
LaunchedEffect(isKeyboardOpen) {
|
||||||
|
inputBarHeight // 触发 inputBarHeight 的重组
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val windowInsets = WindowInsets.ime
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val currentDensity by rememberUpdatedState(density)
|
||||||
|
|
||||||
|
LaunchedEffect(windowInsets.getBottom(currentDensity)) {
|
||||||
|
if (windowInsets.getBottom(currentDensity) <= 0) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imagePickUpLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
onSendImage(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = inputBarHeight)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(appColors.background)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = {
|
||||||
|
text = it
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(appColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isKeyboardOpen = focusState.isFocused
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
keyboardController = softwareKeyboardController
|
||||||
|
awaitFirstDown().also {
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_camera),
|
||||||
|
contentDescription = "Emoji",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
imagePickUpLauncher.launch(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "image/*"
|
||||||
|
},
|
||||||
|
"Select Image"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = appColors.chatActionColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Crossfade(
|
||||||
|
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
||||||
|
label = ""
|
||||||
|
) { isNotEmpty ->
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_video_share),
|
||||||
|
contentDescription = "Emoji",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
onSend(text)
|
||||||
|
text = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tint = if (isNotEmpty) appColors.main else appColors.chatActionColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: ChatAiViewModel): List<ChatItem> {
|
||||||
|
for (i in chatList.indices) { // Iterate in normal order
|
||||||
|
if (i == 0) {
|
||||||
|
viewModel.showTimestampMap[chatList[i].msgId] = false
|
||||||
|
chatList[i].showTimeDivider = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val currentMessage = chatList[i]
|
||||||
|
val timeDiff = currentMessage.timestamp - chatList[i - 1].timestamp
|
||||||
|
// 时间间隔大于 3 分钟,显示时间戳
|
||||||
|
if (-timeDiff > 30 * 60 * 1000) {
|
||||||
|
viewModel.showTimestampMap[currentMessage.msgId] = true
|
||||||
|
currentMessage.showTimeDivider = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chatList
|
||||||
|
}
|
||||||
271
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiViewModel.kt
Normal file
271
app/src/main/java/com/aiosman/ravenow/ui/chat/ChatAiViewModel.kt
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
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.ChatState
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
|
||||||
|
import com.tencent.imsdk.v2.V2TIMCallback
|
||||||
|
import com.tencent.imsdk.v2.V2TIMManager
|
||||||
|
import com.tencent.imsdk.v2.V2TIMMessage
|
||||||
|
import com.tencent.imsdk.v2.V2TIMSendCallback
|
||||||
|
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|
||||||
|
class ChatAiViewModel(
|
||||||
|
val userId: String,
|
||||||
|
) : ViewModel() {
|
||||||
|
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
||||||
|
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
var textMessageListener: V2TIMAdvancedMsgListener? = null
|
||||||
|
var hasMore by mutableStateOf(true)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var lastMessage: V2TIMMessage? = null
|
||||||
|
val showTimestampMap = mutableMapOf<String, Boolean>() // Add this map
|
||||||
|
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
||||||
|
var goToNew by mutableStateOf(false)
|
||||||
|
fun init(context: Context) {
|
||||||
|
// 获取用户信息
|
||||||
|
viewModelScope.launch {
|
||||||
|
val resp = userService.getUserProfile(userId)
|
||||||
|
userProfile = resp
|
||||||
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
|
||||||
|
RegistListener(context)
|
||||||
|
fetchHistoryMessage(context)
|
||||||
|
// 获取通知信息
|
||||||
|
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
|
||||||
|
chatNotification = notiStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun RegistListener(context: Context) {
|
||||||
|
textMessageListener = object : V2TIMAdvancedMsgListener() {
|
||||||
|
override fun onRecvNewMessage(msg: V2TIMMessage?) {
|
||||||
|
super.onRecvNewMessage(msg)
|
||||||
|
msg?.let {
|
||||||
|
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = userProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
V2TIMManager.getMessageManager().addAdvancedMsgListener(textMessageListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UnRegistListener() {
|
||||||
|
V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearUnRead() {
|
||||||
|
val conversationID = "c2c_${userProfile?.trtcUserId}"
|
||||||
|
V2TIMManager.getConversationManager()
|
||||||
|
.cleanConversationUnreadMessageCount(conversationID, 0, 0, object : V2TIMCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
Log.i("imsdk", "success")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, desc: String) {
|
||||||
|
Log.i("imsdk", "failure, code:$code, desc:$desc")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onLoadMore(context: Context) {
|
||||||
|
if (!hasMore || isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading = true
|
||||||
|
viewModelScope.launch {
|
||||||
|
V2TIMManager.getMessageManager().getC2CHistoryMessageList(
|
||||||
|
userProfile?.trtcUserId!!,
|
||||||
|
20,
|
||||||
|
lastMessage,
|
||||||
|
object : V2TIMValueCallback<List<V2TIMMessage>> {
|
||||||
|
override fun onSuccess(p0: List<V2TIMMessage>?) {
|
||||||
|
chatData = chatData + (p0 ?: emptyList()).map {
|
||||||
|
var avatar = userProfile?.avatar
|
||||||
|
if (it.isSelf) {
|
||||||
|
avatar = myProfile?.avatar
|
||||||
|
}
|
||||||
|
ChatItem.convertToChatItem(it, context,avatar)
|
||||||
|
}.filterNotNull()
|
||||||
|
if ((p0?.size ?: 0) < 20) {
|
||||||
|
hasMore = false
|
||||||
|
}
|
||||||
|
lastMessage = p0?.lastOrNull()
|
||||||
|
isLoading = false
|
||||||
|
Log.d("ChatViewModel", "fetch history message success")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("ChatViewModel", "fetch history message error: $p1")
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendMessage(message: String, context: Context) {
|
||||||
|
V2TIMManager.getInstance().sendC2CTextMessage(
|
||||||
|
message,
|
||||||
|
userProfile?.trtcUserId!!,
|
||||||
|
object : V2TIMSendCallback<V2TIMMessage> {
|
||||||
|
override fun onProgress(p0: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("ChatViewModel", "send message error: $p1")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(p0: V2TIMMessage?) {
|
||||||
|
Log.d("ChatViewModel", "send message success")
|
||||||
|
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendImageMessage(imageUri: Uri, context: Context) {
|
||||||
|
val tempFile = createTempFile(context, imageUri)
|
||||||
|
val imagePath = tempFile?.path
|
||||||
|
if (imagePath != null) {
|
||||||
|
val v2TIMMessage = V2TIMManager.getMessageManager().createImageMessage(imagePath)
|
||||||
|
V2TIMManager.getMessageManager().sendMessage(
|
||||||
|
v2TIMMessage,
|
||||||
|
userProfile?.trtcUserId!!,
|
||||||
|
null,
|
||||||
|
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
object : V2TIMSendCallback<V2TIMMessage> {
|
||||||
|
override fun onProgress(p0: Int) {
|
||||||
|
Log.d("ChatViewModel", "send image message progress: $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("ChatViewModel", "send image message error: $p1")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(p0: V2TIMMessage?) {
|
||||||
|
Log.d("ChatViewModel", "send image message success")
|
||||||
|
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTempFile(context: Context, uri: Uri): File? {
|
||||||
|
return try {
|
||||||
|
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||||
|
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||||
|
cursor?.use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||||
|
val filePath = it.getString(columnIndex)
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
||||||
|
val tempFile =
|
||||||
|
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
||||||
|
val outputStream = FileOutputStream(tempFile)
|
||||||
|
|
||||||
|
inputStream?.use { input ->
|
||||||
|
outputStream.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchHistoryMessage(context: Context) {
|
||||||
|
V2TIMManager.getMessageManager().getC2CHistoryMessageList(
|
||||||
|
userProfile?.trtcUserId!!,
|
||||||
|
20,
|
||||||
|
null,
|
||||||
|
object : V2TIMValueCallback<List<V2TIMMessage>> {
|
||||||
|
override fun onSuccess(p0: List<V2TIMMessage>?) {
|
||||||
|
chatData = (p0 ?: emptyList()).mapNotNull {
|
||||||
|
var avatar = userProfile?.avatar
|
||||||
|
if (it.isSelf) {
|
||||||
|
avatar = myProfile?.avatar
|
||||||
|
}
|
||||||
|
ChatItem.convertToChatItem(it, context,avatar)
|
||||||
|
}
|
||||||
|
if ((p0?.size ?: 0) < 20) {
|
||||||
|
hasMore = false
|
||||||
|
}
|
||||||
|
lastMessage = p0?.lastOrNull()
|
||||||
|
Log.d("ChatViewModel", "fetch history message success")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("ChatViewModel", "fetch history message error: $p1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDisplayChatList(): List<ChatItem> {
|
||||||
|
val list = chatData
|
||||||
|
// Update showTimestamp for each message
|
||||||
|
for (item in list) {
|
||||||
|
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateNotificationStrategy(strategy: String) {
|
||||||
|
userProfile?.let {
|
||||||
|
val result = ChatState.updateChatNotification(it.id, strategy)
|
||||||
|
chatNotification = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
||||||
|
}
|
||||||
@@ -34,12 +34,14 @@ import com.aiosman.ravenow.entity.AgentEntity
|
|||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.exp.timeAgo
|
import com.aiosman.ravenow.exp.timeAgo
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentCard(
|
fun AgentCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
agentEntity: AgentEntity,
|
agentEntity: AgentEntity,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -50,6 +52,9 @@ fun AgentCard(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 8.dp)
|
modifier = Modifier.padding(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onClick ()
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.composables.AgentCard
|
import com.aiosman.ravenow.ui.composables.AgentCard
|
||||||
|
import com.aiosman.ravenow.ui.navigateToChat
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MineAgent() {
|
fun MineAgent() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
val model = MineAgentViewModel
|
val model = MineAgentViewModel
|
||||||
var agentList = model.agentList
|
var agentList = model.agentList
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -108,7 +113,11 @@ fun MineAgent() {
|
|||||||
key = { idx -> idx }
|
key = { idx -> idx }
|
||||||
) { idx ->
|
) { idx ->
|
||||||
val agentItem = agentList[idx]
|
val agentItem = agentList[idx]
|
||||||
AgentCard(agentEntity = agentItem)
|
AgentCard(agentEntity = agentItem,
|
||||||
|
onClick = {
|
||||||
|
//model.createSingleChat(agentItem.openId)
|
||||||
|
model.goToChatAi(agentItem.openId,navController)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多指示器
|
// 加载更多指示器
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ object MineAgentViewModel : ViewModel() {
|
|||||||
return body.toString()
|
return body.toString()
|
||||||
|
|
||||||
}
|
}
|
||||||
fun goToChat(
|
fun goToChatAi(
|
||||||
conversation: Conversation,
|
openId: String,
|
||||||
navController: NavHostController
|
navController: NavHostController
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val profile = userService.getUserProfileByTrtcUserId(conversation.trtcUserId)
|
val profile = userService.getUserProfileByOpenId(openId)
|
||||||
navController.navigateToChat(profile.id.toString())
|
navController.navigateToChat(profile.id.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,10 +279,11 @@ fun NotificationsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
,
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (AppState.enableChat){
|
if (AppState.enableChat){
|
||||||
@@ -303,7 +304,7 @@ fun NotificationsScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
MessageListViewModel.isLoading,
|
MessageListViewModel.isLoading,
|
||||||
|
|||||||
@@ -63,38 +63,40 @@ fun TimelineMomentsList() {
|
|||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
moments.size,
|
moments.size,
|
||||||
key = { idx -> moments[idx].id }
|
key = { idx -> moments.getOrNull(idx)?.id ?: idx }
|
||||||
) { idx ->
|
) { idx ->
|
||||||
val momentItem = moments[idx]
|
moments.getOrNull(idx)?.let { momentItem ->
|
||||||
MomentCard(momentEntity = momentItem,
|
MomentCard(
|
||||||
onAddComment = {
|
momentEntity = momentItem,
|
||||||
scope.launch {
|
onAddComment = {
|
||||||
model.onAddComment(momentItem.id)
|
scope.launch {
|
||||||
}
|
model.onAddComment(momentItem.id)
|
||||||
},
|
|
||||||
onLikeClick = {
|
|
||||||
scope.launch {
|
|
||||||
if (momentItem.liked) {
|
|
||||||
model.dislikeMoment(momentItem.id)
|
|
||||||
} else {
|
|
||||||
model.likeMoment(momentItem.id)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
onLikeClick = {
|
||||||
onFavoriteClick = {
|
scope.launch {
|
||||||
scope.launch {
|
if (momentItem.liked) {
|
||||||
if (momentItem.isFavorite) {
|
model.dislikeMoment(momentItem.id)
|
||||||
model.unfavoriteMoment(momentItem.id)
|
} else {
|
||||||
} else {
|
model.likeMoment(momentItem.id)
|
||||||
model.favoriteMoment(momentItem.id)
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
onFavoriteClick = {
|
||||||
onFollowClick = {
|
scope.launch {
|
||||||
model.followAction(momentItem)
|
if (momentItem.isFavorite) {
|
||||||
},
|
model.unfavoriteMoment(momentItem.id)
|
||||||
showFollowButton = false
|
} else {
|
||||||
)
|
model.favoriteMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFollowClick = {
|
||||||
|
model.followAction(momentItem)
|
||||||
|
},
|
||||||
|
showFollowButton = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ fun ProfileV3(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(bannerHeight.dp - 24.dp)
|
.height(bannerHeight.dp - 24.dp)
|
||||||
.let {
|
.let {
|
||||||
if (isSelf) {
|
if (isSelf&&isMain) {
|
||||||
it.noRippleClickable {
|
it.noRippleClickable {
|
||||||
Intent(Intent.ACTION_PICK).apply {
|
Intent(Intent.ACTION_PICK).apply {
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
@@ -383,8 +383,11 @@ fun ProfileV3(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
horizontal = 16.dp,
|
horizontal = 16.dp,
|
||||||
vertical = 8.dp
|
vertical = 8.dp,
|
||||||
),
|
).noRippleClickable {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (!isMain) {
|
if (!isMain) {
|
||||||
|
|||||||
@@ -1391,12 +1391,12 @@ fun PostMenuModal(
|
|||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
.padding(vertical = 47.dp, horizontal = 20.dp)
|
.padding(vertical = 47.dp, horizontal = 20.dp)
|
||||||
) {
|
) {
|
||||||
|
if(AppState.UserId == userId){
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(60.dp),
|
.size(60.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if(AppState.UserId == userId){
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
@@ -1435,7 +1435,6 @@ fun PostMenuModal(
|
|||||||
.size(60.dp),
|
.size(60.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if(AppState.UserId == userId){
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
@@ -1467,7 +1466,7 @@ fun PostMenuModal(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,30 @@
|
|||||||
<string name="empty_my_post_content">发布一个动态吧</string>
|
<string name="empty_my_post_content">发布一个动态吧</string>
|
||||||
<string name="edit_profile">编辑个人资料</string>
|
<string name="edit_profile">编辑个人资料</string>
|
||||||
<string name="logout">登出</string>
|
<string name="logout">登出</string>
|
||||||
<string name="change_password">变更密码</string>
|
<string name="current_password_tip1">请输入当前密码</string>
|
||||||
|
<string name="current_password_tip2">密码长度必须至少为 8 个字符</string>
|
||||||
|
<string name="current_password_tip3">密码必须至少包含一位数字</string>
|
||||||
|
<string name="current_password_tip4">密码必须至少包含一个大写字母</string>
|
||||||
|
<string name="change_password_tip1">密码不匹配</string>
|
||||||
|
<string name="current_password">当前密码</string>
|
||||||
|
<string name="current_password_tip5">请输入当前密码</string>
|
||||||
|
<string name="new_password">新密码</string>
|
||||||
|
<string name="new_password_tip1">请输入新密码</string>
|
||||||
|
<string name="confirm_new_password">确认新密码</string>
|
||||||
|
<string name="confirm_new_password_tip1">请确认新密码</string>
|
||||||
|
|
||||||
|
<string name="change_password">修改密码</string>
|
||||||
|
<string name="please_enter_current_password">请输入当前密码</string>
|
||||||
|
<string name="password_length_tip">密码长度至少8位</string>
|
||||||
|
<string name="password_digit_tip">密码必须包含至少一个数字</string>
|
||||||
|
<string name="password_uppercase_tip">密码必须包含至少一个大写字母</string>
|
||||||
|
<string name="password_lowercase_tip">密码必须包含至少一个小写字母</string>
|
||||||
|
<string name="password_not_match">两次输入的密码不一致</string>
|
||||||
|
<string name="enter_current_password">请输入当前密码</string>
|
||||||
|
<string name="enter_new_password">请输入新密码</string>
|
||||||
|
<string name="enter_new_password_again">请再次输入新密码</string>
|
||||||
|
<string name="confirm_change">确认修改</string>
|
||||||
|
|
||||||
<string name="cancel">取消</string>
|
<string name="cancel">取消</string>
|
||||||
<string name="bio">个性签名</string>
|
<string name="bio">个性签名</string>
|
||||||
<string name="nickname">昵称</string>
|
<string name="nickname">昵称</string>
|
||||||
|
|||||||
@@ -50,6 +50,17 @@
|
|||||||
<string name="edit_profile">Edit profile</string>
|
<string name="edit_profile">Edit profile</string>
|
||||||
<string name="logout">Logout</string>
|
<string name="logout">Logout</string>
|
||||||
<string name="change_password">Change password</string>
|
<string name="change_password">Change password</string>
|
||||||
|
<string name="current_password_tip1">Please enter your current password</string>
|
||||||
|
<string name="current_password_tip2">Password must be at least 8 characters long</string>
|
||||||
|
<string name="current_password_tip3">Password must contain at least one digit</string>
|
||||||
|
<string name="current_password_tip4">Password must contain at least one uppercase letter</string>
|
||||||
|
<string name="change_password_tip1">Passwords do not match</string>
|
||||||
|
<string name="current_password">Current password</string>
|
||||||
|
<string name="current_password_tip5">Enter your current password</string>
|
||||||
|
<string name="new_password">New password</string>
|
||||||
|
<string name="new_password_tip1">Enter your new password</string>
|
||||||
|
<string name="confirm_new_password">Confirm new password</string>
|
||||||
|
<string name="confirm_new_password_tip1">Confirm new password</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="bio">Signature</string>
|
<string name="bio">Signature</string>
|
||||||
<string name="nickname">Name</string>
|
<string name="nickname">Name</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user