点赞和评论

This commit is contained in:
2024-07-30 14:28:13 +08:00
parent 406caa3702
commit 0730fdea68
10 changed files with 357 additions and 93 deletions

View File

@@ -12,6 +12,7 @@ interface CommentService {
suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer<Comment> suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer<Comment>
suspend fun createComment(postId: Int, content: String, authorId: Int): Comment suspend fun createComment(postId: Int, content: String, authorId: Int): Comment
suspend fun likeComment(commentId: Int) suspend fun likeComment(commentId: Int)
suspend fun dislikeComment(commentId: Int)
} }
@@ -25,7 +26,7 @@ data class Comment(
val postId: Int = 0, val postId: Int = 0,
val avatar: String, val avatar: String,
val author: Int, val author: Int,
val liked: Boolean, var liked: Boolean,
) )
class CommentPagingSource( class CommentPagingSource(
@@ -82,6 +83,12 @@ class TestCommentServiceImpl : CommentService {
) )
} }
rawList = rawList.sortedBy { -it.id } rawList = rawList.sortedBy { -it.id }
rawList.forEach {
val myLikeIdList = TestDatabase.likeCommentList.filter { it.second == 1 }.map { it.first }
if (myLikeIdList.contains(it.id)) {
it.liked = true
}
}
val currentSublist = rawList.subList(from, min(to, rawList.size)) val currentSublist = rawList.subList(from, min(to, rawList.size))
return ListContainer( return ListContainer(
total = rawList.size, total = rawList.size,
@@ -121,7 +128,18 @@ class TestCommentServiceImpl : CommentService {
it it
} }
} }
TestDatabase.likeCommentList += Pair(commentId, 1)
}
override suspend fun dislikeComment(commentId: Int) {
TestDatabase.comment = TestDatabase.comment.map {
if (it.id == commentId) {
it.copy(likes = it.likes - 1)
} else {
it
}
}
TestDatabase.likeCommentList = TestDatabase.likeCommentList.filter { it.first != commentId }
} }
companion object { companion object {

View File

@@ -10,6 +10,7 @@ import kotlin.math.min
interface MomentService { interface MomentService {
suspend fun getMomentById(id: Int): MomentItem suspend fun getMomentById(id: Int): MomentItem
suspend fun likeMoment(id: Int) suspend fun likeMoment(id: Int)
suspend fun dislikeMoment(id: Int)
suspend fun getMoments( suspend fun getMoments(
pageNumber: Int, pageNumber: Int,
author: Int? = null, author: Int? = null,
@@ -81,6 +82,10 @@ class TestMomentServiceImpl() : MomentService {
testMomentBackend.likeMoment(id) testMomentBackend.likeMoment(id)
} }
override suspend fun dislikeMoment(id: Int) {
testMomentBackend.dislikeMoment(id)
}
} }
class TestMomentBackend( class TestMomentBackend(
@@ -113,6 +118,12 @@ class TestMomentBackend(
) )
} }
val currentSublist = rawList.subList(from, min(to, rawList.size)) val currentSublist = rawList.subList(from, min(to, rawList.size))
currentSublist.forEach {
val myLikeIdList = TestDatabase.likeMomentList.filter { it.second == 1 }.map { it.first }
if (myLikeIdList.contains(it.id)) {
it.liked = true
}
}
// delay // delay
kotlinx.coroutines.delay(loadDelay) kotlinx.coroutines.delay(loadDelay)
return ListContainer( return ListContainer(
@@ -124,7 +135,14 @@ class TestMomentBackend(
} }
suspend fun getMomentById(id: Int): MomentItem { suspend fun getMomentById(id: Int): MomentItem {
return TestDatabase.momentData[id] var moment = TestDatabase.momentData.first {
it.id == id
}
val isLike = TestDatabase.likeMomentList.any {
it.first == id && it.second == 1
}
moment = moment.copy(liked = isLike)
return moment
} }
suspend fun likeMoment(id: Int) { suspend fun likeMoment(id: Int) {
@@ -133,6 +151,17 @@ class TestMomentBackend(
} }
val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount + 1) val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount + 1)
TestDatabase.updateMomentById(id, newMoment) TestDatabase.updateMomentById(id, newMoment)
TestDatabase.likeMomentList += Pair(id, 1)
}
suspend fun dislikeMoment(id: Int) {
val oldMoment = TestDatabase.momentData.first {
it.id == id
}
val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount - 1)
TestDatabase.updateMomentById(id, newMoment)
TestDatabase.likeMomentList = TestDatabase.likeMomentList.filter {
it.first != id
}
} }
} }

