feat(posts): comments and actions in post detail

This commit is contained in:
Diego Beraldin 2023-08-03 23:13:38 +02:00
parent 0f1a1dbb4e
commit 1440fd6390
27 changed files with 565 additions and 234 deletions

View File

@ -14,7 +14,7 @@ kotlin {
android {
compilations.all {
kotlinOptions {
jvmTarget = "17"
jvmTarget = "1.8"
}
}
}

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CommentResponse(
@SerialName("comment_view") val commentView: CommentView,
@SerialName("recipient_ids") val recipientIds: List<LocalUserId>,
@SerialName("form_id") val formId: String? = null,
)

View File

@ -0,0 +1,14 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateCommentForm(
@SerialName("content") val content: String,
@SerialName("post_id") val postId: PostId,
@SerialName("parent_id") val parentId: CommentId? = null,
@SerialName("language_id") val languageId: LanguageId? = null,
@SerialName("form_id") val formId: String? = null,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateCommentLikeForm(
@SerialName("comment_id") val commentId: CommentId,
@SerialName("score") val score: Int,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DeleteCommentForm(
@SerialName("comment_id") val commentId: CommentId,
@SerialName("deleted") val deleted: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class EditCommentForm(
@SerialName("comment_id") val commentId: CommentId,
@SerialName("content") val content: String? = null,
@SerialName("language_id") val languageId: LanguageId? = null,
@SerialName("form_id") val formId: String? = null,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SaveCommentForm(
@SerialName("comment_id") val commentId: CommentId,
@SerialName("save") val save: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -1,15 +1,25 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.service
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreateCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreateCommentLikeForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.DeleteCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetCommentsResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.PUT
import de.jensklingenberg.ktorfit.http.Query
interface CommentService {
@GET("comment/list")
suspend fun getComments(
suspend fun getAll(
@Query("auth") auth: String? = null,
@Query("limit") limit: Int? = null,
@Query("sort") sort: SortType? = null,
@ -22,4 +32,24 @@ interface CommentService {
@Query("community_name") communityName: String? = null,
@Query("saved_only") savedOnly: Boolean? = null,
): Response<GetCommentsResponse>
@PUT("comment/save")
@Headers("Content-Type: application/json")
suspend fun save(@Body form: SaveCommentForm): Response<CommentResponse>
@POST("comment/like")
@Headers("Content-Type: application/json")
suspend fun like(@Body form: CreateCommentLikeForm): Response<CommentResponse>
@POST("comment")
@Headers("Content-Type: application/json")
suspend fun create(@Body form: CreateCommentForm): Response<CommentResponse>
@PUT("comment")
@Headers("Content-Type: application/json")
suspend fun edit(@Body form: EditCommentForm): Response<CommentResponse>
@POST("comment/delete")
@Headers("Content-Type: application/json")
suspend fun delete(@Body form: DeleteCommentForm): Response<CommentResponse>
}

View File

@ -21,19 +21,19 @@ import de.jensklingenberg.ktorfit.http.Query
interface PostService {
@GET("post/list")
suspend fun getPosts(
suspend fun getAll(
@Query("auth") auth: String? = null,
@Query("limit") limit: Int? = null,
@Query("sort") sort: SortType? = null,
@Query("comment_id") commentId: Int? = null,
@Query("page") page: Int? = null,
@Query("type_") type: com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType? = null,
@Query("type_") type: ListingType? = null,
@Query("community_name") communityName: String? = null,
@Query("saved_only") savedOnly: Boolean? = null,
): Response<GetPostsResponse>
@GET("post")
suspend fun getPost(
suspend fun get(
@Query("auth") auth: String? = null,
@Query("id") id: Int? = null,
@Query("comment_id") commentId: Int? = null,
@ -41,21 +41,21 @@ interface PostService {
@PUT("post/save")
@Headers("Content-Type: application/json")
suspend fun savePost(@Body form: SavePostForm): Response<PostResponse>
suspend fun save(@Body form: SavePostForm): Response<PostResponse>
@POST("post/like")
@Headers("Content-Type: application/json")
suspend fun likePost(@Body form: CreatePostLikeForm): Response<PostResponse>
suspend fun like(@Body form: CreatePostLikeForm): Response<PostResponse>
@POST("post")
@Headers("Content-Type: application/json")
suspend fun createPost(@Body form: CreatePostForm): Response<PostResponse>
suspend fun create(@Body form: CreatePostForm): Response<PostResponse>
@PUT("post")
@Headers("Content-Type: application/json")
suspend fun editPost(@Body form: EditPostForm): Response<PostResponse>
suspend fun edit(@Body form: EditPostForm): Response<PostResponse>
@POST("post/delete")
@Headers("Content-Type: application/json")
suspend fun deletePost(@Body form: DeletePostForm): Response<PostResponse>
suspend fun delete(@Body form: DeletePostForm): Response<PostResponse>
}

View File

@ -1,81 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.Markdown
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import io.kamel.image.KamelImage
import io.kamel.image.asyncPainterResource
@Composable
fun CommentCard(
comment: CommentModel,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(CornerSize.m),
).padding(
vertical = Spacing.lHalf,
horizontal = Spacing.s,
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
val communityName = comment.community?.name.orEmpty()
val communityIcon = comment.community?.icon.orEmpty()
val communityHost = comment.community?.host.orEmpty()
val iconSize = 16.dp
if (communityName.isNotEmpty()) {
if (communityIcon.isNotEmpty()) {
val painterResource = asyncPainterResource(data = communityIcon)
KamelImage(
modifier = Modifier.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
resource = painterResource,
contentDescription = null,
contentScale = ContentScale.FillBounds,
)
}
Text(
text = buildString {
append(communityName)
if (communityHost.isNotEmpty()) {
append("@$communityHost")
}
},
style = MaterialTheme.typography.bodySmall,
)
}
}
val body = comment.text
if (body.isNotEmpty()) {
Markdown(content = body)
}
}
}
}

View File

@ -3,18 +3,16 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.diegoberaldin.raccoonforlemmy.core.markdown.compose.Markdown
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
@Composable
fun PostCardBody(
modifier: Modifier = Modifier,
post: PostModel,
text: String,
) {
val body = post.text
if (body.isNotEmpty()) {
if (text.isNotEmpty()) {
Markdown(
modifier = modifier,
content = body,
content = text,
)
}
}

View File

@ -23,64 +23,69 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
@Composable
fun PostCardFooter(
post: PostModel,
onUpVote: (Boolean) -> Unit,
onDownVote: (Boolean) -> Unit,
onSave: (Boolean) -> Unit,
onReply: () -> Unit,
comments: Int? = null,
score: Int,
saved: Boolean = false,
upVoted: Boolean = false,
downVoted: Boolean = false,
onUpVote: ((Boolean) -> Unit)? = null,
onDownVote: ((Boolean) -> Unit)? = null,
onSave: ((Boolean) -> Unit)? = null,
onReply: (() -> Unit)? = null,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
val buttonModifier = Modifier.size(32.dp).padding(4.dp)
Image(
modifier = buttonModifier.onClick(onReply),
imageVector = Icons.Default.Chat,
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
)
Text(
modifier = Modifier.padding(end = Spacing.s),
text = "${post.comments}",
)
if (comments != null) {
Image(
modifier = buttonModifier.onClick {
onReply?.invoke()
},
imageVector = Icons.Default.Chat,
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
)
Text(
modifier = Modifier.padding(end = Spacing.s),
text = "$comments",
)
}
Spacer(modifier = Modifier.weight(1f))
Image(
modifier = buttonModifier.onClick {
onSave(!post.saved)
onSave?.invoke(!saved)
},
imageVector = if (!post.saved) {
imageVector = if (!saved) {
Icons.Default.BookmarkBorder
} else {
Icons.Default.Bookmark
},
contentDescription = null,
colorFilter = ColorFilter.tint(
color = if (post.saved) {
color = if (saved) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.onSurface
},
),
)
val upvoted = post.myVote > 0
val downvoted = post.myVote < 0
Image(
modifier = buttonModifier.onClick {
onUpVote(!upvoted)
onUpVote?.invoke(!upVoted)
},
imageVector = if (upvoted) {
imageVector = if (upVoted) {
Icons.Filled.ThumbUp
} else {
Icons.Outlined.ThumbUp
},
contentDescription = null,
colorFilter = ColorFilter.tint(
color = if (upvoted) {
color = if (upVoted) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.onSurface
@ -88,20 +93,20 @@ fun PostCardFooter(
),
)
Text(
text = "${post.score}",
text = "$score",
)
Image(
modifier = buttonModifier.onClick {
onDownVote(!downvoted)
onDownVote?.invoke(!downVoted)
},
imageVector = if (downvoted) {
imageVector = if (downVoted) {
Icons.Filled.ThumbDown
} else {
Icons.Outlined.ThumbDown
},
contentDescription = null,
colorFilter = ColorFilter.tint(
color = if (downvoted) {
color = if (downVoted) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.onSurface

View File

@ -11,6 +11,7 @@ val postDetailModule = module {
mvi = DefaultMviModel(PostDetailScreenMviModel.UiState()),
post = params[0],
identityRepository = get(),
postsRepository = get(),
commentRepository = get(),
keyStore = get(),
)

View File

@ -0,0 +1,58 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardFooter
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardSubtitle
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
@Composable
fun CommentCard(
comment: CommentModel,
onUpVote: ((Boolean) -> Unit)? = null,
onDownVote: ((Boolean) -> Unit)? = null,
onSave: ((Boolean) -> Unit)? = null,
onReply: (() -> Unit)? = null,
) {
Card(
modifier = Modifier.fillMaxWidth().background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(CornerSize.m),
).padding(
vertical = Spacing.lHalf,
horizontal = Spacing.s,
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
PostCardSubtitle(
creator = comment.creator,
)
PostCardBody(
text = comment.text,
)
PostCardFooter(
score = comment.score,
saved = comment.saved,
upVoted = comment.myVote > 0,
downVoted = comment.myVote < 0,
onUpVote = onUpVote,
onDownVote = onDownVote,
onSave = onSave,
onReply = onReply,
)
}
}
}

View File

@ -33,7 +33,6 @@ import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommentCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardFooter
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardImage
@ -70,6 +69,7 @@ class PostDetailScreen(
)
},
) { padding ->
val post = uiState.post
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(PostDetailScreenMviModel.Intent.Refresh)
})
@ -88,26 +88,53 @@ class PostDetailScreen(
)
PostCardImage(post)
PostCardBody(
post = post,
text = post.text,
)
PostCardFooter(
post = post,
comments = post.comments,
score = post.score,
upVoted = post.myVote > 0,
downVoted = post.myVote < 0,
saved = post.saved,
onUpVote = {
// TODO
model.reduce(PostDetailScreenMviModel.Intent.UpVotePost(it, post))
},
onDownVote = {
// TODO
model.reduce(PostDetailScreenMviModel.Intent.DownVotePost(it, post))
},
onSave = {
// TODO
},
onReply = {
// TODO
model.reduce(PostDetailScreenMviModel.Intent.SavePost(it, post))
},
)
}
items(uiState.comments) { comment ->
CommentCard(comment)
CommentCard(
comment = comment,
onUpVote = {
model.reduce(
PostDetailScreenMviModel.Intent.UpVoteComment(
it,
comment,
),
)
},
onDownVote = {
model.reduce(
PostDetailScreenMviModel.Intent.DownVoteComment(
it,
comment,
),
)
},
onSave = {
model.reduce(
PostDetailScreenMviModel.Intent.SaveComment(
it,
comment,
),
)
},
)
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
interface PostDetailScreenMviModel :
MviModel<PostDetailScreenMviModel.Intent, PostDetailScreenMviModel.UiState, PostDetailScreenMviModel.Effect> {
@ -9,9 +10,16 @@ interface PostDetailScreenMviModel :
sealed interface Intent {
object Refresh : Intent
object LoadNextPage : Intent
data class UpVotePost(val value: Boolean, val post: PostModel) : Intent
data class DownVotePost(val value: Boolean, val post: PostModel) : Intent
data class SavePost(val value: Boolean, val post: PostModel) : Intent
data class UpVoteComment(val value: Boolean, val comment: CommentModel) : Intent
data class DownVoteComment(val value: Boolean, val comment: CommentModel) : Intent
data class SaveComment(val value: Boolean, val comment: CommentModel) : Intent
}
data class UiState(
val post: PostModel = PostModel(),
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,

View File

@ -6,9 +6,11 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
@ -17,6 +19,7 @@ class PostDetailScreenViewModel(
private val mvi: DefaultMviModel<PostDetailScreenMviModel.Intent, PostDetailScreenMviModel.UiState, PostDetailScreenMviModel.Effect>,
private val post: PostModel,
private val identityRepository: IdentityRepository,
private val postsRepository: PostsRepository,
private val commentRepository: CommentRepository,
private val keyStore: TemporaryKeyStore,
) : MviModel<PostDetailScreenMviModel.Intent, PostDetailScreenMviModel.UiState, PostDetailScreenMviModel.Effect> by mvi,
@ -24,6 +27,7 @@ class PostDetailScreenViewModel(
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
mvi.updateState { it.copy(post = post) }
refresh()
}
@ -31,6 +35,35 @@ class PostDetailScreenViewModel(
when (intent) {
PostDetailScreenMviModel.Intent.LoadNextPage -> loadNextPage()
PostDetailScreenMviModel.Intent.Refresh -> refresh()
is PostDetailScreenMviModel.Intent.DownVoteComment -> downVoteComment(
intent.comment,
intent.value,
)
is PostDetailScreenMviModel.Intent.DownVotePost -> downVotePost(
intent.post,
intent.value,
)
is PostDetailScreenMviModel.Intent.SaveComment -> saveComment(
intent.comment,
intent.value,
)
is PostDetailScreenMviModel.Intent.SavePost -> savePost(
intent.post,
intent.value,
)
is PostDetailScreenMviModel.Intent.UpVoteComment -> upVoteComment(
intent.comment,
intent.value,
)
is PostDetailScreenMviModel.Intent.UpVotePost -> upVotePost(
intent.post,
intent.value,
)
}
}
@ -74,4 +107,106 @@ class PostDetailScreenViewModel(
}
}
}
private fun upVotePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.upVote(
auth = auth,
post = post,
voted = value,
)
mvi.updateState { it.copy(post = newPost) }
}
}
private fun downVotePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.downVote(
auth = auth,
post = post,
downVoted = value,
)
mvi.updateState { it.copy(post = newPost) }
}
}
private fun savePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.save(
auth = auth,
post = post,
saved = value,
)
mvi.updateState { it.copy(post = newPost) }
}
}
private fun upVoteComment(comment: CommentModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newComment = commentRepository.upVote(
auth = auth,
comment = comment,
voted = value,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
}
}
private fun downVoteComment(comment: CommentModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newComment = commentRepository.downVote(
auth = auth,
comment = comment,
downVoted = value,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
}
}
private fun saveComment(comment: CommentModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newComment = commentRepository.save(
auth = auth,
comment = comment,
saved = value,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
}
}
}

View File

@ -11,7 +11,7 @@ kotlin {
android {
compilations.all {
kotlinOptions {
jvmTarget = "17"
jvmTarget = "1.8"
}
}
}

View File

@ -4,4 +4,8 @@ data class CommentModel(
val id: Int = 0,
val text: String,
val community: CommunityModel? = null,
val creator: UserModel? = null,
val score: Int = 0,
val myVote: Int = 0,
val saved: Boolean = false,
)

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreateCommentLikeForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
@ -8,7 +10,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.to
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
class CommentRepository(
private val serviceProvider: ServiceProvider,
private val services: ServiceProvider,
) {
companion object {
const val DEFAULT_PAGE_SIZE = 20
@ -22,7 +24,7 @@ class CommentRepository(
type: ListingType = ListingType.All,
sort: SortType = SortType.Active,
): List<CommentModel> {
val response = serviceProvider.comment.getComments(
val response = services.comment.getAll(
auth = auth,
postId = postId,
page = page,
@ -33,4 +35,50 @@ class CommentRepository(
val dto = response.body()?.comments ?: emptyList()
return dto.map { it.toModel() }
}
suspend fun upVote(comment: CommentModel, auth: String, voted: Boolean): CommentModel {
val data = CreateCommentLikeForm(
commentId = comment.id,
score = if (voted) 1 else 0,
auth = auth,
)
services.comment.like(data)
return comment.copy(
myVote = if (voted) 1 else 0,
score = when {
voted && comment.myVote < 0 -> comment.score + 2
voted -> comment.score + 1
!voted -> comment.score - 1
else -> comment.score
},
)
}
suspend fun downVote(comment: CommentModel, auth: String, downVoted: Boolean): CommentModel {
val data = CreateCommentLikeForm(
commentId = comment.id,
score = if (downVoted) -1 else 0,
auth = auth,
)
services.comment.like(data)
return comment.copy(
myVote = if (downVoted) -1 else 0,
score = when {
downVoted && comment.myVote > 0 -> comment.score - 2
downVoted -> comment.score - 1
!downVoted -> comment.score + 1
else -> comment.score
},
)
}
suspend fun save(comment: CommentModel, auth: String, saved: Boolean): CommentModel {
val data = SaveCommentForm(
commentId = comment.id,
save = saved,
auth = auth,
)
services.comment.save(data)
return comment.copy(saved = saved)
}
}

View File

@ -24,7 +24,7 @@ class PostsRepository(
type: ListingType = ListingType.Local,
sort: SortType = SortType.Active,
): List<PostModel> {
val response = services.post.getPosts(
val response = services.post.getAll(
auth = auth,
page = page,
limit = limit,
@ -35,57 +35,49 @@ class PostsRepository(
return dto.map { it.toModel() }
}
suspend fun upVote(post: PostModel, auth: String) {
suspend fun upVote(post: PostModel, auth: String, voted: Boolean): PostModel {
val data = CreatePostLikeForm(
postId = post.id,
score = 1,
score = if (voted) 1 else 0,
auth = auth,
)
services.post.likePost(data)
services.post.like(data)
return post.copy(
myVote = if (voted) 1 else 0,
score = when {
voted && post.myVote < 0 -> post.score + 2
voted -> post.score + 1
!voted -> post.score - 1
else -> post.score
},
)
}
suspend fun undoUpVote(post: PostModel, auth: String) {
suspend fun downVote(post: PostModel, auth: String, downVoted: Boolean): PostModel {
val data = CreatePostLikeForm(
postId = post.id,
score = 0,
score = if (downVoted) -1 else 0,
auth = auth,
)
services.post.likePost(data)
}
suspend fun downVote(post: PostModel, auth: String) {
val data = CreatePostLikeForm(
postId = post.id,
score = -1,
auth = auth,
services.post.like(data)
return post.copy(
myVote = if (downVoted) -1 else 0,
score = when {
downVoted && post.myVote > 0 -> post.score - 2
downVoted -> post.score - 1
!downVoted -> post.score + 1
else -> post.score
},
)
services.post.likePost(data)
}
suspend fun undoDownVote(post: PostModel, auth: String) {
val data = CreatePostLikeForm(
postId = post.id,
score = 0,
auth = auth,
)
services.post.likePost(data)
}
suspend fun save(post: PostModel, auth: String) {
suspend fun save(post: PostModel, auth: String, saved: Boolean): PostModel {
val data = SavePostForm(
postId = post.id,
save = true,
save = saved,
auth = auth,
)
services.post.savePost(data)
}
suspend fun undoSave(post: PostModel, auth: String) {
val data = SavePostForm(
postId = post.id,
save = false,
auth = auth,
)
services.post.savePost(data)
services.post.save(data)
return post.copy(saved = saved)
}
}

View File

@ -79,6 +79,10 @@ internal fun CommentView.toModel() = CommentModel(
id = comment.id,
text = comment.content,
community = community.toModel(),
creator = creator.toModel(),
score = counts.score,
saved = saved,
myVote = myVote ?: 0,
)
internal fun Community.toModel() = CommunityModel(

View File

@ -30,10 +30,10 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
fun PostCard(
modifier: Modifier = Modifier,
post: PostModel,
onUpVote: (Boolean) -> Unit = {},
onDownVote: (Boolean) -> Unit = {},
onSave: (Boolean) -> Unit = {},
onReply: () -> Unit = {},
onUpVote: ((Boolean) -> Unit)? = null,
onDownVote: ((Boolean) -> Unit)? = null,
onSave: ((Boolean) -> Unit)? = null,
onReply: (() -> Unit)? = null,
) {
Card(
modifier = modifier
@ -57,7 +57,7 @@ fun PostCard(
Box {
PostCardBody(
modifier = Modifier.heightIn(max = 200.dp).padding(bottom = Spacing.xs),
post = post,
text = post.text,
)
Box(
modifier = Modifier
@ -75,7 +75,11 @@ fun PostCard(
)
}
PostCardFooter(
post = post,
comments = post.comments,
score = post.score,
upVoted = post.myVote > 0,
downVoted = post.myVote < 0,
saved = post.saved,
onUpVote = onUpVote,
onDownVote = onDownVote,
onSave = onSave,

View File

@ -117,30 +117,16 @@ class HomeScreenModel(
private fun upVote(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
if (value) {
postsRepository.upVote(
post = post,
auth = auth,
)
} else {
postsRepository.undoUpVote(
post = post,
auth = auth,
)
}
val newPost = postsRepository.upVote(
post = post,
auth = auth,
voted = value,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
p.copy(
myVote = if (value) 1 else 0,
score = when {
value && post.myVote < 0 -> p.score + 2
value -> p.score + 1
!value -> p.score - 1
else -> p.score
},
)
newPost
} else {
p
}
@ -153,30 +139,16 @@ class HomeScreenModel(
private fun downVote(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
if (value) {
postsRepository.downVote(
post = post,
auth = auth,
)
} else {
postsRepository.undoDownVote(
post = post,
auth = auth,
)
}
val newPost = postsRepository.downVote(
post = post,
auth = auth,
downVoted = value,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
p.copy(
myVote = if (value) -1 else 0,
score = when {
value && post.myVote > 0 -> p.score - 2
value -> p.score - 1
!value -> p.score + 1
else -> p.score
},
)
newPost
} else {
p
}
@ -189,22 +161,16 @@ class HomeScreenModel(
private fun save(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
if (value) {
postsRepository.save(
post = post,
auth = auth,
)
} else {
postsRepository.undoSave(
post = post,
auth = auth,
)
}
val newPost = postsRepository.save(
post = post,
auth = auth,
saved = value,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
p.copy(saved = value)
newPost
} else {
p
}

View File

@ -0,0 +1,52 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardFooter
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardSubtitle
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
@Composable
fun ProfileCommentCard(
comment: CommentModel,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(CornerSize.m),
).padding(
vertical = Spacing.lHalf,
horizontal = Spacing.s,
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
PostCardSubtitle(
community = comment.community,
)
PostCardBody(
text = comment.text,
)
PostCardFooter(
score = comment.score,
saved = comment.saved,
upVoted = comment.myVote > 0,
downVoted = comment.myVote < 0,
)
}
}
}

View File

@ -24,7 +24,6 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommentCard
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfileCommentsViewModel
@ -50,7 +49,9 @@ internal class ProfileCommentsScreen(
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
items(uiState.comments) { comment ->
CommentCard(comment)
ProfileCommentCard(
comment = comment,
)
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardFooter
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardImage
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardSubtitle
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
@ -55,7 +56,7 @@ fun ProfilePostCard(
Box {
PostCardBody(
modifier = Modifier.heightIn(max = 200.dp).padding(Spacing.xs),
post = post,
text = post.text,
)
Box(
modifier = Modifier
@ -72,6 +73,13 @@ fun ProfilePostCard(
),
)
}
PostCardFooter(
comments = post.comments,
score = post.score,
saved = post.saved,
upVoted = post.myVote > 0,
downVoted = post.myVote < 0,
)
}
}
}