更新聊天

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.V2TIMManager
import com.tencent.imsdk.v2.V2TIMSDKConfig
import com.tencent.imsdk.v2.V2TIMUserFullInfo
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.suspendCoroutine
object AppState {
var UserId: Int? = null
@@ -60,24 +64,46 @@ object AppState {
V2TIMManager.getInstance().initSDK(context, appConfig.trtcAppId, config)
try {
val sign = accountService.getMyTrtcSign()
V2TIMManager.getInstance().login(
sign.userId,
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")
}
}
)
loginToTrtc(sign.userId, sign.sig)
updateTrtcUserProfile()
} 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() {
// 重置动态列表页面

View File

@@ -48,6 +48,8 @@ interface UserService {
followingId: Int? = null
): ListContainer<AccountProfileEntity>
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
}
class UserServiceImpl : UserService {
@@ -89,4 +91,10 @@ class UserServiceImpl : UserService {
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
): Response<DataContainer<AccountProfile>>
@GET("profile/trtc/{id}")
suspend fun getAccountProfileByTrtcUserId(
@Path("id") id: String
): Response<DataContainer<AccountProfile>>
@POST("user/{id}/follow")
suspend fun followUser(
@Path("id") id: Int

View File

@@ -1,6 +1,5 @@
package com.aiosman.riderpro.ui.chat
import android.view.KeyEvent
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateDpAsState
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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onKeyEvent
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
@@ -56,19 +55,17 @@ 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.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.exp.formatChatTime
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -90,6 +87,7 @@ fun ChatScreen(userId: String) {
DisposableEffect(Unit) {
onDispose {
viewModel.UnRegistListener()
viewModel.clearUnRead()
}
}
val listState = rememberLazyListState()
@@ -191,6 +189,7 @@ fun ChatScreen(userId: String) {
@Composable
fun ChatSelfItem(item: ChatItem) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxWidth()
@@ -204,8 +203,10 @@ fun ChatSelfItem(item: ChatItem) {
horizontalAlignment = androidx.compose.ui.Alignment.End,
) {
Row() {
val calendar = java.util.Calendar.getInstance()
calendar.timeInMillis = item.timestamp
Text(
text = item.time,
text = calendar.time.formatChatTime(context),
style = TextStyle(
color = Color.Gray,
fontSize = 14.sp
@@ -344,7 +345,10 @@ fun ChatInput(
var text by remember { mutableStateOf("") }
val inputBarHeight by animateDpAsState(
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 的动画目标值

View File

@@ -15,18 +15,22 @@ import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.exp.formatChatTime
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
data class ChatItem(
val message: String,
val avatar: String,
val time: String,
val userId: String,
val nickname: String
val nickname: String,
val timeCategory: String = "",
val timestamp: Long = 0
)
class ChatViewModel(
@@ -66,8 +70,19 @@ class ChatViewModel(
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 convertToChatItem(message: V2TIMMessage, context: Context): ChatItem {
val avatar = if (message.sender == userProfile?.trtcUserId) {
userProfile?.avatar ?: ""
@@ -88,7 +103,8 @@ class ChatViewModel(
avatar = avatar,
time = calendar.time.formatChatTime(context),
userId = message.sender,
nickname = nickname
nickname = nickname,
timestamp = timestamp * 1000
)
}
@@ -166,8 +182,8 @@ class ChatViewModel(
}
)
}
fun getDisplayChatList(): List<ChatItem> {
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.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.index.tabs.profile.MomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.UserInformation
import com.aiosman.riderpro.ui.index.tabs.profile.v2.MomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.v2.UserInformation
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch