From 24c0f57926e6708e117b8929f9334177c7e4f2ed Mon Sep 17 00:00:00 2001 From: AllenTom Date: Fri, 27 Sep 2024 21:11:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=81=8A=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/aiosman/riderpro/AppState.kt | 52 +++- .../com/aiosman/riderpro/data/UserService.kt | 8 + .../aiosman/riderpro/data/api/RiderProAPI.kt | 5 + .../aiosman/riderpro/ui/chat/ChatScreen.kt | 18 +- .../aiosman/riderpro/ui/chat/ChatViewModel.kt | 24 +- .../ui/comment/notice/CommentNotice.kt | 241 ++++++++++++++++++ .../riderpro/ui/profile/AccountProfile.kt | 4 +- 7 files changed, 326 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/aiosman/riderpro/ui/comment/notice/CommentNotice.kt diff --git a/app/src/main/java/com/aiosman/riderpro/AppState.kt b/app/src/main/java/com/aiosman/riderpro/AppState.kt index 9b634b1..a21bac8 100644 --- a/app/src/main/java/com/aiosman/riderpro/AppState.kt +++ b/app/src/main/java/com/aiosman/riderpro/AppState.kt @@ -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() { // 重置动态列表页面 diff --git a/app/src/main/java/com/aiosman/riderpro/data/UserService.kt b/app/src/main/java/com/aiosman/riderpro/data/UserService.kt index 96313c4..fa8fd78 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/UserService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/UserService.kt @@ -48,6 +48,8 @@ interface UserService { followingId: Int? = null ): ListContainer + 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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt index 543d96c..f2d1209 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt @@ -265,6 +265,11 @@ interface RiderProAPI { @Path("id") id: Int ): Response> + @GET("profile/trtc/{id}") + suspend fun getAccountProfileByTrtcUserId( + @Path("id") id: String + ): Response> + @POST("user/{id}/follow") suspend fun followUser( @Path("id") id: Int diff --git a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt index eda8a4e..0ff1c21 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt @@ -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 的动画目标值 diff --git a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt index d309814..11d877c 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt @@ -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 { return chatData } + } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/comment/notice/CommentNotice.kt b/app/src/main/java/com/aiosman/riderpro/ui/comment/notice/CommentNotice.kt new file mode 100644 index 0000000..66d8115 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/comment/notice/CommentNotice.kt @@ -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( + key = "CommentNotice", + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): 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) + ) + } + } + + + } + } + + + } +} diff --git a/app/src/main/java/com/aiosman/riderpro/ui/profile/AccountProfile.kt b/app/src/main/java/com/aiosman/riderpro/ui/profile/AccountProfile.kt index 2fb7242..30995ac 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/profile/AccountProfile.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/profile/AccountProfile.kt @@ -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