新增评论回复功能
- 允许用户回复评论和子评论 - 点击回复按钮,弹出评论框,并显示回复的用户 - 评论 列表中显示回复的用户和内容 - 点击回复内容中的用户名,跳转到用户主页 - 优化评论列表加载逻辑,支持加载更多子评论
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CommentEntity>
|
||||
|
||||
/**
|
||||
* 创建评论
|
||||
* @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<Comment>,
|
||||
@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<CommentEntity> {
|
||||
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<CommentEntity> {
|
||||
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) {
|
||||
|
||||
@@ -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<Unit>
|
||||
): Response<DataContainer<Comment>>
|
||||
|
||||
@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<ListContainer<Comment>>
|
||||
|
||||
@GET("account/my")
|
||||
|
||||
@@ -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<CommentEntity>,
|
||||
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<Int, CommentEntity>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CommentEntity> {
|
||||
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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ fun AccountEditScreen() {
|
||||
if (bannerImageUrl != null) {
|
||||
bannerImageUrl.toString()
|
||||
} else {
|
||||
it.banner
|
||||
it.banner!!
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -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<CommentEntity>>(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<CommentEntity?>(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<CommentEntity?>(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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -158,7 +158,7 @@ fun DiscoverView() {
|
||||
.padding(2.dp)
|
||||
|
||||
.noRippleClickable {
|
||||
PostViewModel.preTransit(momentItem)
|
||||
// PostViewModel.preTransit(momentItem)
|
||||
navController.navigate(
|
||||
NavigationRoute.Post.route.replace(
|
||||
"{id}",
|
||||
|
||||
@@ -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<CommentEntity>>(PagingData.empty())
|
||||
val commentsFlow = _commentsFlow.asStateFlow()
|
||||
var order: String by mutableStateOf("like")
|
||||
var addedCommentList by mutableStateOf<List<CommentEntity>>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PostViewModel>(
|
||||
key = "PostViewModel_$id",
|
||||
factory = object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): 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<CommentEntity?>(null) }
|
||||
var replyComment by remember { mutableStateOf<CommentEntity?>(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<CommentEntity> = 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(
|
||||
|
||||
@@ -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<CommentEntity>>(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<AccountProfileEntity?>(null)
|
||||
var moment by mutableStateOf<MomentEntity?>(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)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user