更新聊天

This commit is contained in:
2024-09-27 21:11:48 +08:00
parent 55753d06cb
commit 24c0f57926
7 changed files with 326 additions and 26 deletions

View File

@@ -22,7 +22,11 @@ import com.tencent.imsdk.v2.V2TIMCallback
import com.tencent.imsdk.v2.V2TIMLogListener import com.tencent.imsdk.v2.V2TIMLogListener
import com.tencent.imsdk.v2.V2TIMManager import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMSDKConfig import com.tencent.imsdk.v2.V2TIMSDKConfig
import com.tencent.imsdk.v2.V2TIMUserFullInfo
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.suspendCoroutine
object AppState { object AppState {
var UserId: Int? = null var UserId: Int? = null
@@ -60,24 +64,46 @@ object AppState {
V2TIMManager.getInstance().initSDK(context, appConfig.trtcAppId, config) V2TIMManager.getInstance().initSDK(context, appConfig.trtcAppId, config)
try { try {
val sign = accountService.getMyTrtcSign() val sign = accountService.getMyTrtcSign()
V2TIMManager.getInstance().login( loginToTrtc(sign.userId, sign.sig)
sign.userId, updateTrtcUserProfile()
sign.sig,
object : V2TIMCallback {
override fun onError(code: Int, desc: String?) {
Log.e("V2TIMManager", "login failed: $code, $desc")
}
override fun onSuccess() {
Log.d("V2TIMManager", "login success")
}
}
)
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
suspend fun loginToTrtc(userId: String, userSig: String): Boolean {
return suspendCoroutine { continuation ->
V2TIMManager.getInstance().login(userId, userSig, object : V2TIMCallback {
override fun onError(code: Int, desc: String?) {
continuation.resumeWith(Result.failure(Exception("Login failed: $code, $desc")))
}
override fun onSuccess() {
continuation.resumeWith(Result.success(true))
}
})
}
}
suspend fun updateTrtcUserProfile() {
val accountService: AccountService = AccountServiceImpl()
val profile = accountService.getMyAccountProfile()
val info = V2TIMUserFullInfo()
info.setNickname(profile.nickName)
info.faceUrl = profile.avatar
info.selfSignature = profile.bio
return suspendCoroutine { continuation ->
V2TIMManager.getInstance().setSelfInfo(info, object : V2TIMCallback {
override fun onError(code: Int, desc: String?) {
continuation.resumeWith(Result.failure(Exception("Update user profile failed: $code, $desc")))
}
override fun onSuccess() {
continuation.resumeWith(Result.success(Unit))
}
})
}
}
fun ReloadAppState() { fun ReloadAppState() {
// 重置动态列表页面 // 重置动态列表页面

View File

@@ -48,6 +48,8 @@ interface UserService {
followingId: Int? = null followingId: Int? = null
): ListContainer<AccountProfileEntity> ): ListContainer<AccountProfileEntity>
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
} }
class UserServiceImpl : UserService { class UserServiceImpl : UserService {
@@ -89,4 +91,10 @@ class UserServiceImpl : UserService {
pageSize = body.pageSize, pageSize = body.pageSize,
) )
} }
override suspend fun getUserProfileByTrtcUserId(id: String): AccountProfileEntity {
val resp = ApiClient.api.getAccountProfileByTrtcUserId(id)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
}
} }

View File

@@ -265,6 +265,11 @@ interface RiderProAPI {
@Path("id") id: Int @Path("id") id: Int
): Response<DataContainer<AccountProfile>> ): Response<DataContainer<AccountProfile>>
@GET("profile/trtc/{id}")
suspend fun getAccountProfileByTrtcUserId(
@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

View File

@@ -1,6 +1,5 @@
package com.aiosman.riderpro.ui.chat package com.aiosman.riderpro.ui.chat
import android.view.KeyEvent
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@@ -46,8 +45,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -56,19 +55,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.exp.formatChatTime
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -90,6 +87,7 @@ fun ChatScreen(userId: String) {
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
viewModel.UnRegistListener() viewModel.UnRegistListener()
viewModel.clearUnRead()
} }
} }
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -191,6 +189,7 @@ fun ChatScreen(userId: String) {
@Composable @Composable
fun ChatSelfItem(item: ChatItem) { fun ChatSelfItem(item: ChatItem) {
val context = LocalContext.current
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -204,8 +203,10 @@ fun ChatSelfItem(item: ChatItem) {
horizontalAlignment = androidx.compose.ui.Alignment.End, horizontalAlignment = androidx.compose.ui.Alignment.End,
) { ) {
Row() { Row() {
val calendar = java.util.Calendar.getInstance()
calendar.timeInMillis = item.timestamp
Text( Text(
text = item.time, text = calendar.time.formatChatTime(context),
style = TextStyle( style = TextStyle(
color = Color.Gray, color = Color.Gray,
fontSize = 14.sp fontSize = 14.sp
@@ -344,7 +345,10 @@ fun ChatInput(
var text by remember { mutableStateOf("") } var text by remember { mutableStateOf("") }
val inputBarHeight by animateDpAsState( val inputBarHeight by animateDpAsState(
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp), targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
animationSpec = tween(durationMillis = 300), label = "" animationSpec = tween(
durationMillis = 300,
easing = androidx.compose.animation.core.LinearEasing
), label = ""
) )
// 在 isKeyboardOpen 变化时立即更新 inputBarHeight 的动画目标值 // 在 isKeyboardOpen 变化时立即更新 inputBarHeight 的动画目标值

View File

@@ -15,18 +15,22 @@ import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.AccountProfileEntity import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.exp.formatChatTime import com.aiosman.riderpro.exp.formatChatTime
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
import com.tencent.imsdk.v2.V2TIMCallback
import com.tencent.imsdk.v2.V2TIMManager import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMMessage import com.tencent.imsdk.v2.V2TIMMessage
import com.tencent.imsdk.v2.V2TIMSendCallback import com.tencent.imsdk.v2.V2TIMSendCallback
import com.tencent.imsdk.v2.V2TIMValueCallback import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class ChatItem( data class ChatItem(
val message: String, val message: String,
val avatar: String, val avatar: String,
val time: String, val time: String,
val userId: String, val userId: String,
val nickname: String val nickname: String,
val timeCategory: String = "",
val timestamp: Long = 0
) )
class ChatViewModel( class ChatViewModel(
@@ -66,8 +70,19 @@ class ChatViewModel(
fun UnRegistListener() { fun UnRegistListener() {
V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener); 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 convertToChatItem(message: V2TIMMessage, context: Context): ChatItem { fun convertToChatItem(message: V2TIMMessage, context: Context): ChatItem {
val avatar = if (message.sender == userProfile?.trtcUserId) { val avatar = if (message.sender == userProfile?.trtcUserId) {
userProfile?.avatar ?: "" userProfile?.avatar ?: ""
@@ -88,7 +103,8 @@ class ChatViewModel(
avatar = avatar, avatar = avatar,
time = calendar.time.formatChatTime(context), time = calendar.time.formatChatTime(context),
userId = message.sender, userId = message.sender,
nickname = nickname nickname = nickname,
timestamp = timestamp * 1000
) )
} }
@@ -166,8 +182,8 @@ class ChatViewModel(
} }
) )
} }
fun getDisplayChatList(): List<ChatItem> { fun getDisplayChatList(): List<ChatItem> {
return chatData return chatData
} }
} }

View File

@@ -0,0 +1,241 @@
package com.aiosman.riderpro.ui.comment.notice
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
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 androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.exp.timeAgo
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.navigateToPost
import kotlinx.coroutines.launch
@Composable
fun CommentNoticeScreen() {
val viewModel = viewModel<CommentNoticeListViewModel>(
key = "CommentNotice",
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return CommentNoticeListViewModel() as T
}
}
)
val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.initData(context)
}
var dataFlow = viewModel.commentItemsFlow
var comments = dataFlow.collectAsLazyPagingItems()
val navController = LocalNavController.current
Column(
modifier = Modifier.fillMaxSize()
) {
StatusBarSpacer()
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
NoticeScreenHeader("Comment", moreIcon = false)
}
LazyColumn(
modifier = Modifier
.fillMaxSize().padding(horizontal = 16.dp)
) {
items(comments.itemCount) { index ->
comments[index]?.let { comment ->
CommentNoticeItem(comment) {
viewModel.updateReadStatus(comment.id)
viewModel.viewModelScope.launch {
var highlightCommentId = comment.id
comment.parentCommentId?.let {
highlightCommentId = it
}
navController.navigateToPost(
id = comment.post!!.id,
highlightCommentId = highlightCommentId,
initImagePagerIndex = 0
)
}
}
}
}
// handle load error
when {
comments.loadState.append is LoadState.Loading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
LinearProgressIndicator(
modifier = Modifier.width(160.dp),
color = Color(0xFFDA3832)
)
}
}
}
comments.loadState.append is LoadState.Error -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.noRippleClickable {
comments.retry()
},
contentAlignment = Alignment.Center
) {
Text(
text = "Load comment error, click to retry",
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(72.dp))
}
}
}
}
@Composable
fun CommentNoticeItem(
commentItem: CommentEntity,
onPostClick: () -> Unit = {},
) {
val navController = LocalNavController.current
val context = LocalContext.current
Row(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
) {
Box {
CustomAsyncImage(
context = context,
imageUrl = commentItem.avatar,
contentDescription = commentItem.name,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
commentItem.author.toString()
)
)
}
)
}
Row(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.noRippleClickable {
onPostClick()
}
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = commentItem.name,
fontSize = 18.sp,
modifier = Modifier
)
Spacer(modifier = Modifier.height(4.dp))
Row {
var text = commentItem.comment
if (commentItem.parentCommentId != null) {
text = "Reply you: $text"
}
Text(
text = text,
fontSize = 14.sp,
maxLines = 1,
color = Color(0x99000000),
modifier = Modifier.weight(1f),
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = commentItem.date.timeAgo(context),
fontSize = 14.sp,
color = Color(0x66000000)
)
}
}
Spacer(modifier = Modifier.width(24.dp))
commentItem.post?.let {
Box {
Box(
modifier = Modifier.padding(4.dp)
) {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Image",
modifier = Modifier
.size(48.dp)
)
// unread indicator
}
if (commentItem.unread) {
Box(
modifier = Modifier
.background(Color(0xFFE53935), CircleShape)
.size(12.dp)
.align(Alignment.TopEnd)
)
}
}
}
}
}
}

View File

@@ -35,8 +35,8 @@ import com.aiosman.riderpro.R
import com.aiosman.riderpro.exp.viewModelFactory import com.aiosman.riderpro.exp.viewModelFactory
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.index.tabs.profile.MomentPostUnit import com.aiosman.riderpro.ui.index.tabs.profile.v2.MomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.UserInformation import com.aiosman.riderpro.ui.index.tabs.profile.v2.UserInformation
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch