新增评论回复功能
- 允许用户回复评论和子评论 - 点击回复按钮,弹出评论框,并显示回复的用户 - 评论 列表中显示回复的用户和内容 - 点击回复内容中的用户名,跳转到用户主页 - 优化评论列表加载逻辑,支持加载更多子评论
This commit is contained in:
@@ -38,6 +38,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// Firebase Analytics
|
// Firebase Analytics
|
||||||
private lateinit var analytics: FirebaseAnalytics
|
private lateinit var analytics: FirebaseAnalytics
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
// 请求通知权限
|
// 请求通知权限
|
||||||
private val requestPermissionLauncher = registerForActivityResult(
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission(),
|
ActivityResultContracts.RequestPermission(),
|
||||||
@@ -100,13 +101,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
val postId = intent.getStringExtra("POST_ID")
|
val postId = intent.getStringExtra("POST_ID")
|
||||||
if (postId != null) {
|
if (postId != null) {
|
||||||
Log.d("MainActivity", "Navigation to Post$postId")
|
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 postId 动态ID,过滤条件
|
||||||
* @param postUser 动态作者ID,获取某个用户所有动态下的评论
|
* @param postUser 动态作者ID,获取某个用户所有动态下的评论
|
||||||
* @param selfNotice 是否是自己的通知
|
* @param selfNotice 是否是自己的通知
|
||||||
|
* @param order 排序
|
||||||
|
* @param parentCommentId 父评论ID
|
||||||
|
* @param pageSize 每页数量
|
||||||
* @return 评论列表
|
* @return 评论列表
|
||||||
*/
|
*/
|
||||||
suspend fun getComments(
|
suspend fun getComments(
|
||||||
@@ -22,15 +25,24 @@ interface CommentService {
|
|||||||
postId: Int? = null,
|
postId: Int? = null,
|
||||||
postUser: Int? = null,
|
postUser: Int? = null,
|
||||||
selfNotice: Boolean? = null,
|
selfNotice: Boolean? = null,
|
||||||
order: String? = null
|
order: String? = null,
|
||||||
|
parentCommentId: Int? = null,
|
||||||
|
pageSize: Int? = null
|
||||||
): ListContainer<CommentEntity>
|
): ListContainer<CommentEntity>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建评论
|
* 创建评论
|
||||||
* @param postId 动态ID
|
* @param postId 动态ID
|
||||||
* @param content 评论内容
|
* @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?,
|
val post: NoticePost?,
|
||||||
// 是否未读
|
// 是否未读
|
||||||
@SerializedName("isUnread")
|
@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
|
* 转换为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?,
|
postId: Int?,
|
||||||
postUser: Int?,
|
postUser: Int?,
|
||||||
selfNotice: Boolean?,
|
selfNotice: Boolean?,
|
||||||
order: String?
|
order: String?,
|
||||||
|
parentCommentId: Int?
|
||||||
): ListContainer<CommentEntity> {
|
): ListContainer<CommentEntity> {
|
||||||
return commentService.getComments(
|
return commentService.getComments(
|
||||||
pageNumber,
|
pageNumber,
|
||||||
postId,
|
postId,
|
||||||
postUser = postUser,
|
postUser = postUser,
|
||||||
selfNotice = selfNotice,
|
selfNotice = selfNotice,
|
||||||
order = order
|
order = order,
|
||||||
|
parentCommentId = parentCommentId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +174,9 @@ class CommentServiceImpl : CommentService {
|
|||||||
postId: Int?,
|
postId: Int?,
|
||||||
postUser: Int?,
|
postUser: Int?,
|
||||||
selfNotice: Boolean?,
|
selfNotice: Boolean?,
|
||||||
order: String?
|
order: String?,
|
||||||
|
parentCommentId: Int?,
|
||||||
|
pageSize: Int?
|
||||||
): ListContainer<CommentEntity> {
|
): ListContainer<CommentEntity> {
|
||||||
val resp = ApiClient.api.getComments(
|
val resp = ApiClient.api.getComments(
|
||||||
page = pageNumber,
|
page = pageNumber,
|
||||||
@@ -155,7 +185,9 @@ class CommentServiceImpl : CommentService {
|
|||||||
order = order,
|
order = order,
|
||||||
selfNotice = selfNotice?.let {
|
selfNotice = selfNotice?.let {
|
||||||
if (it) 1 else 0
|
if (it) 1 else 0
|
||||||
}
|
},
|
||||||
|
parentCommentId = parentCommentId,
|
||||||
|
pageSize = pageSize ?: 20
|
||||||
)
|
)
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get comments")
|
val body = resp.body() ?: throw ServiceException("Failed to get comments")
|
||||||
return ListContainer(
|
return ListContainer(
|
||||||
@@ -166,9 +198,18 @@ class CommentServiceImpl : CommentService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createComment(postId: Int, content: String) {
|
override suspend fun createComment(
|
||||||
val resp = ApiClient.api.createComment(postId, CommentRequestBody(content))
|
postId: Int,
|
||||||
return
|
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) {
|
override suspend fun likeComment(commentId: Int) {
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ data class ValidateTokenResult(
|
|||||||
|
|
||||||
data class CommentRequestBody(
|
data class CommentRequestBody(
|
||||||
@SerializedName("content")
|
@SerializedName("content")
|
||||||
val content: String
|
val content: String,
|
||||||
|
@SerializedName("parentCommentId")
|
||||||
|
val parentCommentId: Int? = null,
|
||||||
|
@SerializedName("replyUserId")
|
||||||
|
val replyUserId: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ChangePasswordRequestBody(
|
data class ChangePasswordRequestBody(
|
||||||
@@ -149,7 +153,7 @@ interface RiderProAPI {
|
|||||||
suspend fun createComment(
|
suspend fun createComment(
|
||||||
@Path("id") id: Int,
|
@Path("id") id: Int,
|
||||||
@Body body: CommentRequestBody
|
@Body body: CommentRequestBody
|
||||||
): Response<Unit>
|
): Response<DataContainer<Comment>>
|
||||||
|
|
||||||
@POST("comment/{id}/like")
|
@POST("comment/{id}/like")
|
||||||
suspend fun likeComment(
|
suspend fun likeComment(
|
||||||
@@ -175,6 +179,7 @@ interface RiderProAPI {
|
|||||||
@Query("postUser") postUser: Int? = null,
|
@Query("postUser") postUser: Int? = null,
|
||||||
@Query("selfNotice") selfNotice: Int? = 0,
|
@Query("selfNotice") selfNotice: Int? = 0,
|
||||||
@Query("order") order: String? = null,
|
@Query("order") order: String? = null,
|
||||||
|
@Query("parentCommentId") parentCommentId: Int? = null,
|
||||||
): Response<ListContainer<Comment>>
|
): Response<ListContainer<Comment>>
|
||||||
|
|
||||||
@GET("account/my")
|
@GET("account/my")
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ data class CommentEntity(
|
|||||||
val author: Long,
|
val author: Long,
|
||||||
var liked: Boolean,
|
var liked: Boolean,
|
||||||
var unread: Boolean = false,
|
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(
|
class CommentPagingSource(
|
||||||
@@ -27,7 +34,8 @@ class CommentPagingSource(
|
|||||||
private val postId: Int? = null,
|
private val postId: Int? = null,
|
||||||
private val postUser: Int? = null,
|
private val postUser: Int? = null,
|
||||||
private val selfNotice: Boolean? = null,
|
private val selfNotice: Boolean? = null,
|
||||||
private val order: String? = null
|
private val order: String? = null,
|
||||||
|
private val parentCommentId: Int? = null
|
||||||
) : PagingSource<Int, CommentEntity>() {
|
) : PagingSource<Int, CommentEntity>() {
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CommentEntity> {
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CommentEntity> {
|
||||||
return try {
|
return try {
|
||||||
@@ -37,7 +45,8 @@ class CommentPagingSource(
|
|||||||
postId = postId,
|
postId = postId,
|
||||||
postUser = postUser,
|
postUser = postUser,
|
||||||
selfNotice = selfNotice,
|
selfNotice = selfNotice,
|
||||||
order = order
|
order = order,
|
||||||
|
parentCommentId = parentCommentId
|
||||||
)
|
)
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
data = comments.list,
|
data = comments.list,
|
||||||
|
|||||||
@@ -132,12 +132,6 @@ fun NavigationController(
|
|||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.Post.route,
|
route = NavigationRoute.Post.route,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }),
|
arguments = listOf(navArgument("id") { type = NavType.StringType }),
|
||||||
enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(durationMillis = 50))
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
fadeOut(animationSpec = tween(durationMillis = 50))
|
|
||||||
}
|
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAnimatedContentScope provides this,
|
LocalAnimatedContentScope provides this,
|
||||||
@@ -165,13 +159,6 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.NewPost.route,
|
route = NavigationRoute.NewPost.route,
|
||||||
enterTransition = {
|
|
||||||
|
|
||||||
fadeIn(animationSpec = tween(durationMillis = 100))
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
fadeOut(animationSpec = tween(durationMillis = 100))
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
NewPostScreen()
|
NewPostScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ fun AccountEditScreen() {
|
|||||||
if (bannerImageUrl != null) {
|
if (bannerImageUrl != null) {
|
||||||
bannerImageUrl.toString()
|
bannerImageUrl.toString()
|
||||||
} else {
|
} else {
|
||||||
it.banner
|
it.banner!!
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.navigationBars
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -47,24 +48,17 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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 androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.riderpro.AppState
|
import com.aiosman.riderpro.AppState
|
||||||
import com.aiosman.riderpro.R
|
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.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.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.riderpro.ui.post.CommentContent
|
||||||
import com.aiosman.riderpro.ui.post.CommentMenuModal
|
import com.aiosman.riderpro.ui.post.CommentMenuModal
|
||||||
import com.aiosman.riderpro.ui.post.CommentsSection
|
import com.aiosman.riderpro.ui.post.CommentsSection
|
||||||
|
import com.aiosman.riderpro.ui.post.CommentsViewModel
|
||||||
import com.aiosman.riderpro.ui.post.OrderSelectionComponent
|
import com.aiosman.riderpro.ui.post.OrderSelectionComponent
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,50 +67,13 @@ import kotlinx.coroutines.launch
|
|||||||
class CommentModalViewModel(
|
class CommentModalViewModel(
|
||||||
val postId: Int?
|
val postId: Int?
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val commentService: CommentService = CommentServiceImpl()
|
|
||||||
var commentText by mutableStateOf("")
|
var commentText by mutableStateOf("")
|
||||||
var order by mutableStateOf("like")
|
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId.toString())
|
||||||
val commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
|
||||||
|
|
||||||
init {
|
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()
|
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.reloadComments()
|
|
||||||
}
|
}
|
||||||
var showCommentMenu by remember { mutableStateOf(false) }
|
var showCommentMenu by remember { mutableStateOf(false) }
|
||||||
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val comments = model.commentsFlow.collectAsLazyPagingItems()
|
|
||||||
val insets = WindowInsets
|
val insets = WindowInsets
|
||||||
val imePadding = insets.ime.getBottom(density = LocalDensity.current)
|
val imePadding = insets.ime.getBottom(density = LocalDensity.current)
|
||||||
var bottomPadding by remember { mutableStateOf(0.dp) }
|
var bottomPadding by remember { mutableStateOf(0.dp) }
|
||||||
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(imePadding) {
|
LaunchedEffect(imePadding) {
|
||||||
bottomPadding = imePadding.dp
|
bottomPadding = imePadding.dp
|
||||||
}
|
}
|
||||||
@@ -179,7 +137,7 @@ fun CommentModalContent(
|
|||||||
onDeleteClick = {
|
onDeleteClick = {
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
contextComment?.let {
|
contextComment?.let {
|
||||||
model.updateDeleteComment(it.id)
|
commentViewModel.deleteComment(it.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -188,7 +146,9 @@ fun CommentModalContent(
|
|||||||
suspend fun sendComment() {
|
suspend fun sendComment() {
|
||||||
if (model.commentText.isNotEmpty()) {
|
if (model.commentText.isNotEmpty()) {
|
||||||
softwareKeyboardController?.hide()
|
softwareKeyboardController?.hide()
|
||||||
model.createComment()
|
commentViewModel.createComment(
|
||||||
|
model.commentText,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onCommentAdded()
|
onCommentAdded()
|
||||||
}
|
}
|
||||||
@@ -225,8 +185,8 @@ fun CommentModalContent(
|
|||||||
color = Color(0xff666666)
|
color = Color(0xff666666)
|
||||||
)
|
)
|
||||||
OrderSelectionComponent {
|
OrderSelectionComponent {
|
||||||
model.order = it
|
commentViewModel.order = it
|
||||||
model.reloadComments()
|
commentViewModel.reloadComment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -235,82 +195,49 @@ fun CommentModalContent(
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
CommentsSection(
|
LazyColumn(
|
||||||
lazyPagingItems = comments,
|
modifier = Modifier
|
||||||
onLike = { commentEntity: CommentEntity ->
|
.fillMaxWidth()
|
||||||
scope.launch {
|
) {
|
||||||
if (commentEntity.liked) {
|
item {
|
||||||
model.commentService.dislikeComment(commentEntity.id)
|
CommentContent(
|
||||||
} else {
|
viewModel = commentViewModel,
|
||||||
model.commentService.likeComment(commentEntity.id)
|
|
||||||
}
|
|
||||||
model.reloadComments()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = { commentEntity: CommentEntity ->
|
onLongClick = { commentEntity: CommentEntity ->
|
||||||
if (AppState.UserId?.toLong() == commentEntity.author) {
|
|
||||||
contextComment = commentEntity
|
|
||||||
showCommentMenu = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onWillCollapse = {
|
onReply = { parentComment, _, _, _ ->
|
||||||
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(72.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xfff7f7f7))
|
.background(Color(0xfff7f7f7))
|
||||||
) {
|
) {
|
||||||
Box(
|
EditCommentBottomModal(replyComment) {
|
||||||
modifier = Modifier
|
commentViewModel.viewModelScope.launch {
|
||||||
.fillMaxWidth()
|
if (replyComment != null) {
|
||||||
.padding(horizontal = 16.dp)
|
if (replyComment?.parentCommentId != null) {
|
||||||
.height(64.dp)
|
// 第三级评论
|
||||||
) {
|
commentViewModel.createComment(
|
||||||
Row(
|
it,
|
||||||
modifier = Modifier
|
parentCommentId = replyComment?.parentCommentId,
|
||||||
.fillMaxWidth()
|
replyUserId = replyComment?.author?.toInt()
|
||||||
.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
|
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
// 子级评论
|
||||||
|
commentViewModel.createComment(it, replyComment?.id)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
} else {
|
||||||
Image(
|
// 顶级评论
|
||||||
painter = painterResource(id = R.drawable.rider_pro_send),
|
commentViewModel.createComment(it)
|
||||||
contentDescription = "Send",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
scope.launch {
|
|
||||||
sendComment()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(navBarHeight))
|
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.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
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.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.entity.CommentEntity
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditCommentBottomModal(onSend: (String) -> Unit = {}) {
|
fun EditCommentBottomModal(
|
||||||
|
replyComment: CommentEntity? = null,
|
||||||
|
onSend: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
@@ -49,8 +61,61 @@ fun EditCommentBottomModal(onSend: (String) -> Unit = {}) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xfff7f7f7))
|
.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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
@@ -62,7 +127,7 @@ fun EditCommentBottomModal(onSend: (String) -> Unit = {}) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(Color(0xffe5e5e5))
|
.background(Color(0xffe5e5e5))
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = text,
|
value = text,
|
||||||
@@ -79,17 +144,6 @@ fun EditCommentBottomModal(onSend: (String) -> Unit = {}) {
|
|||||||
minLines = 5
|
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))
|
Spacer(modifier = Modifier.height(navBarHeight))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.aiosman.riderpro.ui.composables
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -16,6 +17,8 @@ import androidx.core.graphics.drawable.toBitmap
|
|||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.request.SuccessResult
|
import coil.request.SuccessResult
|
||||||
import com.aiosman.riderpro.utils.BlurHashDecoder
|
import com.aiosman.riderpro.utils.BlurHashDecoder
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ fun NotificationsScreen() {
|
|||||||
CommentNoticeItem(comment) {
|
CommentNoticeItem(comment) {
|
||||||
MessageListViewModel.updateReadStatus(comment.id)
|
MessageListViewModel.updateReadStatus(comment.id)
|
||||||
MessageListViewModel.viewModelScope.launch {
|
MessageListViewModel.viewModelScope.launch {
|
||||||
PostViewModel.postId = comment.postId.toString()
|
// PostViewModel.postId = comment.postId.toString()
|
||||||
PostViewModel.initData()
|
// PostViewModel.initData()
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.Post.route.replace(
|
NavigationRoute.Post.route.replace(
|
||||||
"{id}",
|
"{id}",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ fun MomentsList() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
||||||
.padding(
|
.padding(
|
||||||
top = statusBarPaddingValues.calculateTopPadding(),
|
top = statusBarPaddingValues.calculateTopPadding(),
|
||||||
bottom = navigationBarPaddings
|
bottom = navigationBarPaddings
|
||||||
@@ -175,7 +176,7 @@ fun MomentCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
PostViewModel.preTransit(momentEntity)
|
// PostViewModel.preTransit(momentEntity)
|
||||||
navController.navigate("Post/${momentEntity.id}")
|
navController.navigate("Post/${momentEntity.id}")
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -195,7 +196,11 @@ fun MomentCard(
|
|||||||
navController.navigate(NavigationRoute.NewPost.route)
|
navController.navigate(NavigationRoute.NewPost.route)
|
||||||
},
|
},
|
||||||
onFavoriteClick = onFavoriteClick,
|
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(
|
fun MomentBottomOperateRowGroup(
|
||||||
onLikeClick: () -> Unit = {},
|
onLikeClick: () -> Unit = {},
|
||||||
onAddComment: () -> Unit = {},
|
onAddComment: () -> Unit = {},
|
||||||
|
onCommentClick: () -> Unit = {},
|
||||||
onFavoriteClick: () -> Unit = {},
|
onFavoriteClick: () -> Unit = {},
|
||||||
onShareClick: () -> Unit = {},
|
onShareClick: () -> Unit = {},
|
||||||
momentEntity: MomentEntity,
|
momentEntity: MomentEntity,
|
||||||
imageIndex: Int = 0
|
imageIndex: Int = 0
|
||||||
) {
|
) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
var showCommentModal by remember { mutableStateOf(false) }
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
if (showCommentModal) {
|
if (showCommentModal) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
@@ -521,7 +528,7 @@ fun MomentBottomOperateRowGroup(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showCommentModal = true
|
onCommentClick()
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -820,7 +820,7 @@ fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
|
|||||||
.aspectRatio(3f / 2f)
|
.aspectRatio(3f / 2f)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
PostViewModel.preTransit(momentEntity)
|
// PostViewModel.preTransit(momentEntity)
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.Post.route.replace(
|
NavigationRoute.Post.route.replace(
|
||||||
"{id}",
|
"{id}",
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ fun DiscoverView() {
|
|||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
PostViewModel.preTransit(momentItem)
|
// PostViewModel.preTransit(momentItem)
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.Post.route.replace(
|
NavigationRoute.Post.route.replace(
|
||||||
"{id}",
|
"{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.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
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.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.LazyPagingItems
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.riderpro.AppState
|
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.formatPostTime
|
||||||
import com.aiosman.riderpro.exp.timeAgo
|
import com.aiosman.riderpro.exp.timeAgo
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
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.AnimatedFavouriteIcon
|
||||||
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
|
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
|
||||||
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
||||||
@@ -86,12 +95,21 @@ import kotlinx.coroutines.launch
|
|||||||
fun PostScreen(
|
fun PostScreen(
|
||||||
id: String,
|
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 scope = rememberCoroutineScope()
|
||||||
val commentsPagging = viewModel.commentsFlow.collectAsLazyPagingItems()
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
var showCommentMenu by remember { mutableStateOf(false) }
|
var showCommentMenu by remember { mutableStateOf(false) }
|
||||||
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.initData()
|
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(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@@ -131,10 +186,9 @@ fun PostScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCreateComment = {
|
onCreateCommentClick = {
|
||||||
scope.launch {
|
replyComment = null
|
||||||
viewModel.createComment(it)
|
showCommentModal = true
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onFavoriteClick = {
|
onFavoriteClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -221,40 +275,26 @@ fun PostScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
OrderSelectionComponent() {
|
OrderSelectionComponent() {
|
||||||
viewModel.order = it
|
commentsViewModel.order = it
|
||||||
viewModel.reloadComment()
|
viewModel.reloadComment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
items(commentsPagging.itemCount) { idx ->
|
CommentContent(
|
||||||
val item = commentsPagging[idx] ?: return@items
|
viewModel = commentsViewModel,
|
||||||
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 = {
|
onLongClick = {
|
||||||
if (AppState.UserId != item.id) {
|
|
||||||
return@CommentItem
|
|
||||||
}
|
|
||||||
showCommentMenu = true
|
showCommentMenu = true
|
||||||
contextComment = item
|
contextComment = it
|
||||||
|
},
|
||||||
|
onReply = { parentComment, _, _, _ ->
|
||||||
|
replyComment = parentComment
|
||||||
|
showCommentModal = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(72.dp))
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Header(
|
fun Header(
|
||||||
@@ -526,8 +647,17 @@ fun CommentsSection(
|
|||||||
@Composable
|
@Composable
|
||||||
fun CommentItem(
|
fun CommentItem(
|
||||||
commentEntity: CommentEntity,
|
commentEntity: CommentEntity,
|
||||||
onLike: () -> Unit = {},
|
isChild: Boolean = false,
|
||||||
onLongClick: () -> Unit = {}
|
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 context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
@@ -544,7 +674,7 @@ fun CommentItem(
|
|||||||
Row(modifier = Modifier.padding(vertical = 8.dp)) {
|
Row(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(if (isChild) 24.dp else 40.dp)
|
||||||
.background(Color.Gray.copy(alpha = 0.1f))
|
.background(Color.Gray.copy(alpha = 0.1f))
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -559,7 +689,7 @@ fun CommentItem(
|
|||||||
context = context,
|
context = context,
|
||||||
imageUrl = commentEntity.avatar,
|
imageUrl = commentEntity.avatar,
|
||||||
contentDescription = "Comment Profile Picture",
|
contentDescription = "Comment Profile Picture",
|
||||||
modifier = Modifier.size(40.dp),
|
modifier = Modifier.size(if (isChild) 24.dp else 40.dp),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -568,12 +698,77 @@ fun CommentItem(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp)
|
Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp)
|
||||||
Text(text = commentEntity.comment, fontSize = 14.sp)
|
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(
|
||||||
text = commentEntity.date.timeAgo(context),
|
text = commentEntity.date.timeAgo(context),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color.Gray
|
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))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Column(
|
Column(
|
||||||
@@ -581,7 +776,9 @@ fun CommentItem(
|
|||||||
) {
|
) {
|
||||||
AnimatedLikeIcon(
|
AnimatedLikeIcon(
|
||||||
liked = commentEntity.liked,
|
liked = commentEntity.liked,
|
||||||
onClick = onLike,
|
onClick = {
|
||||||
|
onLike(commentEntity)
|
||||||
|
},
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Text(text = commentEntity.likes.toString(), fontSize = 12.sp)
|
Text(text = commentEntity.likes.toString(), fontSize = 12.sp)
|
||||||
@@ -589,11 +786,40 @@ fun CommentItem(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 12.dp + 40.dp)
|
||||||
) {
|
) {
|
||||||
commentEntity.replies.forEach { reply ->
|
val addedCommentList =
|
||||||
CommentItem(reply)
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PostBottomBar(
|
fun PostBottomBar(
|
||||||
onCreateComment: (String) -> Unit = {},
|
onCreateCommentClick: () -> Unit = {},
|
||||||
onLikeClick: () -> Unit = {},
|
onLikeClick: () -> Unit = {},
|
||||||
onFavoriteClick: () -> Unit = {},
|
onFavoriteClick: () -> Unit = {},
|
||||||
momentEntity: MomentEntity?
|
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(
|
Column(
|
||||||
modifier = Modifier.background(Color.White)
|
modifier = Modifier.background(Color.White)
|
||||||
) {
|
) {
|
||||||
@@ -650,7 +859,7 @@ fun PostBottomBar(
|
|||||||
.height(31.dp)
|
.height(31.dp)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showCommentModal = true
|
onCreateCommentClick()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -5,135 +5,62 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.AccountService
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
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.MomentService
|
||||||
import com.aiosman.riderpro.data.UserService
|
import com.aiosman.riderpro.data.UserService
|
||||||
import com.aiosman.riderpro.data.UserServiceImpl
|
import com.aiosman.riderpro.data.UserServiceImpl
|
||||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
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.MomentEntity
|
||||||
import com.aiosman.riderpro.entity.MomentServiceImpl
|
import com.aiosman.riderpro.entity.MomentServiceImpl
|
||||||
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel
|
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
|
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 accountProfileEntity by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
var moment by mutableStateOf<MomentEntity?>(null)
|
var moment by mutableStateOf<MomentEntity?>(null)
|
||||||
var accountService: AccountService = AccountServiceImpl()
|
var accountService: AccountService = AccountServiceImpl()
|
||||||
|
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
* 预加载,在跳转到 PostScreen 之前设置好内容
|
||||||
*/
|
*/
|
||||||
fun preTransit(momentEntity: MomentEntity?) {
|
fun preTransit(momentEntity: MomentEntity?) {
|
||||||
this.postId = momentEntity?.id.toString()
|
|
||||||
this.moment = momentEntity
|
this.moment = momentEntity
|
||||||
viewModelScope.launch {
|
this.nickname = momentEntity?.nickname ?: ""
|
||||||
Pager(
|
this.commentsViewModel = CommentsViewModel(postId)
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
commentsViewModel.preTransit()
|
||||||
pagingSourceFactory = {
|
|
||||||
CommentPagingSource(
|
|
||||||
CommentRemoteDataSource(commentService),
|
|
||||||
postId = postId.toInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
).flow.cachedIn(viewModelScope).collectLatest {
|
|
||||||
_commentsFlow.value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadComment() {
|
fun reloadComment() {
|
||||||
viewModelScope.launch {
|
commentsViewModel.reloadComment()
|
||||||
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 initData() {
|
suspend fun initData() {
|
||||||
moment = service.getMomentById(postId.toInt())
|
moment = service.getMomentById(postId.toInt())
|
||||||
accountProfileEntity = userService.getUserProfile(moment?.authorId.toString())
|
// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString())
|
||||||
viewModelScope.launch {
|
commentsViewModel.reloadComment()
|
||||||
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())
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun likeComment(commentId: Int) {
|
suspend fun likeComment(commentId: Int) {
|
||||||
commentService.likeComment(commentId)
|
commentsViewModel.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun unlikeComment(commentId: Int) {
|
suspend fun unlikeComment(commentId: Int) {
|
||||||
commentService.dislikeComment(commentId)
|
commentsViewModel.unlikeComment(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createComment(content: String) {
|
suspend fun createComment(
|
||||||
commentService.createComment(postId.toInt(), content)
|
content: String, parentCommentId: Int? = null, replyUserId: Int? = null
|
||||||
this.moment = service.getMomentById(postId.toInt())
|
) {
|
||||||
MomentViewModel.updateCommentCount(postId.toInt())
|
commentsViewModel.createComment(content, parentCommentId, replyUserId)
|
||||||
reloadComment()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun likeMoment() {
|
suspend fun likeMoment() {
|
||||||
@@ -165,8 +92,7 @@ object PostViewModel : ViewModel() {
|
|||||||
moment?.let {
|
moment?.let {
|
||||||
service.unfavoriteMoment(it.id)
|
service.unfavoriteMoment(it.id)
|
||||||
moment = moment?.copy(
|
moment = moment?.copy(
|
||||||
favoriteCount = moment?.favoriteCount?.minus(1) ?: 0,
|
favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, isFavorite = false
|
||||||
isFavorite = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,16 +110,14 @@ object PostViewModel : ViewModel() {
|
|||||||
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
|
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteComment(commentId: Int) {
|
fun deleteComment(commentId: Int) {
|
||||||
viewModelScope.launch {
|
commentsViewModel.deleteComment(commentId)
|
||||||
commentService.DeleteComment(commentId)
|
|
||||||
moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0)
|
moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0)
|
||||||
reloadComment()
|
|
||||||
moment?.let {
|
moment?.let {
|
||||||
MomentViewModel.updateMomentCommentCount(it.id, -1)
|
MomentViewModel.updateMomentCommentCount(it.id, -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var avatar: String? = null
|
var avatar: String? = null
|
||||||
get() {
|
get() {
|
||||||
@@ -215,6 +139,7 @@ object PostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMoment(callback: () -> Unit) {
|
fun deleteMoment(callback: () -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
moment?.let {
|
moment?.let {
|
||||||
@@ -225,4 +150,8 @@ object PostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadMoreSubComments(commentId: Int) {
|
||||||
|
commentsViewModel.loadMoreSubComments(commentId)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user