View File

@@ -17,5 +17,6 @@ data class MomentItem(
val shareCount: Int, val shareCount: Int,
val favoriteCount: Int, val favoriteCount: Int,
val images: List<String> = emptyList(), val images: List<String> = emptyList(),
val authorId: Int = 0 val authorId: Int = 0,
var liked: Boolean = false,
) )

View File

@@ -39,13 +39,14 @@ object TestDatabase {
) )
var followList = emptyList<Pair<Int, Int>>() var followList = emptyList<Pair<Int, Int>>()
var likeCommentList = emptyList<Pair<Int, Int>>() var likeCommentList = emptyList<Pair<Int, Int>>()
var likeMomentList = emptyList<Pair<Int, Int>>()
init { init {
val faker = faker { val faker = faker {
this.fakerConfig { this.fakerConfig {
locale = "en" locale = "en"
} }
} }
accountData = (0..100).toList().mapIndexed { idx, _ -> accountData = (0..20).toList().mapIndexed { idx, _ ->
AccountProfile( AccountProfile(
id = idx, id = idx,
followerCount = 0, followerCount = 0,
@@ -58,7 +59,7 @@ object TestDatabase {
) )
} }
// make a random follow rel // make a random follow rel
for (i in 0..500) { for (i in 0..100) {
var person1 = accountData.random() var person1 = accountData.random()
var persion2 = accountData.random() var persion2 = accountData.random()
followList += Pair(person1.id, persion2.id) followList += Pair(person1.id, persion2.id)
@@ -74,13 +75,14 @@ object TestDatabase {
} }
} }
momentData = (0..200).toList().mapIndexed { idx, _ -> momentData = (0..60).toList().mapIndexed { idx, _ ->
val person = accountData.random() val person = accountData.random()
// make fake comment // make fake comment
for (i in 0..faker.random.nextInt(0, 5)) { val commentCount = faker.random.nextInt(0, 50)
for (i in 0..commentCount) {
commentIdCounter += 1 commentIdCounter += 1
val commentPerson = accountData.random() val commentPerson = accountData.random()
var newComment = Comment( var newComment = Comment(
name = commentPerson.nickName, name = commentPerson.nickName,
comment = "this is comment ${commentIdCounter}", comment = "this is comment ${commentIdCounter}",
date = "2023-02-02 11:23", date = "2023-02-02 11:23",
@@ -94,12 +96,17 @@ object TestDatabase {
) )
// generate like comment list // generate like comment list
for (likeIdx in 0..faker.random.nextInt(0, 5)) { for (likeIdx in 0..faker.random.nextInt(0, 5)) {
val likePerson = accountData.random() val likePerson = accountData.random()
likeCommentList += Pair(commentIdCounter, likePerson.id) likeCommentList += Pair(commentIdCounter, likePerson.id)
newComment = newComment.copy(likes = newComment.likes + 1) newComment = newComment.copy(likes = newComment.likes + 1)
} }
comment += newComment comment += newComment
} }
val likeCount = faker.random.nextInt(0, 5)
for (i in 0..likeCount) {
val likePerson = accountData.random()
likeMomentList += Pair(idx, likePerson.id)
}
MomentItem( MomentItem(
id = idx, id = idx,
avatar = person.avatar, avatar = person.avatar,
@@ -109,8 +116,8 @@ object TestDatabase {
followStatus = false, followStatus = false,
momentTextContent = "By strongarming Ducati into giving him the factory seat.Marquez effectively …", momentTextContent = "By strongarming Ducati into giving him the factory seat.Marquez effectively …",
momentPicture = R.drawable.default_moment_img, momentPicture = R.drawable.default_moment_img,
likeCount = faker.random.nextInt(0, 100), likeCount = likeCount,
commentCount = faker.random.nextInt(0, 100), commentCount = commentCount + 1,
shareCount = faker.random.nextInt(0, 100), shareCount = faker.random.nextInt(0, 100),
favoriteCount = faker.random.nextInt(0, 100), favoriteCount = faker.random.nextInt(0, 100),
images = imageList.shuffled().take(3), images = imageList.shuffled().take(3),

View File

@@ -58,8 +58,8 @@ import kotlinx.coroutines.launch
class CommentModalViewModel( class CommentModalViewModel(
postId: Int? postId: Int?
):ViewModel(){ ) : ViewModel() {
val commentService:CommentService = TestCommentServiceImpl() val commentService: CommentService = TestCommentServiceImpl()
val commentsFlow: Flow<PagingData<Comment>> = Pager( val commentsFlow: Flow<PagingData<Comment>> = Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false), config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { pagingSourceFactory = {
@@ -70,10 +70,16 @@ class CommentModalViewModel(
} }
).flow.cachedIn(viewModelScope) ).flow.cachedIn(viewModelScope)
} }
@Preview @Preview
@Composable @Composable
fun CommentModalContent(postId: Int? = null, onDismiss: () -> Unit = {}) { fun CommentModalContent(
postId: Int? = null,
onCommentAdded: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val model = viewModel<CommentModalViewModel>( val model = viewModel<CommentModalViewModel>(
key = "CommentModalViewModel_$postId",
factory = object : ViewModelProvider.Factory { factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return CommentModalViewModel(postId) as T return CommentModalViewModel(postId) as T
@@ -102,6 +108,7 @@ fun CommentModalContent(postId: Int? = null, onDismiss: () -> Unit = {}) {
commentText = "" commentText = ""
} }
comments.refresh() comments.refresh()
onCommentAdded()
} }
Column( Column(
modifier = Modifier modifier = Modifier
@@ -129,8 +136,7 @@ fun CommentModalContent(postId: Int? = null, onDismiss: () -> Unit = {}) {
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.weight(1f) .weight(1f)
) { ) {
CommentsSection(lazyPagingItems = comments, onLike = { CommentsSection(lazyPagingItems = comments, onLike = { comment: Comment ->
comment: Comment ->
scope.launch { scope.launch {
model.commentService.likeComment(comment.id) model.commentService.likeComment(comment.id)
comments.refresh() comments.refresh()

View File

@@ -24,9 +24,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun AnimatedLikeIcon( fun AnimatedLikeIcon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
liked: Boolean = false,
onClick: (() -> Unit)? = null onClick: (() -> Unit)? = null
) { ) {
var liked by remember { mutableStateOf(false) }
val animatableRotation = remember { Animatable(0f) } val animatableRotation = remember { Animatable(0f) }
val animatedColor by animateColorAsState(targetValue = if (liked) Color(0xFFd83737) else Color.Black) val animatedColor by animateColorAsState(targetValue = if (liked) Color(0xFFd83737) else Color.Black)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -52,13 +52,11 @@ fun AnimatedLikeIcon(
) )
} }
Box(contentAlignment = Alignment.Center, modifier = Modifier.noRippleClickable { Box(contentAlignment = Alignment.Center, modifier = Modifier.noRippleClickable {
liked = !liked
onClick?.invoke() onClick?.invoke()
// Trigger shake animation // Trigger shake animation
scope.launch { scope.launch {
shake() shake()
} }
}) { }) {
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_like), painter = painterResource(id = R.drawable.rider_pro_like),

View File

@@ -6,13 +6,16 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -22,9 +25,11 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
@Composable @Composable
fun EditCommentBottomModal() { fun EditCommentBottomModal(onSend: (String) -> Unit = {}) {
var text by remember { mutableStateOf("") }
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -49,8 +54,10 @@ fun EditCommentBottomModal() {
) { ) {
BasicTextField( BasicTextField(
value = "", value = text,
onValueChange = { }, onValueChange = {
text = it
},
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
textStyle = TextStyle( textStyle = TextStyle(
@@ -67,10 +74,11 @@ fun EditCommentBottomModal() {
contentDescription = "Send", contentDescription = "Send",
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.noRippleClickable {
onSend(text)
text = ""
},
) )
} }
} }
} }

View File

@@ -58,23 +58,30 @@ import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun MomentsList() { fun MomentsList() {
val model = MomentViewModel val model = MomentViewModel
var dataFlow = model.momentsFlow var dataFlow = model.momentsFlow
var moments = dataFlow.collectAsLazyPagingItems() var moments = dataFlow.collectAsLazyPagingItems()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LazyColumn { LazyColumn {
items(moments.itemCount) { idx -> items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items val momentItem = moments[idx] ?: return@items
MomentCard(momentItem = momentItem, onLikeClick = { MomentCard(momentItem = momentItem,
scope.launch { onAddComment = {
model.likeMoment(momentItem.id) scope.launch {
// moments.refresh() model.onAddComment(momentItem.id)
} }
}) },
onLikeClick = {
scope.launch {
if (momentItem.liked) {
model.dislikeMoment(momentItem.id)
} else {
model.likeMoment(momentItem.id)
}
}
})
} }
} }
@@ -83,7 +90,8 @@ fun MomentsList() {
@Composable @Composable
fun MomentCard( fun MomentCard(
momentItem: MomentItem, momentItem: MomentItem,
onLikeClick: () -> Unit onLikeClick: () -> Unit,
onAddComment: () -> Unit = {}
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Column(
@@ -103,7 +111,12 @@ fun MomentCard(
.fillMaxHeight() .fillMaxHeight()
.weight(1f) .weight(1f)
ModificationListHeader() ModificationListHeader()
MomentBottomOperateRowGroup(momentOperateBtnBoxModifier, momentItem = momentItem, onLikeClick = onLikeClick) MomentBottomOperateRowGroup(
momentOperateBtnBoxModifier,
momentItem = momentItem,
onLikeClick = onLikeClick,
onAddComment = onAddComment
)
} }
} }
@@ -214,9 +227,16 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
AsyncImage( AsyncImage(
momentItem.avatar, momentItem.avatar,
contentDescription = "", contentDescription = "",
modifier = Modifier.size(40.dp).noRippleClickable { modifier = Modifier
navController.navigate(NavigationRoute.AccountProfile.route.replace("{id}", momentItem.authorId.toString())) .size(40.dp)
}, .noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
momentItem.authorId.toString()
)
)
},
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Column( Column(
@@ -310,6 +330,7 @@ fun MomentOperateBtn(count: String, content: @Composable () -> Unit) {
fun MomentBottomOperateRowGroup( fun MomentBottomOperateRowGroup(
modifier: Modifier, modifier: Modifier,
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
onAddComment: () -> Unit = {},
momentItem: MomentItem momentItem: MomentItem
) { ) {
var systemUiController = rememberSystemUiController() var systemUiController = rememberSystemUiController()
@@ -323,7 +344,10 @@ fun MomentBottomOperateRowGroup(
) )
) { ) {
systemUiController.setNavigationBarColor(Color(0xfff7f7f7)) systemUiController.setNavigationBarColor(Color(0xfff7f7f7))
CommentModalContent(postId = momentItem.id) { CommentModalContent(postId = momentItem.id, onCommentAdded = {
showCommentModal = false
onAddComment()
}) {
systemUiController.setNavigationBarColor(Color.Black) systemUiController.setNavigationBarColor(Color.Black)
} }
} }
@@ -338,8 +362,14 @@ fun MomentBottomOperateRowGroup(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MomentOperateBtn(count = momentItem.likeCount.toString()) { MomentOperateBtn(count = momentItem.likeCount.toString()) {
AnimatedLikeIcon(modifier = Modifier.size(24.dp)) { AnimatedLikeIcon(
modifier = Modifier.size(24.dp),
liked = momentItem.liked
) {
onLikeClick() onLikeClick()
} }
} }
} }
@@ -352,19 +382,28 @@ fun MomentBottomOperateRowGroup(
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MomentOperateBtn(icon = R.drawable.rider_pro_moment_comment, count = momentItem.commentCount.toString()) MomentOperateBtn(
icon = R.drawable.rider_pro_moment_comment,
count = momentItem.commentCount.toString()
)
} }
Box( Box(
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MomentOperateBtn(icon = R.drawable.rider_pro_share, count = momentItem.shareCount.toString()) MomentOperateBtn(
icon = R.drawable.rider_pro_share,
count = momentItem.shareCount.toString()
)
} }
Box( Box(
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MomentOperateBtn(icon = R.drawable.rider_pro_favoriate, count = momentItem.favoriteCount.toString()) MomentOperateBtn(
icon = R.drawable.rider_pro_favoriate,
count = momentItem.favoriteCount.toString()
)
} }
} }
} }

View File

@@ -31,22 +31,23 @@ object MomentViewModel : ViewModel() {
val profile = accountService.getMyAccountProfile() val profile = accountService.getMyAccountProfile()
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = { MomentPagingSource( pagingSourceFactory = {
MomentRemoteDataSource(momentService), MomentPagingSource(
timelineId = profile.id MomentRemoteDataSource(momentService),
) } timelineId = profile.id
)
}
).flow.cachedIn(viewModelScope).collectLatest { ).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it _momentsFlow.value = it
} }
} }
} }
suspend fun likeMoment(id: Int) { fun updateLikeCount(id: Int) {
momentService.likeMoment(id)
val currentPagingData = _momentsFlow.value val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem -> val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount + 1) momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
} else { } else {
momentItem momentItem
} }
@@ -54,4 +55,42 @@ object MomentViewModel : ViewModel() {
_momentsFlow.value = updatedPagingData _momentsFlow.value = updatedPagingData
} }
suspend fun likeMoment(id: Int) {
momentService.likeMoment(id)
updateLikeCount(id)
}
fun updateCommentCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(commentCount = momentItem.commentCount + 1)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun onAddComment(id: Int) {
val currentPagingData = _momentsFlow.value
updateCommentCount(id)
}
fun updateDislikeMomentById(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun dislikeMoment(id: Int) {
momentService.dislikeMoment(id)
updateDislikeMomentById(id)
}
} }

View File

@@ -58,11 +58,17 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
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.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.map
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
@@ -71,66 +77,154 @@ import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.Comment import com.aiosman.riderpro.data.Comment
import com.aiosman.riderpro.data.CommentPagingSource import com.aiosman.riderpro.data.CommentPagingSource
import com.aiosman.riderpro.data.CommentRemoteDataSource import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.CommentService
import com.aiosman.riderpro.data.TestCommentServiceImpl import com.aiosman.riderpro.data.TestCommentServiceImpl
import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.TestAccountServiceImpl import com.aiosman.riderpro.data.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestMomentServiceImpl import com.aiosman.riderpro.data.TestMomentServiceImpl
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentItem
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PostViewModel(
@OptIn(ExperimentalSharedTransitionApi::class) val postId: String
@Composable ) : ViewModel() {
fun PostScreen(
id: String,
) {
var service: MomentService = TestMomentServiceImpl() var service: MomentService = TestMomentServiceImpl()
var commentService: CommentService = TestCommentServiceImpl()
private var _commentsFlow = MutableStateFlow<PagingData<Comment>>(PagingData.empty())
val commentsFlow = _commentsFlow.asStateFlow()
var commentSource = CommentPagingSource( init {
CommentRemoteDataSource(TestCommentServiceImpl()) viewModelScope.launch {
) Pager(
val commentsFlow: Flow<PagingData<Comment>> = Pager( config = PagingConfig(pageSize = 5, enablePlaceholders = false),
config = PagingConfig(pageSize = 5, enablePlaceholders = false), pagingSourceFactory = {
pagingSourceFactory = { commentSource } CommentPagingSource(
).flow CommentRemoteDataSource(commentService),
val lazyPagingItems = commentsFlow.collectAsLazyPagingItems() postId = postId.toInt()
var showCollapseContent by remember { mutableStateOf(true) } )
val scrollState = rememberLazyListState() }
val uiController = rememberSystemUiController() ).flow.cachedIn(viewModelScope).collectLatest {
var moment by remember { mutableStateOf<MomentItem?>(null) } _commentsFlow.value = it
var accountProfile by remember { mutableStateOf<AccountProfile?>(null) } }
}
}
var accountProfile by mutableStateOf<AccountProfile?>(null)
var moment by mutableStateOf<MomentItem?>(null)
var accountService: AccountService = TestAccountServiceImpl() var accountService: AccountService = TestAccountServiceImpl()
LaunchedEffect(Unit) {
uiController.setNavigationBarColor(Color.White) suspend fun initData() {
moment = service.getMomentById(id.toInt()) moment = service.getMomentById(postId.toInt())
moment?.let { moment?.let {
accountProfile = accountService.getAccountProfileById(it.authorId) accountProfile = accountService.getAccountProfileById(it.authorId)
} }
} }
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)
} else {
comment
}
}
_commentsFlow.value = updatedPagingData
}
suspend fun createComment(content: String) {
commentService.createComment(postId.toInt(), content, 1)
MomentViewModel.updateCommentCount(postId.toInt())
}
suspend fun likeMoment() {
moment?.let {
service.likeMoment(it.id)
moment = moment?.copy(likeCount = moment?.likeCount?.plus(1) ?: 0, liked = true)
MomentViewModel.updateLikeCount(it.id)
}
}
suspend fun dislikeMoment() {
moment?.let {
service.dislikeMoment(it.id)
moment = moment?.copy(likeCount = moment?.likeCount?.minus(1) ?: 0, liked = false)
// update home list
MomentViewModel.updateDislikeMomentById(it.id)
}
}
}
@Composable
fun PostScreen(
id: String,
) {
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 scope = rememberCoroutineScope()
val commentsPagging = viewModel.commentsFlow.collectAsLazyPagingItems()
var showCollapseContent by remember { mutableStateOf(true) }
val scrollState = rememberLazyListState()
val uiController = rememberSystemUiController()
LaunchedEffect(Unit) {
uiController.setNavigationBarColor(Color.White)
viewModel.initData()
}
StatusBarMaskLayout { StatusBarMaskLayout {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { BottomNavigationBar() } bottomBar = {
BottomNavigationBar(
onLikeClick = {
scope.launch {
if (viewModel.moment?.liked == true) {
viewModel.dislikeMoment()
} else {
viewModel.likeMoment()
}
}
},
onCreateComment = {
scope.launch {
viewModel.createComment(it)
commentsPagging.refresh()
}
},
momentItem = viewModel.moment
)
}
) { ) {
it it
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
Header(accountProfile) Header(viewModel.accountProfile)
Column(modifier = Modifier.animateContentSize()) { Column(modifier = Modifier.animateContentSize()) {
AnimatedVisibility(visible = showCollapseContent) { AnimatedVisibility(visible = showCollapseContent) {
// collapse content // collapse content
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -140,13 +234,13 @@ fun PostScreen(
) { ) {
PostImageView( PostImageView(
id, id,
moment?.images ?: emptyList() viewModel.moment?.images ?: emptyList()
) )
} }
PostDetails( PostDetails(
id, id,
moment viewModel.moment
) )
} }
} }
@@ -156,7 +250,14 @@ fun PostScreen(
.fillMaxWidth() .fillMaxWidth()
) { ) {
CommentsSection(lazyPagingItems = lazyPagingItems, scrollState, onLike = {}) { CommentsSection(
lazyPagingItems = commentsPagging,
scrollState,
onLike = { comment: Comment ->
scope.launch {
viewModel.likeComment(comment.id)
}
}) {
showCollapseContent = it showCollapseContent = it
} }
} }
@@ -182,7 +283,6 @@ fun Header(accountProfile: AccountProfile?) {
navController.popBackStack() navController.popBackStack()
} }
.size(32.dp) .size(32.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
accountProfile?.let { accountProfile?.let {
@@ -191,7 +291,15 @@ fun Header(accountProfile: AccountProfile?) {
contentDescription = "Profile Picture", contentDescription = "Profile Picture",
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(CircleShape), .clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
accountProfile.id.toString()
)
)
},
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
@@ -298,11 +406,9 @@ fun PostDetails(
) )
Text(text = "12-11 发布") Text(text = "12-11 发布")
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = "共231条评论") Text(text = "${momentItem?.commentCount ?: 0} Comments")
} }
} }
@Composable @Composable
@@ -319,7 +425,7 @@ fun CommentsSection(
) { ) {
items(lazyPagingItems.itemCount) { idx -> items(lazyPagingItems.itemCount) { idx ->
val item = lazyPagingItems[idx] ?: return@items val item = lazyPagingItems[idx] ?: return@items
CommentItem(item,onLike={ CommentItem(item, onLike = {
onLike(item) onLike(item)
}) })
} }
@@ -339,7 +445,7 @@ fun CommentsSection(
@Composable @Composable
fun CommentItem(comment: Comment,onLike:()->Unit = {}) { fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
Column { Column {
Row(modifier = Modifier.padding(vertical = 8.dp)) { Row(modifier = Modifier.padding(vertical = 8.dp)) {
AsyncImage( AsyncImage(
@@ -361,7 +467,11 @@ fun CommentItem(comment: Comment,onLike:()->Unit = {}) {
IconButton(onClick = { IconButton(onClick = {
onLike() onLike()
}) { }) {
Icon(Icons.Filled.Favorite, contentDescription = "Like") Icon(
Icons.Filled.Favorite,
contentDescription = "Like",
tint = if (comment.liked) Color.Red else Color.Gray
)
} }
Text(text = comment.likes.toString()) Text(text = comment.likes.toString())
} }
@@ -379,7 +489,11 @@ fun CommentItem(comment: Comment,onLike:()->Unit = {}) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BottomNavigationBar() { fun BottomNavigationBar(
onCreateComment: (String) -> Unit = {},
onLikeClick: () -> Unit = {},
momentItem: MomentItem?
) {
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
if (showCommentModal) { if (showCommentModal) {
@@ -392,7 +506,10 @@ fun BottomNavigationBar() {
dragHandle = {}, dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) { ) {
EditCommentBottomModal() EditCommentBottomModal() {
onCreateComment(it)
showCommentModal = false
}
} }
} }
Column( Column(
@@ -427,10 +544,12 @@ fun BottomNavigationBar() {
} }
IconButton( IconButton(
onClick = { /*TODO*/ }) { onClick = {
Icon(Icons.Filled.Favorite, contentDescription = "Send") onLikeClick()
}) {
Icon(Icons.Filled.Favorite, contentDescription = "like", tint = if (momentItem?.liked == true) Color.Red else Color.Gray)
} }
Text(text = "2077") Text(text = momentItem?.likeCount.toString())
IconButton( IconButton(
onClick = { /*TODO*/ }) { onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Star, contentDescription = "Send") Icon(Icons.Filled.Star, contentDescription = "Send")