From 7c0d18357192d7b4a0e8f62e3ebd70218b22adbe Mon Sep 17 00:00:00 2001 From: AllenTom Date: Sun, 8 Sep 2024 21:46:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=84=E8=AE=BA=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 允许用户回复评论和子评论 - 点击回复按钮,弹出评论框,并显示回复的用户 - 评论 列表中显示回复的用户和内容 - 点击回复内容中的用户名,跳转到用户主页 - 优化评论列表加载逻辑,支持加载更多子评论 --- .../java/com/aiosman/riderpro/MainActivity.kt | 8 +- .../aiosman/riderpro/data/CommentService.kt | 63 +++- .../aiosman/riderpro/data/api/RiderProAPI.kt | 9 +- .../com/aiosman/riderpro/entity/Comment.kt | 15 +- .../main/java/com/aiosman/riderpro/ui/Navi.kt | 13 - .../com/aiosman/riderpro/ui/account/edit.kt | 2 +- .../riderpro/ui/comment/CommentModal.kt | 171 +++------ .../ui/composables/EditCommentBottomModal.kt | 82 ++++- .../aiosman/riderpro/ui/composables/Image.kt | 3 + .../ui/index/tabs/message/MessageList.kt | 4 +- .../riderpro/ui/index/tabs/moment/Moment.kt | 13 +- .../riderpro/ui/index/tabs/profile/Profile.kt | 2 +- .../ui/index/tabs/search/DiscoverScreen.kt | 2 +- .../riderpro/ui/post/CommentsViewModel.kt | 163 +++++++++ .../java/com/aiosman/riderpro/ui/post/Post.kt | 343 ++++++++++++++---- .../aiosman/riderpro/ui/post/PostViewModel.kt | 131 ++----- 16 files changed, 677 insertions(+), 347 deletions(-) create mode 100644 app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt diff --git a/app/src/main/java/com/aiosman/riderpro/MainActivity.kt b/app/src/main/java/com/aiosman/riderpro/MainActivity.kt index 6cd0209..b982779 100644 --- a/app/src/main/java/com/aiosman/riderpro/MainActivity.kt +++ b/app/src/main/java/com/aiosman/riderpro/MainActivity.kt @@ -38,6 +38,7 @@ class MainActivity : ComponentActivity() { // Firebase Analytics private lateinit var analytics: FirebaseAnalytics private val scope = CoroutineScope(Dispatchers.Main) + // 请求通知权限 private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -100,12 +101,7 @@ class MainActivity : ComponentActivity() { val postId = intent.getStringExtra("POST_ID") if (postId != null) { Log.d("MainActivity", "Navigation to Post$postId") - PostViewModel.postId = postId - PostViewModel.viewModelScope.launch { - PostViewModel.initData() - navController.navigate(NavigationRoute.Post.route.replace("{id}", postId)) - } - + navController.navigate(NavigationRoute.Post.route.replace("{id}", postId)) } } } diff --git a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt index 8adceb8..70b817b 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt @@ -15,6 +15,9 @@ interface CommentService { * @param postId 动态ID,过滤条件 * @param postUser 动态作者ID,获取某个用户所有动态下的评论 * @param selfNotice 是否是自己的通知 + * @param order 排序 + * @param parentCommentId 父评论ID + * @param pageSize 每页数量 * @return 评论列表 */ suspend fun getComments( @@ -22,15 +25,24 @@ interface CommentService { postId: Int? = null, postUser: Int? = null, selfNotice: Boolean? = null, - order: String? = null + order: String? = null, + parentCommentId: Int? = null, + pageSize: Int? = null ): ListContainer /** * 创建评论 * @param postId 动态ID * @param content 评论内容 + * @param parentCommentId 父评论ID + * @param replyUserId 回复用户ID */ - suspend fun createComment(postId: Int, content: String) + suspend fun createComment( + postId: Int, + content: String, + parentCommentId: Int? = null, + replyUserId: Int? = null + ): CommentEntity /** * 点赞评论 @@ -87,7 +99,15 @@ data class Comment( val post: NoticePost?, // 是否未读 @SerializedName("isUnread") - val isUnread: Boolean + val isUnread: Boolean, + @SerializedName("reply") + val reply: List, + @SerializedName("replyUser") + val replyUser: User?, + @SerializedName("parentCommentId") + val parentCommentId: Int?, + @SerializedName("replyCount") + val replyCount: Int ) { /** * 转换为Entity @@ -114,7 +134,13 @@ data class Comment( ) } ) - } + }, + reply = reply.map { it.toCommentEntity() }, + replyUserNickname = replyUser?.nickName, + replyUserId = replyUser?.id, + replyUserAvatar = replyUser?.avatar?.let { "${ApiClient.BASE_SERVER}$it" }, + parentCommentId = parentCommentId, + replyCount = replyCount ) } } @@ -127,14 +153,16 @@ class CommentRemoteDataSource( postId: Int?, postUser: Int?, selfNotice: Boolean?, - order: String? + order: String?, + parentCommentId: Int? ): ListContainer { return commentService.getComments( pageNumber, postId, postUser = postUser, selfNotice = selfNotice, - order = order + order = order, + parentCommentId = parentCommentId ) } } @@ -146,7 +174,9 @@ class CommentServiceImpl : CommentService { postId: Int?, postUser: Int?, selfNotice: Boolean?, - order: String? + order: String?, + parentCommentId: Int?, + pageSize: Int? ): ListContainer { val resp = ApiClient.api.getComments( page = pageNumber, @@ -155,7 +185,9 @@ class CommentServiceImpl : CommentService { order = order, selfNotice = selfNotice?.let { if (it) 1 else 0 - } + }, + parentCommentId = parentCommentId, + pageSize = pageSize ?: 20 ) val body = resp.body() ?: throw ServiceException("Failed to get comments") return ListContainer( @@ -166,9 +198,18 @@ class CommentServiceImpl : CommentService { ) } - override suspend fun createComment(postId: Int, content: String) { - val resp = ApiClient.api.createComment(postId, CommentRequestBody(content)) - return + override suspend fun createComment( + postId: Int, + content: String, + parentCommentId: Int?, + replyUserId: Int?, + ): CommentEntity { + val resp = ApiClient.api.createComment( + postId, + CommentRequestBody(content, parentCommentId, replyUserId), + ) + val body = resp.body() ?: throw ServiceException("Failed to create comment") + return body.data.toCommentEntity() } override suspend fun likeComment(commentId: Int) { 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 1b6e989..8764166 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 @@ -61,7 +61,11 @@ data class ValidateTokenResult( data class CommentRequestBody( @SerializedName("content") - val content: String + val content: String, + @SerializedName("parentCommentId") + val parentCommentId: Int? = null, + @SerializedName("replyUserId") + val replyUserId: Int? = null, ) data class ChangePasswordRequestBody( @@ -149,7 +153,7 @@ interface RiderProAPI { suspend fun createComment( @Path("id") id: Int, @Body body: CommentRequestBody - ): Response + ): Response> @POST("comment/{id}/like") suspend fun likeComment( @@ -175,6 +179,7 @@ interface RiderProAPI { @Query("postUser") postUser: Int? = null, @Query("selfNotice") selfNotice: Int? = 0, @Query("order") order: String? = null, + @Query("parentCommentId") parentCommentId: Int? = null, ): Response> @GET("account/my") diff --git a/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt b/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt index f5fed8d..73a737c 100644 --- a/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt +++ b/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt @@ -19,7 +19,14 @@ data class CommentEntity( val author: Long, var liked: Boolean, var unread: Boolean = false, - var post: NoticePost? + var post: NoticePost?, + var reply: List, + var replyUserId: Long?, + var replyUserNickname: String?, + var replyUserAvatar: String?, + var parentCommentId: Int?, + var replyCount: Int, + var replyPage: Int = 1 ) class CommentPagingSource( @@ -27,7 +34,8 @@ class CommentPagingSource( private val postId: Int? = null, private val postUser: Int? = null, private val selfNotice: Boolean? = null, - private val order: String? = null + private val order: String? = null, + private val parentCommentId: Int? = null ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { return try { @@ -37,7 +45,8 @@ class CommentPagingSource( postId = postId, postUser = postUser, selfNotice = selfNotice, - order = order + order = order, + parentCommentId = parentCommentId ) LoadResult.Page( data = comments.list, diff --git a/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt b/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt index 319bbe9..c73e941 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt @@ -132,12 +132,6 @@ fun NavigationController( composable( route = NavigationRoute.Post.route, arguments = listOf(navArgument("id") { type = NavType.StringType }), - enterTransition = { - fadeIn(animationSpec = tween(durationMillis = 50)) - }, - exitTransition = { - fadeOut(animationSpec = tween(durationMillis = 50)) - } ) { backStackEntry -> CompositionLocalProvider( LocalAnimatedContentScope provides this, @@ -165,13 +159,6 @@ fun NavigationController( } composable( route = NavigationRoute.NewPost.route, - enterTransition = { - - fadeIn(animationSpec = tween(durationMillis = 100)) - }, - exitTransition = { - fadeOut(animationSpec = tween(durationMillis = 100)) - } ) { NewPostScreen() } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt b/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt index fe98fc3..b6e489e 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt @@ -196,7 +196,7 @@ fun AccountEditScreen() { if (bannerImageUrl != null) { bannerImageUrl.toString() } else { - it.banner + it.banner!! }, contentDescription = null, modifier = Modifier diff --git a/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt b/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt index bc80d27..e2ebbbf 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt @@ -16,6 +16,7 @@ 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.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.ExperimentalMaterial3Api @@ -47,24 +48,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems import com.aiosman.riderpro.AppState import com.aiosman.riderpro.R -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.CommentService -import com.aiosman.riderpro.data.CommentServiceImpl import com.aiosman.riderpro.entity.CommentEntity -import com.aiosman.riderpro.entity.CommentPagingSource +import com.aiosman.riderpro.ui.composables.EditCommentBottomModal import com.aiosman.riderpro.ui.modifiers.noRippleClickable +import com.aiosman.riderpro.ui.post.CommentContent import com.aiosman.riderpro.ui.post.CommentMenuModal import com.aiosman.riderpro.ui.post.CommentsSection +import com.aiosman.riderpro.ui.post.CommentsViewModel import com.aiosman.riderpro.ui.post.OrderSelectionComponent -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -73,50 +67,13 @@ import kotlinx.coroutines.launch class CommentModalViewModel( val postId: Int? ) : ViewModel() { - val commentService: CommentService = CommentServiceImpl() var commentText by mutableStateOf("") - var order by mutableStateOf("like") - val commentsFlow = MutableStateFlow>(PagingData.empty()) - + var commentsViewModel: CommentsViewModel = CommentsViewModel(postId.toString()) init { - reloadComments() + commentsViewModel.preTransit() } - fun updateDeleteComment(commentId: Int) { - viewModelScope.launch { - commentService.DeleteComment(commentId) - reloadComments() - } - } - fun reloadComments() { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 20, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId, - order = order - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - commentsFlow.value = it - } - } - - } - - /** - * 创建评论 - */ - suspend fun createComment() { - postId?.let { - commentService.createComment(postId, commentText) - reloadComments() - commentText = "" - } - } } @@ -142,18 +99,19 @@ fun CommentModalContent( } } ) + val commentViewModel = model.commentsViewModel var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() LaunchedEffect(Unit) { - model.reloadComments() + } var showCommentMenu by remember { mutableStateOf(false) } var contextComment by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() - val comments = model.commentsFlow.collectAsLazyPagingItems() val insets = WindowInsets val imePadding = insets.ime.getBottom(density = LocalDensity.current) var bottomPadding by remember { mutableStateOf(0.dp) } var softwareKeyboardController = LocalSoftwareKeyboardController.current + var replyComment by remember { mutableStateOf(null) } + LaunchedEffect(imePadding) { bottomPadding = imePadding.dp } @@ -179,7 +137,7 @@ fun CommentModalContent( onDeleteClick = { showCommentMenu = false contextComment?.let { - model.updateDeleteComment(it.id) + commentViewModel.deleteComment(it.id) } } ) @@ -188,7 +146,9 @@ fun CommentModalContent( suspend fun sendComment() { if (model.commentText.isNotEmpty()) { softwareKeyboardController?.hide() - model.createComment() + commentViewModel.createComment( + model.commentText, + ) } onCommentAdded() } @@ -225,8 +185,8 @@ fun CommentModalContent( color = Color(0xff666666) ) OrderSelectionComponent { - model.order = it - model.reloadComments() + commentViewModel.order = it + commentViewModel.reloadComment() } } Box( @@ -235,83 +195,50 @@ fun CommentModalContent( .padding(horizontal = 16.dp) .weight(1f) ) { - CommentsSection( - lazyPagingItems = comments, - onLike = { commentEntity: CommentEntity -> - scope.launch { - if (commentEntity.liked) { - model.commentService.dislikeComment(commentEntity.id) - } else { - model.commentService.likeComment(commentEntity.id) - } - model.reloadComments() - } - }, - onLongClick = { commentEntity: CommentEntity -> - if (AppState.UserId?.toLong() == commentEntity.author) { - contextComment = commentEntity - showCommentMenu = true - } - }, - onWillCollapse = { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + item { + CommentContent( + viewModel = commentViewModel, + onLongClick = { commentEntity: CommentEntity -> - }, - ) + }, + onReply = { parentComment, _, _, _ -> + + }, + ) + Spacer(modifier = Modifier.height(72.dp)) + } + + } } Column( modifier = Modifier .fillMaxWidth() .background(Color(0xfff7f7f7)) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .height(64.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - verticalAlignment = Alignment.CenterVertically - ) { - // rounded - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(RoundedCornerShape(20.dp)) - .background(Color(0xffe5e5e5)) - .padding(horizontal = 16.dp, vertical = 12.dp) - - ) { - BasicTextField( - value = model.commentText, - onValueChange = { text -> model.commentText = text }, - modifier = Modifier - .fillMaxWidth(), - textStyle = TextStyle( - color = Color.Black, - fontWeight = FontWeight.Normal + EditCommentBottomModal(replyComment) { + commentViewModel.viewModelScope.launch { + if (replyComment != null) { + if (replyComment?.parentCommentId != null) { + // 第三级评论 + commentViewModel.createComment( + it, + parentCommentId = replyComment?.parentCommentId, + replyUserId = replyComment?.author?.toInt() ) - ) - + } else { + // 子级评论 + commentViewModel.createComment(it, replyComment?.id) + } + } else { + // 顶级评论 + commentViewModel.createComment(it) } - Spacer(modifier = Modifier.width(16.dp)) - Image( - painter = painterResource(id = R.drawable.rider_pro_send), - contentDescription = "Send", - modifier = Modifier - .size(32.dp) - .noRippleClickable { - scope.launch { - sendComment() - } - } - ) } - } Spacer(modifier = Modifier.height(navBarHeight)) diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt index b2798d6..f4a6bbc 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt @@ -8,14 +8,17 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -28,18 +31,27 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.aiosman.riderpro.R +import com.aiosman.riderpro.entity.CommentEntity import com.aiosman.riderpro.ui.modifiers.noRippleClickable @Composable -fun EditCommentBottomModal(onSend: (String) -> Unit = {}) { +fun EditCommentBottomModal( + replyComment: CommentEntity? = null, + onSend: (String) -> Unit = {} +) { var text by remember { mutableStateOf("") } var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val focusRequester = remember { FocusRequester() } + val context = LocalContext.current LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -49,8 +61,61 @@ fun EditCommentBottomModal(onSend: (String) -> Unit = {}) { modifier = Modifier .fillMaxWidth() .background(Color(0xfff7f7f7)) - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + if (replyComment == null) "评论" else "回复", + fontWeight = FontWeight.W600, + modifier = Modifier.weight(1f), + fontSize = 20.sp, + fontStyle = FontStyle.Italic + ) + Image( + painter = painterResource(id = R.drawable.rider_pro_send), + contentDescription = "Send", + modifier = Modifier + .size(32.dp) + .noRippleClickable { + onSend(text) + text = "" + }, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + if (replyComment != null) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + ) { + CustomAsyncImage( + context, + replyComment.avatar, + modifier = Modifier.fillMaxSize(), + contentDescription = "Avatar", + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Text(replyComment.name, fontWeight = FontWeight.Bold, fontSize = 16.sp) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + replyComment.comment, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(start = 32.dp), + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(16.dp)) + } Row( modifier = Modifier .fillMaxWidth(), @@ -62,7 +127,7 @@ fun EditCommentBottomModal(onSend: (String) -> Unit = {}) { .weight(1f) .clip(RoundedCornerShape(20.dp)) .background(Color(0xffe5e5e5)) - .padding(horizontal = 16.dp, vertical = 12.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) ) { BasicTextField( value = text, @@ -79,17 +144,6 @@ fun EditCommentBottomModal(onSend: (String) -> Unit = {}) { minLines = 5 ) } - Spacer(modifier = Modifier.width(16.dp)) - Image( - painter = painterResource(id = R.drawable.rider_pro_send), - contentDescription = "Send", - modifier = Modifier - .size(32.dp) - .noRippleClickable { - onSend(text) - text = "" - }, - ) } Spacer(modifier = Modifier.height(navBarHeight)) } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt index 27487da..a95001c 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt @@ -3,6 +3,7 @@ package com.aiosman.riderpro.ui.composables import android.content.Context import android.graphics.Bitmap import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -16,6 +17,8 @@ import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toDrawable import coil.ImageLoader import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter +import coil.compose.rememberImagePainter import coil.request.ImageRequest import coil.request.SuccessResult import com.aiosman.riderpro.utils.BlurHashDecoder diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt index 810b134..a221b94 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt @@ -128,8 +128,8 @@ fun NotificationsScreen() { CommentNoticeItem(comment) { MessageListViewModel.updateReadStatus(comment.id) MessageListViewModel.viewModelScope.launch { - PostViewModel.postId = comment.postId.toString() - PostViewModel.initData() +// PostViewModel.postId = comment.postId.toString() +// PostViewModel.initData() navController.navigate( NavigationRoute.Post.route.replace( "{id}", diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt index dec8987..c27faa5 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt @@ -101,6 +101,7 @@ fun MomentsList() { Column( modifier = Modifier .fillMaxSize() + .padding( top = statusBarPaddingValues.calculateTopPadding(), bottom = navigationBarPaddings @@ -175,7 +176,7 @@ fun MomentCard( modifier = Modifier .fillMaxWidth() .noRippleClickable { - PostViewModel.preTransit(momentEntity) +// PostViewModel.preTransit(momentEntity) navController.navigate("Post/${momentEntity.id}") } ) { @@ -195,7 +196,11 @@ fun MomentCard( navController.navigate(NavigationRoute.NewPost.route) }, onFavoriteClick = onFavoriteClick, - imageIndex = imageIndex + imageIndex = imageIndex, + onCommentClick = { +// PostViewModel.preTransit(momentEntity) + navController.navigate("Post/${momentEntity.id}") + } ) } } @@ -458,11 +463,13 @@ fun MomentOperateBtn(count: String, content: @Composable () -> Unit) { fun MomentBottomOperateRowGroup( onLikeClick: () -> Unit = {}, onAddComment: () -> Unit = {}, + onCommentClick: () -> Unit = {}, onFavoriteClick: () -> Unit = {}, onShareClick: () -> Unit = {}, momentEntity: MomentEntity, imageIndex: Int = 0 ) { + val navController = LocalNavController.current var showCommentModal by remember { mutableStateOf(false) } if (showCommentModal) { ModalBottomSheet( @@ -521,7 +528,7 @@ fun MomentBottomOperateRowGroup( modifier = Modifier .fillMaxHeight() .noRippleClickable { - showCommentModal = true + onCommentClick() }, contentAlignment = Alignment.Center ) { diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt index 9028660..b2ab971 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt @@ -820,7 +820,7 @@ fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) { .aspectRatio(3f / 2f) .padding(16.dp) .noRippleClickable { - PostViewModel.preTransit(momentEntity) +// PostViewModel.preTransit(momentEntity) navController.navigate( NavigationRoute.Post.route.replace( "{id}", diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/search/DiscoverScreen.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/search/DiscoverScreen.kt index 2963249..2af471a 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/search/DiscoverScreen.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/search/DiscoverScreen.kt @@ -158,7 +158,7 @@ fun DiscoverView() { .padding(2.dp) .noRippleClickable { - PostViewModel.preTransit(momentItem) +// PostViewModel.preTransit(momentItem) navController.navigate( NavigationRoute.Post.route.replace( "{id}", diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt new file mode 100644 index 0000000..93805b2 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt @@ -0,0 +1,163 @@ +package com.aiosman.riderpro.ui.post + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.aiosman.riderpro.data.CommentRemoteDataSource +import com.aiosman.riderpro.data.CommentService +import com.aiosman.riderpro.data.CommentServiceImpl +import com.aiosman.riderpro.entity.CommentEntity +import com.aiosman.riderpro.entity.CommentPagingSource +import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class CommentsViewModel( + var postId: String = 0.toString(), +) : ViewModel() { + var commentService: CommentService = CommentServiceImpl() + private var _commentsFlow = MutableStateFlow>(PagingData.empty()) + val commentsFlow = _commentsFlow.asStateFlow() + var order: String by mutableStateOf("like") + var addedCommentList by mutableStateOf>(emptyList()) + + /** + * 预加载,在跳转到 PostScreen 之前设置好内容 + */ + fun preTransit() { + viewModelScope.launch { + Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + CommentPagingSource( + CommentRemoteDataSource(commentService), + postId = postId.toInt() + ) + }).flow.cachedIn(viewModelScope).collectLatest { + _commentsFlow.value = it + } + } + } + + fun reloadComment() { + viewModelScope.launch { + Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + CommentPagingSource( + CommentRemoteDataSource(commentService), + postId = postId.toInt(), + order = order + ) + }).flow.cachedIn(viewModelScope).collectLatest { + _commentsFlow.value = it + } + } + } + + suspend fun likeComment(commentId: Int) { + commentService.likeComment(commentId) + val currentPagingData = commentsFlow.value + val updatedPagingData = currentPagingData.map { comment -> + if (comment.id == commentId) { + comment.copy(liked = !comment.liked, likes = comment.likes + 1) + } else { + // 可能是回复的评论 + comment.copy(reply = comment.reply.map { replyComment -> + if (replyComment.id == commentId) { + replyComment.copy( + liked = !replyComment.liked, likes = replyComment.likes + 1 + ) + } else { + replyComment + } + }) + } + } + _commentsFlow.value = updatedPagingData + // 更新addCommentList + addedCommentList = addedCommentList.map { + if (it.id == commentId) { + it.copy(liked = !it.liked, likes = it.likes + 1) + } else { + it + } + } + } + + suspend fun unlikeComment(commentId: Int) { + commentService.dislikeComment(commentId) + val currentPagingData = commentsFlow.value + val updatedPagingData = currentPagingData.map { comment -> + if (comment.id == commentId) { + comment.copy(liked = !comment.liked, likes = comment.likes - 1) + } else { + // 可能是回复的评论 + comment.copy(reply = comment.reply.map { replyComment -> + if (replyComment.id == commentId) { + replyComment.copy( + liked = !replyComment.liked, likes = replyComment.likes - 1 + ) + } else { + replyComment + } + }) + } + } + _commentsFlow.value = updatedPagingData + + // 更新addCommentList + addedCommentList = addedCommentList.map { + if (it.id == commentId) { + it.copy(liked = !it.liked, likes = it.likes - 1) + } else { + it + } + } + } + + suspend fun createComment( + content: String, parentCommentId: Int? = null, replyUserId: Int? = null + ) { + val comment = + commentService.createComment(postId.toInt(), content, parentCommentId, replyUserId) + MomentViewModel.updateCommentCount(postId.toInt()) + addedCommentList = addedCommentList.plus(comment) + } + + fun deleteComment(commentId: Int) { + viewModelScope.launch { + commentService.DeleteComment(commentId) + reloadComment() + } + } + + fun loadMoreSubComments(commentId: Int) { + viewModelScope.launch { + val currentPagingData = commentsFlow.value + val updatedPagingData = currentPagingData.map { comment -> + if (comment.id == commentId) { + val subCommentList = commentService.getComments( + postId = postId.toInt(), + parentCommentId = commentId, + pageNumber = comment.replyPage + 1, + pageSize = 3, + ).list + return@map comment.copy( + reply = comment.reply.plus(subCommentList), + replyPage = comment.replyPage + 1 + ) + } + comment + } + _commentsFlow.value = updatedPagingData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt index c0748d9..c498340 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -54,10 +55,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle 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.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.aiosman.riderpro.AppState @@ -71,6 +79,7 @@ import com.aiosman.riderpro.entity.MomentImageEntity import com.aiosman.riderpro.exp.formatPostTime import com.aiosman.riderpro.exp.timeAgo import com.aiosman.riderpro.ui.NavigationRoute +import com.aiosman.riderpro.ui.comment.CommentModalViewModel import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder @@ -86,12 +95,21 @@ import kotlinx.coroutines.launch fun PostScreen( id: String, ) { - val viewModel = PostViewModel + val viewModel = viewModel( + key = "PostViewModel_$id", + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return PostViewModel(id) as T + } + } + ) + val commentsViewModel = viewModel.commentsViewModel val scope = rememberCoroutineScope() - val commentsPagging = viewModel.commentsFlow.collectAsLazyPagingItems() val navController = LocalNavController.current var showCommentMenu by remember { mutableStateOf(false) } var contextComment by remember { mutableStateOf(null) } + var replyComment by remember { mutableStateOf(null) } + var showCommentModal by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.initData() } @@ -118,6 +136,43 @@ fun PostScreen( ) } } + if (showCommentModal) { + ModalBottomSheet( + onDismissRequest = { + showCommentModal = false + }, + containerColor = Color.White, + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ), + dragHandle = {}, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + windowInsets = WindowInsets(0) + ) { + EditCommentBottomModal(replyComment) { + viewModel.viewModelScope.launch { + if (replyComment != null) { + if (replyComment?.parentCommentId != null) { + // 第三级评论 + viewModel.createComment( + it, + parentCommentId = replyComment?.parentCommentId, + replyUserId = replyComment?.author?.toInt() + ) + } else { + // 子级评论 + viewModel.createComment(it, replyComment?.id) + } + } else { + // 顶级评论 + viewModel.createComment(it) + } + showCommentModal = false + } + + } + } + } Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { @@ -131,10 +186,9 @@ fun PostScreen( } } }, - onCreateComment = { - scope.launch { - viewModel.createComment(it) - } + onCreateCommentClick = { + replyComment = null + showCommentModal = true }, onFavoriteClick = { scope.launch { @@ -221,39 +275,25 @@ fun PostScreen( ) Spacer(modifier = Modifier.weight(1f)) OrderSelectionComponent() { - viewModel.order = it + commentsViewModel.order = it viewModel.reloadComment() } } Spacer(modifier = Modifier.height(16.dp)) } - - items(commentsPagging.itemCount) { idx -> - val item = commentsPagging[idx] ?: return@items - Box( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - CommentItem( - item, - onLike = { - scope.launch { - if (item.liked) { - viewModel.unlikeComment(item.id) - } else { - viewModel.likeComment(item.id) - } - } - }, - onLongClick = { - if (AppState.UserId != item.id) { - return@CommentItem - } - showCommentMenu = true - contextComment = item - } - ) - } + item { + CommentContent( + viewModel = commentsViewModel, + onLongClick = { + showCommentMenu = true + contextComment = it + }, + onReply = { parentComment, _, _, _ -> + replyComment = parentComment + showCommentModal = true + } + ) } item { Spacer(modifier = Modifier.height(72.dp)) @@ -264,7 +304,88 @@ fun PostScreen( } } +@Composable +fun CommentContent( + viewModel:CommentsViewModel, + onLongClick: (CommentEntity) -> Unit, + onReply: (CommentEntity, Long?, String?, String?) -> Unit +){ + val commentsPagging = viewModel.commentsFlow.collectAsLazyPagingItems() + val addedTopLevelComment = viewModel.addedCommentList.filter { + it.parentCommentId == null + } + for (item in addedTopLevelComment){ + Box( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + CommentItem( + item, + onLike = { comment -> + viewModel.viewModelScope.launch { + if (comment.liked) { + viewModel.unlikeComment(comment.id) + } else { + viewModel.likeComment(comment.id) + } + } + }, + onLongClick = { + if (AppState.UserId != item.id) { + return@CommentItem + } + onLongClick(item) + }, + onReply = { parentComment, _, _, _ -> + onReply(parentComment, parentComment.author, parentComment.name, parentComment.avatar) + }, + onLoadMoreSubComments = { + viewModel.viewModelScope.launch { + viewModel.loadMoreSubComments(it.id) + } + }, + addedCommentList = viewModel.addedCommentList + ) + } + } + + for (idx in 0 until commentsPagging.itemCount){ + val item = commentsPagging[idx] ?: return + Box( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + CommentItem( + item, + onLike = { comment -> + viewModel.viewModelScope.launch { + if (comment.liked) { + viewModel.unlikeComment(comment.id) + } else { + viewModel.likeComment(comment.id) + } + } + }, + onLongClick = { + if (AppState.UserId != item.id) { + return@CommentItem + } + onLongClick(item) + }, + onReply = { parentComment, _, _, _ -> + onReply(parentComment, parentComment.author, parentComment.name, parentComment.avatar) + }, + onLoadMoreSubComments = { + viewModel.viewModelScope.launch { + viewModel.loadMoreSubComments(it.id) + } + }, + addedCommentList = viewModel.addedCommentList + ) + } + } + + +} @OptIn(ExperimentalMaterial3Api::class) @Composable fun Header( @@ -526,8 +647,17 @@ fun CommentsSection( @Composable fun CommentItem( commentEntity: CommentEntity, - onLike: () -> Unit = {}, - onLongClick: () -> Unit = {} + isChild: Boolean = false, + onLike: (commentEntity: CommentEntity) -> Unit = {}, + onReply: ( + parentComment: CommentEntity, + replyUserId: Long?, + replyUserNickname: String?, + replyUserAvatar: String? + ) -> Unit = { _, _, _, _ -> }, + onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {}, + onLongClick: () -> Unit = {}, + addedCommentList: List = emptyList() ) { val context = LocalContext.current val navController = LocalNavController.current @@ -544,7 +674,7 @@ fun CommentItem( Row(modifier = Modifier.padding(vertical = 8.dp)) { Box( modifier = Modifier - .size(40.dp) + .size(if (isChild) 24.dp else 40.dp) .background(Color.Gray.copy(alpha = 0.1f)) .noRippleClickable { navController.navigate( @@ -559,7 +689,7 @@ fun CommentItem( context = context, imageUrl = commentEntity.avatar, contentDescription = "Comment Profile Picture", - modifier = Modifier.size(40.dp), + modifier = Modifier.size(if (isChild) 24.dp else 40.dp), contentScale = ContentScale.Crop ) } @@ -568,12 +698,77 @@ fun CommentItem( modifier = Modifier.weight(1f) ) { Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp) - Text(text = commentEntity.comment, fontSize = 14.sp) - Text( - text = commentEntity.date.timeAgo(context), - fontSize = 12.sp, - color = Color.Gray - ) + Row { + if (isChild) { + val annotatedText = buildAnnotatedString { + if (commentEntity.replyUserId != null) { + pushStringAnnotation( + tag = "replyUser", + annotation = commentEntity.replyUserId.toString() + ) + withStyle( + style = SpanStyle( + fontWeight = FontWeight.W600, + color = Color(0xFF6F94AE) + ) + ) { + append("@${commentEntity.replyUserNickname}") + } + pop() + } + append(" ${commentEntity.comment}") + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations( + tag = "replyUser", + start = offset, + end = offset + ).firstOrNull()?.let { + navController.navigate( + NavigationRoute.AccountProfile.route.replace( + "{id}", + it.item + ) + ) + } + }, + style = TextStyle(fontSize = 14.sp), + maxLines = Int.MAX_VALUE, + softWrap = true + ) + } else { + Text( + text = commentEntity.comment, + fontSize = 14.sp, + maxLines = Int.MAX_VALUE, + softWrap = true + ) + } + } + Row { + Text( + text = commentEntity.date.timeAgo(context), + fontSize = 12.sp, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Reply", + fontSize = 12.sp, + color = Color.Gray, + modifier = Modifier.noRippleClickable { + onReply( + commentEntity, + commentEntity.replyUserId, + commentEntity.replyUserNickname, + commentEntity.replyUserAvatar + ) + } + ) + } + } Spacer(modifier = Modifier.width(16.dp)) Column( @@ -581,7 +776,9 @@ fun CommentItem( ) { AnimatedLikeIcon( liked = commentEntity.liked, - onClick = onLike, + onClick = { + onLike(commentEntity) + }, modifier = Modifier.size(20.dp) ) Text(text = commentEntity.likes.toString(), fontSize = 12.sp) @@ -589,11 +786,40 @@ fun CommentItem( } Spacer(modifier = Modifier.height(8.dp)) Column( - modifier = Modifier.padding(start = 16.dp) + modifier = Modifier.padding(start = 12.dp + 40.dp) ) { - commentEntity.replies.forEach { reply -> - CommentItem(reply) + val addedCommentList = + addedCommentList.filter { it.parentCommentId == commentEntity.id } + addedCommentList.forEach { addedComment -> + CommentItem( + addedComment, + isChild = true, + onLike = onLike, + onReply = onReply, + onLongClick = onLongClick + ) } + commentEntity.reply.forEach { reply -> + CommentItem( + reply, + isChild = true, + onLike = onLike, + onReply = onReply, + onLongClick = onLongClick + ) + } + if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) { + val remaining = commentEntity.replyCount - commentEntity.reply.size + Text( + text = "View $remaining more replies", + fontSize = 12.sp, + color = Color(0xFF6F94AE), + modifier = Modifier.noRippleClickable { + onLoadMoreSubComments?.invoke(commentEntity) + } + ) + } + } } } @@ -601,29 +827,12 @@ fun CommentItem( @OptIn(ExperimentalMaterial3Api::class) @Composable fun PostBottomBar( - onCreateComment: (String) -> Unit = {}, + onCreateCommentClick: () -> Unit = {}, onLikeClick: () -> Unit = {}, onFavoriteClick: () -> Unit = {}, momentEntity: MomentEntity? ) { - var showCommentModal by remember { mutableStateOf(false) } - if (showCommentModal) { - ModalBottomSheet( - onDismissRequest = { showCommentModal = false }, - containerColor = Color.White, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ), - dragHandle = {}, - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - windowInsets = WindowInsets(0) - ) { - EditCommentBottomModal() { - onCreateComment(it) - showCommentModal = false - } - } - } + Column( modifier = Modifier.background(Color.White) ) { @@ -650,7 +859,7 @@ fun PostBottomBar( .height(31.dp) .padding(8.dp) .noRippleClickable { - showCommentModal = true + onCreateCommentClick() } ) { Row( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt index cb881c3..b6c8491 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt @@ -5,135 +5,62 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.CommentService -import com.aiosman.riderpro.data.CommentServiceImpl import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.UserService import com.aiosman.riderpro.data.UserServiceImpl import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.entity.CommentEntity -import com.aiosman.riderpro.entity.CommentPagingSource import com.aiosman.riderpro.entity.MomentEntity import com.aiosman.riderpro.entity.MomentServiceImpl import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -object PostViewModel : ViewModel() { - var service: MomentService = MomentServiceImpl() - var commentService: CommentService = CommentServiceImpl() - var userService: UserService = UserServiceImpl() - private var _commentsFlow = MutableStateFlow>(PagingData.empty()) - val commentsFlow = _commentsFlow.asStateFlow() - var postId: String = "" - var order: String by mutableStateOf("like") - // 预加载的 moment +class PostViewModel( + val postId: String +) : ViewModel() { + var service: MomentService = MomentServiceImpl() + var userService: UserService = UserServiceImpl() + var accountProfileEntity by mutableStateOf(null) var moment by mutableStateOf(null) var accountService: AccountService = AccountServiceImpl() + var commentsViewModel: CommentsViewModel = CommentsViewModel(postId) /** * 预加载,在跳转到 PostScreen 之前设置好内容 */ fun preTransit(momentEntity: MomentEntity?) { - this.postId = momentEntity?.id.toString() this.moment = momentEntity - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId = postId.toInt() - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _commentsFlow.value = it - } - } + this.nickname = momentEntity?.nickname ?: "" + this.commentsViewModel = CommentsViewModel(postId) + commentsViewModel.preTransit() } fun reloadComment() { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId = postId.toInt(), - order = order - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _commentsFlow.value = it - } - } + commentsViewModel.reloadComment() } suspend fun initData() { moment = service.getMomentById(postId.toInt()) - accountProfileEntity = userService.getUserProfile(moment?.authorId.toString()) - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId = postId.toInt() - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _commentsFlow.value = it - } - } -// moment?.let { -// accountProfileEntity = userService.getUserProfile(it.authorId.toString()) -// } +// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString()) + commentsViewModel.reloadComment() } suspend fun likeComment(commentId: Int) { - commentService.likeComment(commentId) - val currentPagingData = commentsFlow.value - val updatedPagingData = currentPagingData.map { comment -> - if (comment.id == commentId) { - comment.copy(liked = !comment.liked, likes = comment.likes + 1) - } else { - comment - } - } - _commentsFlow.value = updatedPagingData + commentsViewModel.likeComment(commentId) } suspend fun unlikeComment(commentId: Int) { - commentService.dislikeComment(commentId) - val currentPagingData = commentsFlow.value - val updatedPagingData = currentPagingData.map { comment -> - if (comment.id == commentId) { - comment.copy(liked = !comment.liked, likes = comment.likes - 1) - } else { - comment - } - } - _commentsFlow.value = updatedPagingData + commentsViewModel.unlikeComment(commentId) } - suspend fun createComment(content: String) { - commentService.createComment(postId.toInt(), content) - this.moment = service.getMomentById(postId.toInt()) - MomentViewModel.updateCommentCount(postId.toInt()) - reloadComment() + suspend fun createComment( + content: String, parentCommentId: Int? = null, replyUserId: Int? = null + ) { + commentsViewModel.createComment(content, parentCommentId, replyUserId) } suspend fun likeMoment() { @@ -165,8 +92,7 @@ object PostViewModel : ViewModel() { moment?.let { service.unfavoriteMoment(it.id) moment = moment?.copy( - favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, - isFavorite = false + favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, isFavorite = false ) } } @@ -184,14 +110,12 @@ object PostViewModel : ViewModel() { accountProfileEntity = accountProfileEntity?.copy(isFollowing = false) } } + fun deleteComment(commentId: Int) { - viewModelScope.launch { - commentService.DeleteComment(commentId) - moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0) - reloadComment() - moment?.let { - MomentViewModel.updateMomentCommentCount(it.id, -1) - } + commentsViewModel.deleteComment(commentId) + moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0) + moment?.let { + MomentViewModel.updateMomentCommentCount(it.id, -1) } } @@ -215,6 +139,7 @@ object PostViewModel : ViewModel() { } return field } + fun deleteMoment(callback: () -> Unit) { viewModelScope.launch { moment?.let { @@ -225,4 +150,8 @@ object PostViewModel : ViewModel() { } } + fun loadMoreSubComments(commentId: Int) { + commentsViewModel.loadMoreSubComments(commentId) + } + } \ No newline at end of file