From 70fadd93cff6e5c22bf586c4bb51d4e05d491ea7 Mon Sep 17 00:00:00 2001 From: Diego Beraldin Date: Fri, 24 Nov 2023 18:56:33 +0100 Subject: [PATCH] feat: mod tools (#153) * update services and DTOs * update repositories and models * feat: add remove bottom sheet * update community detail * update post detail * feat: show featured posts and distinguished comments * feat: report list * chore: translations --- .../core/api/dto/BanFromCommunityForm.kt | 15 + .../core/api/dto/BanFromCommunityResponse.kt | 10 + .../core/api/dto/CreateCommentReportForm.kt | 2 +- .../core/api/dto/DistinguishCommentForm.kt | 11 + .../core/api/dto/FeaturePostForm.kt | 12 + .../api/dto/ListCommentReportsResponse.kt | 9 + .../core/api/dto/ListPostReportsResponse.kt | 9 + .../core/api/dto/LockPostForm.kt | 11 + .../core/api/dto/PostFeatureType.kt | 11 + .../core/api/dto/RemoveCommentForm.kt | 12 + .../core/api/dto/RemovePostForm.kt | 12 + .../core/api/dto/ResolveCommentReportForm.kt | 11 + .../core/api/dto/ResolvePostReportForm.kt | 11 + .../core/api/service/CommentService.kt | 36 ++ .../core/api/service/CommunityService.kt | 9 + .../core/api/service/PostService.kt | 44 ++ .../raccoonforlemmy/core/commonui/di/Utils.kt | 24 +- .../CommunityDetailMviModel.kt | 3 + .../communitydetail/CommunityDetailScreen.kt | 130 +++-- .../CommunityDetailViewModel.kt | 61 ++- .../core/commonui/components/CommentCard.kt | 1 + .../components/CommunityAndCreatorInfo.kt | 30 +- .../core/commonui/components/Options.kt | 6 + .../core/commonui/components/PostCard.kt | 3 + .../core/commonui/di/CommonUiModule.kt | 34 +- .../raccoonforlemmy/core/commonui/di/Utils.kt | 14 +- .../commonui/modals/ReportListTypeSheet.kt | 103 ++++ .../commonui/postdetail/PostDetailMviModel.kt | 4 + .../commonui/postdetail/PostDetailScreen.kt | 86 ++- .../postdetail/PostDetailViewModel.kt | 157 +++--- .../core/commonui/remove/RemoveMviModel.kt | 29 ++ .../core/commonui/remove/RemoveScreen.kt | 163 ++++++ .../core/commonui/remove/RemoveViewModel.kt | 75 +++ .../commonui/reportlist/CommentReportCard.kt | 49 ++ .../commonui/reportlist/InnerReportCard.kt | 256 +++++++++ .../commonui/reportlist/PostReportCard.kt | 100 ++++ .../reportlist/ReportCardPlaceHolder.kt | 63 +++ .../commonui/reportlist/ReportListMviModel.kt | 46 ++ .../commonui/reportlist/ReportListScreen.kt | 490 ++++++++++++++++++ .../reportlist/ReportListViewModel.kt | 272 ++++++++++ .../raccoonforlemmy/core/commonui/di/Utils.kt | 36 +- .../notifications/NotificationCenterEvent.kt | 3 + .../domain/lemmy/data/CommentModel.kt | 2 + .../domain/lemmy/data/CommentReportModel.kt | 14 + .../domain/lemmy/data/PostModel.kt | 3 + .../domain/lemmy/data/PostReportModel.kt | 23 + .../lemmy/repository/CommentRepository.kt | 77 +++ .../lemmy/repository/CommunityRepository.kt | 16 + .../domain/lemmy/repository/PostRepository.kt | 97 ++++ .../domain/lemmy/repository/utils/Mappings.kt | 49 +- .../commonMain/resources/MR/base/strings.xml | 14 + .../commonMain/resources/MR/bg/strings.xml | 15 + .../commonMain/resources/MR/cs/strings.xml | 14 + .../commonMain/resources/MR/da/strings.xml | 14 + .../commonMain/resources/MR/de/strings.xml | 14 + .../commonMain/resources/MR/el/strings.xml | 15 + .../commonMain/resources/MR/es/strings.xml | 14 + .../commonMain/resources/MR/et/strings.xml | 14 + .../commonMain/resources/MR/fi/strings.xml | 14 + .../commonMain/resources/MR/fr/strings.xml | 14 + .../commonMain/resources/MR/ga/strings.xml | 14 + .../commonMain/resources/MR/hr/strings.xml | 14 + .../commonMain/resources/MR/hu/strings.xml | 16 + .../commonMain/resources/MR/it/strings.xml | 14 + .../commonMain/resources/MR/lt/strings.xml | 14 + .../commonMain/resources/MR/lv/strings.xml | 14 + .../commonMain/resources/MR/nl/strings.xml | 14 + .../commonMain/resources/MR/no/strings.xml | 14 + .../commonMain/resources/MR/pl/strings.xml | 14 + .../commonMain/resources/MR/pt/strings.xml | 14 + .../commonMain/resources/MR/ro/strings.xml | 14 + .../commonMain/resources/MR/se/strings.xml | 14 + .../commonMain/resources/MR/sk/strings.xml | 14 + .../commonMain/resources/MR/sl/strings.xml | 14 + 74 files changed, 2955 insertions(+), 129 deletions(-) create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityResponse.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/DistinguishCommentForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/FeaturePostForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListCommentReportsResponse.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListPostReportsResponse.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/LockPostForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/PostFeatureType.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemoveCommentForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemovePostForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolveCommentReportForm.kt create mode 100644 core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolvePostReportForm.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/modals/ReportListTypeSheet.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveMviModel.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveScreen.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveViewModel.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/CommentReportCard.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/InnerReportCard.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/PostReportCard.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/ReportCardPlaceHolder.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/ReportListMviModel.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/ReportListScreen.kt create mode 100644 core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/ReportListViewModel.kt create mode 100644 domain-lemmy/data/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/lemmy/data/CommentReportModel.kt create mode 100644 domain-lemmy/data/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/lemmy/data/PostReportModel.kt diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityForm.kt new file mode 100644 index 000000000..e7ad395f4 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityForm.kt @@ -0,0 +1,15 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BanFromCommunityForm( + @SerialName("community_id") val communityId: CommunityId, + @SerialName("personId") val personId: PersonId, + @SerialName("ban") val ban: Boolean, + @SerialName("remove_data") val removeData: Boolean, + @SerialName("reason") val reson: String?, + @SerialName("expires") val expires: Long?, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityResponse.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityResponse.kt new file mode 100644 index 000000000..6ded14958 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/BanFromCommunityResponse.kt @@ -0,0 +1,10 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BanFromCommunityResponse( + @SerialName("person_view") val personView: PersonView, + @SerialName("banned") val banned: Boolean, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/CreateCommentReportForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/CreateCommentReportForm.kt index 9075d9aac..37343eab4 100644 --- a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/CreateCommentReportForm.kt +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/CreateCommentReportForm.kt @@ -6,6 +6,6 @@ import kotlinx.serialization.Serializable @Serializable data class CreateCommentReportForm( @SerialName("comment_id") val commentId: CommentId, - @SerialName("reason") val reason: String, + @SerialName("reason") val reason: String?, @SerialName("auth") val auth: String, ) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/DistinguishCommentForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/DistinguishCommentForm.kt new file mode 100644 index 000000000..640e68d4f --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/DistinguishCommentForm.kt @@ -0,0 +1,11 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DistinguishCommentForm( + @SerialName("comment_id") val commentId: CommentId, + @SerialName("distinguished") val distinguished: Boolean, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/FeaturePostForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/FeaturePostForm.kt new file mode 100644 index 000000000..6b3798cb7 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/FeaturePostForm.kt @@ -0,0 +1,12 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FeaturePostForm( + @SerialName("post_id") val postId: PostId, + @SerialName("auth") val auth: String, + @SerialName("featured") val featured: Boolean, + @SerialName("feature_type") val featureType: PostFeatureType, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListCommentReportsResponse.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListCommentReportsResponse.kt new file mode 100644 index 000000000..b621f904c --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListCommentReportsResponse.kt @@ -0,0 +1,9 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ListCommentReportsResponse( + @SerialName("comment_reports") val commentReports: List, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListPostReportsResponse.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListPostReportsResponse.kt new file mode 100644 index 000000000..cb3b0dfd7 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ListPostReportsResponse.kt @@ -0,0 +1,9 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ListPostReportsResponse( + @SerialName("post_reports") val postReports: List, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/LockPostForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/LockPostForm.kt new file mode 100644 index 000000000..cada57dd8 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/LockPostForm.kt @@ -0,0 +1,11 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LockPostForm( + @SerialName("post_id") val postId: PostId, + @SerialName("locked") val locked: Boolean, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/PostFeatureType.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/PostFeatureType.kt new file mode 100644 index 000000000..2eca07878 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/PostFeatureType.kt @@ -0,0 +1,11 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName + +enum class PostFeatureType { + @SerialName("Local") + Local, + + @SerialName("Community") + Community, +} diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemoveCommentForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemoveCommentForm.kt new file mode 100644 index 000000000..cfc19d4ee --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemoveCommentForm.kt @@ -0,0 +1,12 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RemoveCommentForm( + @SerialName("comment_id") val commentId: CommentId, + @SerialName("reason") val reason: String?, + @SerialName("removed") val removed: Boolean, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemovePostForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemovePostForm.kt new file mode 100644 index 000000000..35c588f0d --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/RemovePostForm.kt @@ -0,0 +1,12 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RemovePostForm( + @SerialName("post_id") val postId: PostId, + @SerialName("reason") val reason: String?, + @SerialName("removed") val removed: Boolean, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolveCommentReportForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolveCommentReportForm.kt new file mode 100644 index 000000000..816e38b28 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolveCommentReportForm.kt @@ -0,0 +1,11 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResolveCommentReportForm( + @SerialName("report_id") val reportId: CommentReportId, + @SerialName("resolved") val resolved: Boolean, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolvePostReportForm.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolvePostReportForm.kt new file mode 100644 index 000000000..33ad30168 --- /dev/null +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/dto/ResolvePostReportForm.kt @@ -0,0 +1,11 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResolvePostReportForm( + @SerialName("report_id") val reportId: PostReportId, + @SerialName("resolved") val resolved: Boolean, + @SerialName("auth") val auth: String, +) diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommentService.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommentService.kt index 6afd214ba..69d6e0bcd 100644 --- a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommentService.kt +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommentService.kt @@ -8,11 +8,15 @@ 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.CreateCommentReportForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.DeleteCommentForm +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.DistinguishCommentForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditCommentForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetCommentResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetCommentsResponse +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListCommentReportsResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkCommentAsReadForm +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.RemoveCommentForm +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ResolveCommentReportForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm import de.jensklingenberg.ktorfit.Response import de.jensklingenberg.ktorfit.http.Body @@ -95,4 +99,36 @@ interface CommentService { @Body form: CreateCommentReportForm, @Header("Authorization") authHeader: String? = null, ): Response + + @POST("comment/remove") + @Headers("Content-Type: application/json") + suspend fun remove( + @Header("Authorization") authHeader: String? = null, + @Body form: RemoveCommentForm, + ): Response + + @POST("comment/distinguish") + @Headers("Content-Type: application/json") + suspend fun distinguish( + @Header("Authorization") authHeader: String? = null, + @Body form: DistinguishCommentForm, + ): Response + + @GET("comment/report/list") + @Headers("Content-Type: application/json") + suspend fun listReports( + @Header("Authorization") authHeader: String? = null, + @Query("auth") auth: String? = null, + @Query("limit") limit: Int? = null, + @Query("page") page: Int? = null, + @Query("unresolved_only") unresolvedOnly: Boolean? = null, + @Query("community_id") communityId: Int? = null, + ): Response + + @PUT("comment/report/resolve") + @Headers("Content-Type: application/json") + suspend fun resolveReport( + @Header("Authorization") authHeader: String? = null, + @Body form: ResolveCommentReportForm, + ): Response } diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommunityService.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommunityService.kt index c52469c35..e994e1d38 100644 --- a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommunityService.kt +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/CommunityService.kt @@ -1,5 +1,7 @@ package com.github.diegoberaldin.raccoonforlemmy.core.api.service +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BanFromCommunityForm +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BanFromCommunityResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BlockCommunityForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.BlockCommunityResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommunityResponse @@ -48,4 +50,11 @@ interface CommunityService { @Header("Authorization") authHeader: String? = null, @Body form: BlockCommunityForm, ): Response + + @POST("community/ban_user") + @Headers("Content-Type: application/json") + suspend fun ban( + @Header("Authorization") authHeader: String? = null, + @Body form: BanFromCommunityForm, + ): Response } diff --git a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/PostService.kt b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/PostService.kt index 436f51c47..3b5ef8670 100644 --- a/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/PostService.kt +++ b/core-api/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/api/service/PostService.kt @@ -5,13 +5,18 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostLikeForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostReportForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.DeletePostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditPostForm +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.FeaturePostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetPostResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetPostsResponse +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListPostReportsResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.LockPostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPostAsReadForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PictrsImages import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostReportResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostResponse +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.RemovePostForm +import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ResolvePostReportForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SavePostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType import de.jensklingenberg.ktorfit.Response @@ -105,4 +110,43 @@ interface PostService { @Header("Authorization") authHeader: String? = null, @Body form: CreatePostReportForm, ): Response + + @POST("post/feature") + @Headers("Content-Type: application/json") + suspend fun feature( + @Header("Authorization") authHeader: String? = null, + @Body form: FeaturePostForm, + ): Response + + @POST("post/remove") + @Headers("Content-Type: application/json") + suspend fun remove( + @Header("Authorization") authHeader: String? = null, + @Body form: RemovePostForm, + ): Response + + @POST("post/lock") + @Headers("Content-Type: application/json") + suspend fun lock( + @Header("Authorization") authHeader: String? = null, + @Body form: LockPostForm, + ): Response + + @GET("post/report/list") + @Headers("Content-Type: application/json") + suspend fun listReports( + @Header("Authorization") authHeader: String? = null, + @Query("auth") auth: String? = null, + @Query("limit") limit: Int? = null, + @Query("page") page: Int? = null, + @Query("unresolved_only") unresolvedOnly: Boolean? = null, + @Query("community_id") communityId: Int? = null, + ): Response + + @PUT("post/report/resolve") + @Headers("Content-Type: application/json") + suspend fun resolveReport( + @Header("Authorization") authHeader: String? = null, + @Body form: ResolvePostReportForm, + ): Response } diff --git a/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt b/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt index 03600ca44..b1a1ebaed 100644 --- a/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt +++ b/core-commonui/src/androidMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt @@ -19,6 +19,8 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImag import com.github.diegoberaldin.raccoonforlemmy.core.commonui.instanceinfo.InstanceInfoMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.NavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove.RemoveMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist.ReportListMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.selectcommunity.SelectCommunityMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel @@ -56,10 +58,11 @@ actual fun getPostDetailViewModel( post: PostModel, otherInstance: String, highlightCommentId: Int?, + isModerator: Boolean, ): PostDetailMviModel { val res: PostDetailMviModel by inject( clazz = PostDetailMviModel::class.java, - parameters = { parametersOf(post, otherInstance, highlightCommentId) }, + parameters = { parametersOf(post, otherInstance, highlightCommentId, isModerator) }, ) return res } @@ -168,4 +171,23 @@ actual fun getCustomTextToolbar( onShare = onShare, onQuote = onQuote, ) +} + +actual fun getRemoveViewModel( + postId: Int?, + commentId: Int?, +): RemoveMviModel { + val res: RemoveMviModel by inject(RemoveMviModel::class.java, parameters = { + parametersOf(postId, commentId) + }) + return res +} + +actual fun getReportListViewModel( + communityId: Int, +): ReportListMviModel { + val res: ReportListMviModel by inject(ReportListMviModel::class.java, parameters = { + parametersOf(communityId) + }) + return res } \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailMviModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailMviModel.kt index e291c251c..85bdda461 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailMviModel.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailMviModel.kt @@ -32,6 +32,8 @@ interface CommunityDetailMviModel : data object ClearRead : Intent data class StartZombieMode(val index: Int) : Intent data object PauseZombieMode : Intent + data class ModFeaturePost(val id: Int) : Intent + data class ModLockPost(val id: Int) : Intent } data class UiState( @@ -52,6 +54,7 @@ interface CommunityDetailMviModel : val separateUpAndDownVotes: Boolean = false, val autoLoadImages: Boolean = true, val zombieModeActive: Boolean = false, + val isModerator: Boolean = false, ) sealed interface Effect { diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailScreen.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailScreen.kt index d1277b294..022332d71 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailScreen.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailScreen.kt @@ -96,6 +96,8 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.instanceinfo.Insta import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.RawContentDialog import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove.RemoveScreen +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist.ReportListScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter @@ -252,24 +254,31 @@ class CommunityDetailScreen( // options menu Box { - val options = listOf( - Option( + val options = buildList { + this += Option( OptionId.Info, stringResource(MR.strings.community_detail_info) - ), - Option( + ) + this += Option( OptionId.InfoInstance, stringResource(MR.strings.community_detail_instance_info) - ), - Option( + ) + this += Option( OptionId.Block, stringResource(MR.strings.community_detail_block) - ), - Option( + ) + this += Option( OptionId.BlockInstance, stringResource(MR.strings.community_detail_block_instance) - ), - ) + ) + + if (uiState.isModerator) { + this += Option( + OptionId.OpenReports, + stringResource(MR.strings.mod_action_open_reports) + ) + } + } var optionsExpanded by remember { mutableStateOf(false) } var optionsOffset by remember { mutableStateOf(Offset.Zero) } Image( @@ -325,6 +334,13 @@ class CommunityDetailScreen( ) } + OptionId.OpenReports -> { + val screen = ReportListScreen( + communityId = uiState.community.id + ) + navigationCoordinator.pushScreen(screen) + } + else -> Unit } }, @@ -545,6 +561,7 @@ class CommunityDetailScreen( PostDetailScreen( post = post, otherInstance = otherInstanceName, + isMod = uiState.isModerator, ), ) }, @@ -617,52 +634,60 @@ class CommunityDetailScreen( ) }, options = buildList { - add( - Option( - OptionId.Share, - stringResource(MR.strings.post_action_share) - ) + this += Option( + OptionId.Share, + stringResource(MR.strings.post_action_share), ) if (uiState.isLogged && !isOnOtherInstance) { - add( - Option( - OptionId.Hide, - stringResource(MR.strings.post_action_hide) - ) + this += Option( + OptionId.Hide, + stringResource(MR.strings.post_action_hide), ) } - add( - Option( - OptionId.SeeRaw, - stringResource(MR.strings.post_action_see_raw) - ) + this += Option( + OptionId.SeeRaw, + stringResource(MR.strings.post_action_see_raw), ) if (uiState.isLogged && !isOnOtherInstance) { - add( - Option( - OptionId.CrossPost, - stringResource(MR.strings.post_action_cross_post) - ) + this += Option( + OptionId.CrossPost, + stringResource(MR.strings.post_action_cross_post) ) - add( - Option( - OptionId.Report, - stringResource(MR.strings.post_action_report) - ) + this += Option( + OptionId.Report, + stringResource(MR.strings.post_action_report), ) } if (post.creator?.id == uiState.currentUserId && !isOnOtherInstance) { - add( - Option( - OptionId.Edit, - stringResource(MR.strings.post_action_edit) - ) + this += Option( + OptionId.Edit, + stringResource(MR.strings.post_action_edit), ) - add( - Option( - OptionId.Delete, - stringResource(MR.strings.comment_action_delete) - ) + this += Option( + OptionId.Delete, + stringResource(MR.strings.comment_action_delete), + ) + } + if (uiState.isModerator) { + this += Option( + OptionId.FeaturePost, + if (post.featuredCommunity) { + stringResource(MR.strings.mod_action_unmark_as_featured) + } else { + stringResource(MR.strings.mod_action_mark_as_featured) + }, + ) + this += Option( + OptionId.LockPost, + if (post.locked) { + stringResource(MR.strings.mod_action_unlock) + } else { + stringResource(MR.strings.mod_action_lock) + }, + ) + this += Option( + OptionId.Remove, + stringResource(MR.strings.mod_action_remove), ) } }, @@ -702,6 +727,21 @@ class CommunityDetailScreen( CommunityDetailMviModel.Intent.SharePost(post.id) ) + OptionId.FeaturePost -> model.reduce( + CommunityDetailMviModel.Intent.ModFeaturePost( + post.id + ) + ) + + OptionId.LockPost -> model.reduce( + CommunityDetailMviModel.Intent.ModLockPost(post.id) + ) + + OptionId.Remove -> { + val screen = RemoveScreen(postId = post.id) + navigationCoordinator.showBottomSheet(screen) + } + else -> Unit } }) diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailViewModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailViewModel.kt index bb6192186..33c2bb90f 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailViewModel.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/communitydetail/CommunityDetailViewModel.kt @@ -86,6 +86,17 @@ class CommunityDetailViewModel( notificationCenter.subscribe(NotificationCenterEvent.PostUpdated::class).onEach { evt -> handlePostUpdate(evt.model) }.launchIn(this) + notificationCenter.subscribe(NotificationCenterEvent.PostRemoved::class).onEach { evt -> + handlePostDelete(evt.model.id) + }.launchIn(this) + notificationCenter.subscribe(NotificationCenterEvent.CommentRemoved::class) + .onEach { evt -> + val postId = evt.model.postId + uiState.value.posts.firstOrNull { it.id == postId }?.also { + val newPost = it.copy(comments = (it.comments - 1).coerceAtLeast(0)) + handlePostUpdate(newPost) + } + }.launchIn(this) if (uiState.value.currentUserId == null) { val user = siteRepository.getCurrentUser(auth) @@ -167,6 +178,16 @@ class CommunityDetailViewModel( interval = settingsRepository.currentSettings.value.zombieModeInterval, ) } + + is CommunityDetailMviModel.Intent.ModFeaturePost -> uiState.value.posts.firstOrNull { it.id == intent.id } + ?.also { post -> + feature(post = post) + } + + is CommunityDetailMviModel.Intent.ModLockPost -> uiState.value.posts.firstOrNull { it.id == intent.id } + ?.also { post -> + lock(post = post) + } } } @@ -190,9 +211,19 @@ class CommunityDetailViewModel( name = community.name, ) } + val isModerator = communityRepository.getModerators( + auth = auth, + id = community.id, + ).any { it.id == uiState.value.currentUserId } if (refreshedCommunity != null) { - mvi.updateState { it.copy(community = refreshedCommunity) } + mvi.updateState { + it.copy( + community = refreshedCommunity, + isModerator = isModerator, + ) + } } + loadNextPage() } } @@ -581,4 +612,32 @@ class CommunityDetailViewModel( } markAsRead(post) } + + private fun feature(post: PostModel) { + mvi.scope?.launch(Dispatchers.IO) { + val auth = identityRepository.authToken.value.orEmpty() + val newPost = postRepository.featureInCommunity( + postId = post.id, + auth = auth, + featured = !post.featuredCommunity + ) + if (newPost != null) { + handlePostUpdate(newPost) + } + } + } + + private fun lock(post: PostModel) { + mvi.scope?.launch(Dispatchers.IO) { + val auth = identityRepository.authToken.value.orEmpty() + val newPost = postRepository.lock( + postId = post.id, + auth = auth, + locked = !post.locked, + ) + if (newPost != null) { + handlePostUpdate(newPost) + } + } + } } diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommentCard.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommentCard.kt index cb894e07e..c23da1b3e 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommentCard.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommentCard.kt @@ -91,6 +91,7 @@ fun CommentCard( onOpenCreator = onOpenCreator, onOpenCommunity = onOpenCommunity, onToggleExpanded = onToggleExpanded, + distinguished = comment.distinguished, ) ScaledContent { PostCardBody( diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommunityAndCreatorInfo.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommunityAndCreatorInfo.kt index 90b555607..fd1d47929 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommunityAndCreatorInfo.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/CommunityAndCreatorInfo.kt @@ -10,6 +10,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.PushPin +import androidx.compose.material.icons.filled.WorkspacePremium import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -20,6 +23,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick @@ -35,6 +39,9 @@ fun CommunityAndCreatorInfo( autoLoadImages: Boolean = true, community: CommunityModel? = null, creator: UserModel? = null, + distinguished: Boolean = false, + featured: Boolean = false, + locked: Boolean = false, onOpenCommunity: ((CommunityModel) -> Unit)? = null, onOpenCreator: ((UserModel) -> Unit)? = null, onToggleExpanded: (() -> Unit)? = null, @@ -163,8 +170,29 @@ fun CommunityAndCreatorInfo( ) } } + Spacer(modifier = Modifier.weight(1f)) + val buttonModifier = Modifier.size(IconSize.m).padding(3.5.dp) + if (distinguished) { + Icon( + modifier = buttonModifier, + imageVector = Icons.Default.WorkspacePremium, + contentDescription = null, + ) + } else if (featured) { + Icon( + modifier = buttonModifier, + imageVector = Icons.Default.PushPin, + contentDescription = null, + ) + } + if (locked) { + Icon( + modifier = buttonModifier, + imageVector = Icons.Default.Lock, + contentDescription = null, + ) + } if (indicatorExpanded != null) { - Spacer(modifier = Modifier.weight(1f)) val expandedModifier = Modifier .padding(end = Spacing.xs) .onClick( diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/Options.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/Options.kt index 4f8ea2939..20ebff1cb 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/Options.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/Options.kt @@ -19,4 +19,10 @@ sealed class OptionId(val value: Int) { data object BlockInstance : OptionId(10) data object MarkRead : OptionId(11) data object MarkUnread : OptionId(12) + data object FeaturePost : OptionId(13) + data object LockPost : OptionId(14) + data object Remove : OptionId(15) + data object DistinguishComment : OptionId(16) + data object OpenReports : OptionId(17) + data object ResolveReport : OptionId(18) } \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCard.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCard.kt index bb1fa9276..939007cf4 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCard.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/components/PostCard.kt @@ -160,6 +160,7 @@ private fun CompactPost( CommunityAndCreatorInfo( community = post.community, creator = post.creator.takeIf { !hideAuthor }, + featured = post.featuredCommunity, onOpenCommunity = onOpenCommunity, onOpenCreator = onOpenCreator, autoLoadImages = autoLoadImages, @@ -248,6 +249,8 @@ private fun ExtendedPost( modifier = Modifier.padding(horizontal = Spacing.xxs), community = post.community, creator = post.creator.takeIf { !hideAuthor }, + featured = post.featuredCommunity, + locked = post.locked, onOpenCommunity = onOpenCommunity, onOpenCreator = onOpenCreator, autoLoadImages = autoLoadImages, diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/CommonUiModule.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/CommonUiModule.kt index 052168da4..1d68e7c80 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/CommonUiModule.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/CommonUiModule.kt @@ -27,6 +27,10 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.Default import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.NavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailViewModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove.RemoveMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove.RemoveViewModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist.ReportListMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist.ReportListViewModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsViewModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.selectcommunity.SelectCommunityMviModel @@ -57,6 +61,7 @@ val commonUiModule = module { post = params[0], otherInstance = params[1], highlightCommentId = params[2], + isModerator = params[3], identityRepository = get(), siteRepository = get(), postRepository = get(), @@ -191,10 +196,10 @@ val commonUiModule = module { settingsRepository = get(), ) } - factory { + factory { params -> CreateReportViewModel( - postId = it[0], - commentId = it[1], + postId = params[0], + commentId = params[1], mvi = DefaultMviModel(CreateReportMviModel.UiState()), identityRepository = get(), postRepository = get(), @@ -210,4 +215,27 @@ val commonUiModule = module { notificationCenter = get(), ) } + factory { params -> + RemoveViewModel( + postId = params[0], + commentId = params[1], + mvi = DefaultMviModel(RemoveMviModel.UiState()), + identityRepository = get(), + postRepository = get(), + commentRepository = get(), + notificationCenter = get(), + ) + } + factory { params -> + ReportListViewModel( + communityId = params[0], + mvi = DefaultMviModel(ReportListMviModel.UiState()), + identityRepository = get(), + postRepository = get(), + commentRepository = get(), + themeRepository = get(), + settingsRepository = get(), + hapticFeedback = get(), + ) + } } diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt index c19333699..4e9e0e243 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/di/Utils.kt @@ -15,6 +15,8 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImag import com.github.diegoberaldin.raccoonforlemmy.core.commonui.instanceinfo.InstanceInfoMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.NavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove.RemoveMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist.ReportListMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.selectcommunity.SelectCommunityMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel @@ -35,6 +37,7 @@ expect fun getPostDetailViewModel( post: PostModel, otherInstance: String = "", highlightCommentId: Int? = null, + isModerator: Boolean = false, ): PostDetailMviModel expect fun getCommunityDetailViewModel( @@ -84,4 +87,13 @@ expect fun getCustomTextToolbar( onQuote: () -> Unit, ): TextToolbar -expect fun getSelectCommunityViewModel(): SelectCommunityMviModel \ No newline at end of file +expect fun getSelectCommunityViewModel(): SelectCommunityMviModel + +expect fun getRemoveViewModel( + postId: Int? = null, + commentId: Int? = null, +): RemoveMviModel + +expect fun getReportListViewModel( + communityId: Int, +): ReportListMviModel \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/modals/ReportListTypeSheet.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/modals/ReportListTypeSheet.kt new file mode 100644 index 000000000..1bec129ac --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/modals/ReportListTypeSheet.kt @@ -0,0 +1,103 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals + +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.core.screen.Screen +import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.BottomSheetHandle +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator +import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent +import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter +import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick +import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback +import com.github.diegoberaldin.raccoonforlemmy.resources.MR +import dev.icerock.moko.resources.compose.stringResource + +class ReportListTypeSheet : Screen { + @Composable + override fun Content() { + val navigationCoordinator = remember { getNavigationCoordinator() } + val notificationCenter = remember { getNotificationCenter() } + + Column( + modifier = Modifier + .padding( + top = Spacing.s, + start = Spacing.s, + end = Spacing.s, + bottom = Spacing.m, + ), + verticalArrangement = Arrangement.spacedBy(Spacing.s), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + BottomSheetHandle() + Text( + modifier = Modifier.padding(start = Spacing.s, top = Spacing.s), + text = stringResource(MR.strings.report_list_type_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + Column( + modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(Spacing.xxxs), + ) { + Row( + modifier = Modifier.padding( + horizontal = Spacing.s, + vertical = Spacing.m, + ) + .fillMaxWidth() + .onClick( + onClick = rememberCallback { + notificationCenter.send( + NotificationCenterEvent.ChangeReportListType(true) + ) + navigationCoordinator.hideBottomSheet() + }, + ), + ) { + Text( + text = stringResource(MR.strings.report_list_type_unresolved), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = Spacing.s, + vertical = Spacing.m, + ).onClick( + onClick = rememberCallback { + notificationCenter.send( + NotificationCenterEvent.ChangeReportListType(false) + ) + navigationCoordinator.hideBottomSheet() + }, + ), + ) { + Text( + text = stringResource(MR.strings.report_list_type_all), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } + } + } + } +} diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailMviModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailMviModel.kt index 3babfb474..534eb62ec 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailMviModel.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailMviModel.kt @@ -30,10 +30,14 @@ interface PostDetailMviModel : data object DeletePost : Intent data object HapticIndication : Intent data object SharePost : Intent + data object ModFeaturePost : Intent + data object ModLockPost : Intent + data class ModDistinguishComment(val commentId: Int) : Intent } data class UiState( val post: PostModel = PostModel(), + val isModerator: Boolean = false, val isLogged: Boolean = false, val refreshing: Boolean = false, val loading: Boolean = false, diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailScreen.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailScreen.kt index fb98a0a08..a1e3631d9 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailScreen.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailScreen.kt @@ -95,6 +95,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getPostDetailVi import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.RawContentDialog import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove.RemoveScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter @@ -116,6 +117,7 @@ class PostDetailScreen( private val post: PostModel, private val otherInstance: String = "", private val highlightCommentId: Int? = null, + private val isMod: Boolean = false, ) : Screen { @OptIn( @@ -132,6 +134,7 @@ class PostDetailScreen( post = post, highlightCommentId = highlightCommentId, otherInstance = otherInstance, + isModerator = isMod, ) } model.bindToLifecycle(key + post.id.toString()) @@ -404,6 +407,34 @@ class PostDetailScreen( ) ) } + if (uiState.isModerator) { + add( + Option( + OptionId.FeaturePost, + if (uiState.post.featuredCommunity) { + stringResource(MR.strings.mod_action_unmark_as_featured) + } else { + stringResource(MR.strings.mod_action_mark_as_featured) + } + ) + ) + add( + Option( + OptionId.LockPost, + if (uiState.post.locked) { + stringResource(MR.strings.mod_action_unlock) + } else { + stringResource(MR.strings.mod_action_lock) + } + ) + ) + add( + Option( + OptionId.Remove, + stringResource(MR.strings.mod_action_remove) + ) + ) + } }, onOptionSelected = rememberCallbackArgs(model) { idx -> when (idx) { @@ -433,6 +464,19 @@ class PostDetailScreen( OptionId.Share -> model.reduce(PostDetailMviModel.Intent.SharePost) + OptionId.FeaturePost -> model.reduce( + PostDetailMviModel.Intent.ModFeaturePost + ) + + OptionId.LockPost -> model.reduce( + PostDetailMviModel.Intent.ModLockPost + ) + + OptionId.Remove -> { + val screen = RemoveScreen(postId = uiState.post.id) + navigationCoordinator.showBottomSheet(screen) + } + else -> Unit } }, @@ -507,7 +551,7 @@ class PostDetailScreen( } items( uiState.comments.filter { it.visible }, - key = { c -> c.id }) { comment -> + key = { c -> c.id.toString() + c.updateDate }) { comment -> Column { AnimatedContent( targetState = comment.expanded, @@ -672,26 +716,44 @@ class PostDetailScreen( add( Option( OptionId.SeeRaw, - stringResource(MR.strings.post_action_see_raw) + stringResource(MR.strings.post_action_see_raw), ) ) add( Option( OptionId.Report, - stringResource(MR.strings.post_action_report) + stringResource(MR.strings.post_action_report), ) ) if (comment.creator?.id == uiState.currentUserId) { add( Option( OptionId.Edit, - stringResource(MR.strings.post_action_edit) + stringResource(MR.strings.post_action_edit), ) ) add( Option( OptionId.Delete, - stringResource(MR.strings.comment_action_delete) + stringResource(MR.strings.comment_action_delete), + ) + ) + } + if (uiState.isModerator) { + add( + Option( + OptionId.DistinguishComment, + if (comment.distinguished) { + stringResource(MR.strings.mod_action_unmark_as_distinguished) + } else { + stringResource(MR.strings.mod_action_mark_as_distinguished) + }, + ) + ) + add( + Option( + OptionId.Remove, + stringResource(MR.strings.mod_action_remove), ) ) } @@ -726,6 +788,20 @@ class PostDetailScreen( rawContent = comment } + OptionId.DistinguishComment -> model.reduce( + PostDetailMviModel.Intent.ModDistinguishComment( + comment.id + ) + ) + + OptionId.Remove -> { + val screen = + RemoveScreen(commentId = comment.id) + navigationCoordinator.showBottomSheet( + screen + ) + } + else -> Unit } }, diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailViewModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailViewModel.kt index da19e7fc0..d10a710e0 100644 --- a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailViewModel.kt +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/postdetail/PostDetailViewModel.kt @@ -27,6 +27,7 @@ class PostDetailViewModel( private val post: PostModel, private val otherInstance: String, private val highlightCommentId: Int?, + private val isModerator: Boolean, private val identityRepository: IdentityRepository, private val siteRepository: SiteRepository, private val postRepository: PostRepository, @@ -67,6 +68,13 @@ class PostDetailViewModel( } }.launchIn(this) + notificationCenter.subscribe(NotificationCenterEvent.PostRemoved::class).onEach { evt -> + mvi.emitEffect(PostDetailMviModel.Effect.Close) + }.launchIn(this) + notificationCenter.subscribe(NotificationCenterEvent.CommentRemoved::class) + .onEach { evt -> + handleCommentDelete(evt.model.id) + }.launchIn(this) } mvi.scope?.launch(Dispatchers.IO) { @@ -82,7 +90,10 @@ class PostDetailViewModel( } mvi.updateState { - it.copy(post = post) + it.copy( + post = post, + isModerator = isModerator, + ) } val auth = identityRepository.authToken.value @@ -105,6 +116,9 @@ class PostDetailViewModel( ) highlightCommentPath = comment?.path } + if (post.text.isEmpty() && post.title.isEmpty()) { + refreshPost() + } if (mvi.uiState.value.comments.isEmpty()) { refresh() } @@ -214,6 +228,13 @@ class PostDetailViewModel( toggleExpanded(comment) } } + + PostDetailMviModel.Intent.ModFeaturePost -> feature(uiState.value.post) + PostDetailMviModel.Intent.ModLockPost -> lock(uiState.value.post) + is PostDetailMviModel.Intent.ModDistinguishComment -> uiState.value.comments.firstOrNull { it.id == intent.commentId } + ?.also { comment -> + distinguish(comment) + } } } @@ -450,6 +471,20 @@ class PostDetailViewModel( } } + private fun handleCommentUpdate(comment: CommentModel) { + mvi.updateState { + it.copy( + comments = it.comments.map { c -> + if (c.id == comment.id) { + comment + } else { + c + } + }, + ) + } + } + private fun toggleUpVoteComment( comment: CommentModel, feedback: Boolean, @@ -462,17 +497,7 @@ class PostDetailViewModel( comment = comment, voted = newValue, ) - mvi.updateState { - it.copy( - comments = it.comments.map { c -> - if (c.id == comment.id) { - newComment - } else { - c - } - }, - ) - } + handleCommentUpdate(newComment) mvi.scope?.launch(Dispatchers.IO) { try { val auth = identityRepository.authToken.value.orEmpty() @@ -486,17 +511,7 @@ class PostDetailViewModel( ) } catch (e: Throwable) { e.printStackTrace() - mvi.updateState { - it.copy( - comments = it.comments.map { c -> - if (c.id == comment.id) { - comment - } else { - c - } - }, - ) - } + handleCommentUpdate(comment) } } } @@ -510,17 +525,7 @@ class PostDetailViewModel( hapticFeedback.vibrate() } val newComment = commentRepository.asDownVoted(comment, newValue) - mvi.updateState { - it.copy( - comments = it.comments.map { c -> - if (c.id == comment.id) { - newComment - } else { - c - } - }, - ) - } + handleCommentUpdate(newComment) mvi.scope?.launch(Dispatchers.IO) { try { val auth = identityRepository.authToken.value.orEmpty() @@ -534,17 +539,7 @@ class PostDetailViewModel( ) } catch (e: Throwable) { e.printStackTrace() - mvi.updateState { - it.copy( - comments = it.comments.map { c -> - if (c.id == comment.id) { - comment - } else { - c - } - }, - ) - } + handleCommentUpdate(comment) } } } @@ -561,17 +556,7 @@ class PostDetailViewModel( comment = comment, saved = newValue, ) - mvi.updateState { - it.copy( - comments = it.comments.map { c -> - if (c.id == comment.id) { - newComment - } else { - c - } - }, - ) - } + handleCommentUpdate(newComment) mvi.scope?.launch(Dispatchers.IO) { try { val auth = identityRepository.authToken.value.orEmpty() @@ -585,17 +570,7 @@ class PostDetailViewModel( ) } catch (e: Throwable) { e.printStackTrace() - mvi.updateState { - it.copy( - comments = it.comments.map { c -> - if (c.id == comment.id) { - comment - } else { - c - } - }, - ) - } + handleCommentUpdate(comment) } } } @@ -604,11 +579,15 @@ class PostDetailViewModel( mvi.scope?.launch(Dispatchers.IO) { val auth = identityRepository.authToken.value.orEmpty() commentRepository.delete(id, auth) - refresh() + handleCommentDelete(id) refreshPost() } } + private fun handleCommentDelete(id: Int) { + mvi.updateState { it.copy(comments = it.comments.filter { comment -> comment.id != id }) } + } + private fun deletePost() { mvi.scope?.launch(Dispatchers.IO) { val auth = identityRepository.authToken.value.orEmpty() @@ -658,6 +637,48 @@ class PostDetailViewModel( } } } + + private fun feature(post: PostModel) { + mvi.scope?.launch(Dispatchers.IO) { + val auth = identityRepository.authToken.value.orEmpty() + val newPost = postRepository.featureInCommunity( + postId = post.id, + auth = auth, + featured = !post.featuredCommunity + ) + if (newPost != null) { + handlePostUpdate(newPost) + } + } + } + + private fun lock(post: PostModel) { + mvi.scope?.launch(Dispatchers.IO) { + val auth = identityRepository.authToken.value.orEmpty() + val newPost = postRepository.lock( + postId = post.id, + auth = auth, + locked = !post.locked, + ) + if (newPost != null) { + handlePostUpdate(newPost) + } + } + } + + private fun distinguish(comment: CommentModel) { + mvi.scope?.launch(Dispatchers.IO) { + val auth = identityRepository.authToken.value.orEmpty() + val newComment = commentRepository.distinguish( + commentId = comment.id, + auth = auth, + distinguished = !comment.distinguished, + ) + if (newComment != null) { + handleCommentUpdate(newComment) + } + } + } } private data class Node( diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveMviModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveMviModel.kt new file mode 100644 index 000000000..451cd0435 --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveMviModel.kt @@ -0,0 +1,29 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove + +import androidx.compose.runtime.Stable +import cafe.adriel.voyager.core.model.ScreenModel +import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel +import dev.icerock.moko.resources.desc.StringDesc + +@Stable +interface RemoveMviModel : + MviModel, + ScreenModel { + + sealed interface Intent { + data class SetText(val value: String) : Intent + data object Submit : Intent + } + + data class UiState( + val text: String = "", + val textError: StringDesc? = null, + val loading: Boolean = false, + ) + + sealed interface Effect { + data object Success : Effect + + data class Failure(val message: String?) : Effect + } +} diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveScreen.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveScreen.kt new file mode 100644 index 000000000..602c1d348 --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveScreen.kt @@ -0,0 +1,163 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +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.BottomSheetHandle +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.ProgressHud +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getRemoveViewModel +import com.github.diegoberaldin.raccoonforlemmy.resources.MR +import dev.icerock.moko.resources.compose.localized +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class RemoveScreen( + private val postId: Int? = null, + private val commentId: Int? = null, +) : Screen { + @Composable + override fun Content() { + val model = rememberScreenModel { + getRemoveViewModel( + postId = postId, + commentId = commentId, + ) + } + model.bindToLifecycle(key) + val uiState by model.uiState.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + val genericError = stringResource(MR.strings.message_generic_error) + val navigationCoordinator = remember { getNavigationCoordinator() } + + LaunchedEffect(model) { + model.effects.onEach { + when (it) { + is RemoveMviModel.Effect.Failure -> { + snackbarHostState.showSnackbar(it.message ?: genericError) + } + + RemoveMviModel.Effect.Success -> { + navigationCoordinator.hideBottomSheet() + } + } + }.launchIn(this) + } + + Box( + contentAlignment = Alignment.BottomCenter, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(Spacing.s), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.fillMaxWidth().padding(top = Spacing.s), + ) { + Column( + modifier = Modifier.align(Alignment.TopCenter), + verticalArrangement = Arrangement.spacedBy(Spacing.s), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BottomSheetHandle() + val title = when { + commentId != null -> stringResource(MR.strings.create_report_title_comment) + else -> stringResource(MR.strings.create_report_title_post) + } + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + + } + Button( + modifier = Modifier.align(Alignment.TopEnd), + content = { + Text( + text = stringResource(MR.strings.button_confirm), + ) + }, + onClick = { + model.reduce(RemoveMviModel.Intent.Submit) + }, + ) + } + + val commentFocusRequester = remember { FocusRequester() } + TextField( + modifier = Modifier + .focusRequester(commentFocusRequester) + .heightIn(min = 300.dp, max = 500.dp) + .fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ), + label = { + Text(text = stringResource(MR.strings.create_report_placeholder)) + }, + textStyle = MaterialTheme.typography.bodyMedium, + value = uiState.text, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + autoCorrect = true, + ), + onValueChange = { value -> + model.reduce(RemoveMviModel.Intent.SetText(value)) + }, + isError = uiState.textError != null, + supportingText = { + if (uiState.textError != null) { + Text( + text = uiState.textError?.localized().orEmpty(), + color = MaterialTheme.colorScheme.error, + ) + } + }, + ) + Spacer(Modifier.height(Spacing.xxl)) + } + + if (uiState.loading) { + ProgressHud() + } + + SnackbarHost( + modifier = Modifier.padding(bottom = Spacing.xxxl), + hostState = snackbarHostState + ) + } + } +} \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveViewModel.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveViewModel.kt new file mode 100644 index 000000000..5a59a22d0 --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/remove/RemoveViewModel.kt @@ -0,0 +1,75 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.commonui.remove + +import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel +import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel +import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter +import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent +import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch + +class RemoveViewModel( + private val postId: Int?, + private val commentId: Int?, + private val mvi: DefaultMviModel, + private val identityRepository: IdentityRepository, + private val postRepository: PostRepository, + private val commentRepository: CommentRepository, + private val notificationCenter: NotificationCenter, +) : RemoveMviModel, + MviModel by mvi { + + override fun reduce(intent: RemoveMviModel.Intent) { + when (intent) { + is RemoveMviModel.Intent.SetText -> { + mvi.updateState { + it.copy(text = intent.value) + } + } + + RemoveMviModel.Intent.Submit -> submit() + } + } + + private fun submit() { + if (mvi.uiState.value.loading) { + return + } + val text = uiState.value.text + + mvi.updateState { it.copy(loading = true) } + mvi.scope?.launch(Dispatchers.IO) { + try { + val auth = identityRepository.authToken.value.orEmpty() + if (postId != null) { + postRepository.remove( + postId = postId, + reason = text, + auth = auth, + removed = true, + )?.also { post -> + notificationCenter.send(NotificationCenterEvent.PostRemoved(post)) + } + } else if (commentId != null) { + commentRepository.remove( + commentId = commentId, + reason = text, + auth = auth, + removed = true, + )?.also { comment -> + notificationCenter.send(NotificationCenterEvent.CommentRemoved(comment)) + } + } + mvi.emitEffect(RemoveMviModel.Effect.Success) + } catch (e: Throwable) { + val message = e.message + mvi.emitEffect(RemoveMviModel.Effect.Failure(message)) + } finally { + mvi.updateState { it.copy(loading = false) } + } + } + } +} \ No newline at end of file diff --git a/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/CommentReportCard.kt b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/CommentReportCard.kt new file mode 100644 index 000000000..872a020fc --- /dev/null +++ b/core-commonui/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/commonui/reportlist/CommentReportCard.kt @@ -0,0 +1,49 @@ +package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout +import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.Option +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.OptionId +import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardBody +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentReportModel + +@Composable +internal fun CommentReportCard( + report: CommentReportModel, + postLayout: PostLayout = PostLayout.Card, + modifier: Modifier = Modifier, + options: List