智能体会话开发
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.aiosman.ravenow.data
|
||||
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import com.aiosman.ravenow.data.api.AppConfig
|
||||
import com.aiosman.ravenow.data.api.CaptchaInfo
|
||||
@@ -53,6 +54,8 @@ data class AccountProfile(
|
||||
val banner: String?,
|
||||
// trtcUserId
|
||||
val trtcUserId: String,
|
||||
// aiAccount true:ai false:普通用户
|
||||
val aiAccount: Boolean
|
||||
) {
|
||||
/**
|
||||
* 转换为Entity
|
||||
@@ -63,7 +66,13 @@ data class AccountProfile(
|
||||
followerCount = followerCount,
|
||||
followingCount = followingCount,
|
||||
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,
|
||||
country = "Worldwide",
|
||||
isFollowing = isFollowing,
|
||||
@@ -74,6 +83,7 @@ data class AccountProfile(
|
||||
null
|
||||
},
|
||||
trtcUserId = trtcUserId,
|
||||
aiAccount = aiAccount,
|
||||
rawAvatar = avatar
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,8 +48,23 @@ interface UserService {
|
||||
followingId: Int? = null
|
||||
): ListContainer<AccountProfileEntity>
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param id 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
|
||||
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param id 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
|
||||
suspend fun getUserProfileByOpenId(id: String):AccountProfileEntity
|
||||
|
||||
}
|
||||
|
||||
class UserServiceImpl : UserService {
|
||||
@@ -97,4 +112,10 @@ class UserServiceImpl : UserService {
|
||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||
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
|
||||
): Response<DataContainer<AccountProfile>>
|
||||
|
||||
|
||||
@GET("profile/aichat/profile/{id}")
|
||||
suspend fun getAccountProfileByOpenId(
|
||||
@Path("id") id: String
|
||||
): Response<DataContainer<AccountProfile>>
|
||||
|
||||
@POST("user/{id}/follow")
|
||||
suspend fun followUser(
|
||||
@Path("id") id: Int
|
||||
|
||||
@@ -61,6 +61,8 @@ data class AccountProfileEntity(
|
||||
val banner: String?,
|
||||
// trtcUserId
|
||||
val trtcUserId: String,
|
||||
|
||||
val aiAccount: Boolean,
|
||||
val rawAvatar: String
|
||||
)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ data class AgentEntity(
|
||||
//val profile: ProfileEntity,
|
||||
val title: String,
|
||||
val updatedAt: String,
|
||||
val useCount: Int
|
||||
val useCount: Int,
|
||||
)
|
||||
|
||||
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.ResetPasswordScreen
|
||||
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.comment.CommentsScreen
|
||||
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
||||
@@ -91,6 +92,7 @@ sealed class NavigationRoute(
|
||||
data object ResetPassword : NavigationRoute("ResetPassword")
|
||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||
data object Chat : NavigationRoute("Chat/{id}")
|
||||
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
||||
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
||||
data object ImageCrop : NavigationRoute("ImageCrop")
|
||||
data object AccountSetting : NavigationRoute("AccountSetting")
|
||||
@@ -369,6 +371,19 @@ fun NavigationController(
|
||||
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) {
|
||||
CompositionLocalProvider(
|
||||
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(
|
||||
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.exp.timeAgo
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
|
||||
@Composable
|
||||
fun AgentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
agentEntity: AgentEntity,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
@@ -50,6 +52,9 @@ fun AgentCard(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 8.dp)
|
||||
.noRippleClickable {
|
||||
onClick ()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -27,14 +27,19 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.composables.AgentCard
|
||||
import com.aiosman.ravenow.ui.navigateToChat
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun MineAgent() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val model = MineAgentViewModel
|
||||
var agentList = model.agentList
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -108,7 +113,11 @@ fun MineAgent() {
|
||||
key = { idx -> idx }
|
||||
) { 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()
|
||||
|
||||
}
|
||||
fun goToChat(
|
||||
conversation: Conversation,
|
||||
fun goToChatAi(
|
||||
openId: String,
|
||||
navController: NavHostController
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val profile = userService.getUserProfileByTrtcUserId(conversation.trtcUserId)
|
||||
val profile = userService.getUserProfileByOpenId(openId)
|
||||
navController.navigateToChat(profile.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,10 +279,11 @@ fun NotificationsScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
/*Box(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (AppState.enableChat){
|
||||
@@ -303,7 +304,7 @@ fun NotificationsScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
}*/
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
MessageListViewModel.isLoading,
|
||||
|
||||
@@ -63,38 +63,40 @@ fun TimelineMomentsList() {
|
||||
) {
|
||||
items(
|
||||
moments.size,
|
||||
key = { idx -> moments[idx].id }
|
||||
key = { idx -> moments.getOrNull(idx)?.id ?: idx }
|
||||
) { idx ->
|
||||
val momentItem = moments[idx]
|
||||
MomentCard(momentEntity = momentItem,
|
||||
onAddComment = {
|
||||
scope.launch {
|
||||
model.onAddComment(momentItem.id)
|
||||
}
|
||||
},
|
||||
onLikeClick = {
|
||||
scope.launch {
|
||||
if (momentItem.liked) {
|
||||
model.dislikeMoment(momentItem.id)
|
||||
} else {
|
||||
model.likeMoment(momentItem.id)
|
||||
moments.getOrNull(idx)?.let { momentItem ->
|
||||
MomentCard(
|
||||
momentEntity = momentItem,
|
||||
onAddComment = {
|
||||
scope.launch {
|
||||
model.onAddComment(momentItem.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteClick = {
|
||||
scope.launch {
|
||||
if (momentItem.isFavorite) {
|
||||
model.unfavoriteMoment(momentItem.id)
|
||||
} else {
|
||||
model.favoriteMoment(momentItem.id)
|
||||
},
|
||||
onLikeClick = {
|
||||
scope.launch {
|
||||
if (momentItem.liked) {
|
||||
model.dislikeMoment(momentItem.id)
|
||||
} else {
|
||||
model.likeMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFollowClick = {
|
||||
model.followAction(momentItem)
|
||||
},
|
||||
showFollowButton = false
|
||||
)
|
||||
},
|
||||
onFavoriteClick = {
|
||||
scope.launch {
|
||||
if (momentItem.isFavorite) {
|
||||
model.unfavoriteMoment(momentItem.id)
|
||||
} else {
|
||||
model.favoriteMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFollowClick = {
|
||||
model.followAction(momentItem)
|
||||
},
|
||||
showFollowButton = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
|
||||
@@ -233,7 +233,7 @@ fun ProfileV3(
|
||||
.fillMaxWidth()
|
||||
.height(bannerHeight.dp - 24.dp)
|
||||
.let {
|
||||
if (isSelf) {
|
||||
if (isSelf&&isMain) {
|
||||
it.noRippleClickable {
|
||||
Intent(Intent.ACTION_PICK).apply {
|
||||
type = "image/*"
|
||||
@@ -383,8 +383,11 @@ fun ProfileV3(
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp
|
||||
),
|
||||
vertical = 8.dp,
|
||||
).noRippleClickable {
|
||||
|
||||
},
|
||||
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (!isMain) {
|
||||
|
||||
@@ -1391,12 +1391,12 @@ fun PostMenuModal(
|
||||
.background(AppColors.background)
|
||||
.padding(vertical = 47.dp, horizontal = 20.dp)
|
||||
) {
|
||||
if(AppState.UserId == userId){
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.size(60.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if(AppState.UserId == userId){
|
||||
Column(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
@@ -1435,7 +1435,6 @@ fun PostMenuModal(
|
||||
.size(60.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if(AppState.UserId == userId){
|
||||
Column(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
@@ -1467,7 +1466,7 @@ fun PostMenuModal(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user