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
This commit is contained in:
Diego Beraldin 2023-11-24 18:56:33 +01:00 committed by GitHub
parent d136837d04
commit 70fadd93cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2955 additions and 129 deletions

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

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 DistinguishCommentForm(
@SerialName("comment_id") val commentId: CommentId,
@SerialName("distinguished") val distinguished: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -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,
)

View File

@ -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<CommentReportView>,
)

View File

@ -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<PostReportView>,
)

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 LockPostForm(
@SerialName("post_id") val postId: PostId,
@SerialName("locked") val locked: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -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,
}

View File

@ -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,
)

View File

@ -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,
)

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 ResolveCommentReportForm(
@SerialName("report_id") val reportId: CommentReportId,
@SerialName("resolved") val resolved: Boolean,
@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 ResolvePostReportForm(
@SerialName("report_id") val reportId: PostReportId,
@SerialName("resolved") val resolved: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -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<CommentReportResponse>
@POST("comment/remove")
@Headers("Content-Type: application/json")
suspend fun remove(
@Header("Authorization") authHeader: String? = null,
@Body form: RemoveCommentForm,
): Response<CommentResponse>
@POST("comment/distinguish")
@Headers("Content-Type: application/json")
suspend fun distinguish(
@Header("Authorization") authHeader: String? = null,
@Body form: DistinguishCommentForm,
): Response<CommentResponse>
@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<ListCommentReportsResponse>
@PUT("comment/report/resolve")
@Headers("Content-Type: application/json")
suspend fun resolveReport(
@Header("Authorization") authHeader: String? = null,
@Body form: ResolveCommentReportForm,
): Response<CommentReportResponse>
}

View File

@ -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<BlockCommunityResponse>
@POST("community/ban_user")
@Headers("Content-Type: application/json")
suspend fun ban(
@Header("Authorization") authHeader: String? = null,
@Body form: BanFromCommunityForm,
): Response<BanFromCommunityResponse>
}

View File

@ -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<PostReportResponse>
@POST("post/feature")
@Headers("Content-Type: application/json")
suspend fun feature(
@Header("Authorization") authHeader: String? = null,
@Body form: FeaturePostForm,
): Response<PostResponse>
@POST("post/remove")
@Headers("Content-Type: application/json")
suspend fun remove(
@Header("Authorization") authHeader: String? = null,
@Body form: RemovePostForm,
): Response<PostResponse>
@POST("post/lock")
@Headers("Content-Type: application/json")
suspend fun lock(
@Header("Authorization") authHeader: String? = null,
@Body form: LockPostForm,
): Response<PostResponse>
@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<ListPostReportsResponse>
@PUT("post/report/resolve")
@Headers("Content-Type: application/json")
suspend fun resolveReport(
@Header("Authorization") authHeader: String? = null,
@Body form: ResolvePostReportForm,
): Response<PostReportResponse>
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}
})

View File

@ -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)
}
}
}
}

View File

@ -91,6 +91,7 @@ fun CommentCard(
onOpenCreator = onOpenCreator,
onOpenCommunity = onOpenCommunity,
onToggleExpanded = onToggleExpanded,
distinguished = comment.distinguished,
)
ScaledContent {
PostCardBody(

View File

@ -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(

View File

@ -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)
}

View File

@ -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,

View File

@ -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<CreateReportMviModel> {
factory<CreateReportMviModel> { 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<RemoveMviModel> { params ->
RemoveViewModel(
postId = params[0],
commentId = params[1],
mvi = DefaultMviModel(RemoveMviModel.UiState()),
identityRepository = get(),
postRepository = get(),
commentRepository = get(),
notificationCenter = get(),
)
}
factory<ReportListMviModel> { params ->
ReportListViewModel(
communityId = params[0],
mvi = DefaultMviModel(ReportListMviModel.UiState()),
identityRepository = get(),
postRepository = get(),
commentRepository = get(),
themeRepository = get(),
settingsRepository = get(),
hapticFeedback = get(),
)
}
}

View File

@ -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
expect fun getSelectCommunityViewModel(): SelectCommunityMviModel
expect fun getRemoveViewModel(
postId: Int? = null,
commentId: Int? = null,
): RemoveMviModel
expect fun getReportListViewModel(
communityId: Int,
): ReportListMviModel

View File

@ -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,
)
}
}
}
}
}
}

View File

@ -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,

View File

@ -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
}
},

View File

@ -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(

View File

@ -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<RemoveMviModel.Intent, RemoveMviModel.UiState, RemoveMviModel.Effect>,
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
}
}

View File

@ -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
)
}
}
}

View File

@ -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<RemoveMviModel.Intent, RemoveMviModel.UiState, RemoveMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
private val notificationCenter: NotificationCenter,
) : RemoveMviModel,
MviModel<RemoveMviModel.Intent, RemoveMviModel.UiState, RemoveMviModel.Effect> 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) }
}
}
}
}

View File

@ -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<Option> = emptyList(),
autoLoadImages: Boolean = true,
onOpen: (() -> Unit)? = null,
onOptionSelected: ((OptionId) -> Unit)? = null,
) {
InnerReportCard(
modifier = modifier,
reason = report.reason.orEmpty(),
postLayout = postLayout,
creator = report.creator,
date = report.publishDate,
autoLoadImages = autoLoadImages,
options = options,
onOptionSelected = onOptionSelected,
onOpen = onOpen,
originalContent = {
Column {
report.originalText?.also { text ->
PostCardBody(
modifier = Modifier.padding(
vertical = Spacing.xs,
horizontal = Spacing.xs,
),
text = text,
autoLoadImages = autoLoadImages,
)
}
}
}
)
}

View File

@ -0,0 +1,256 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.material.icons.Icons
import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomDropDown
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage
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.core.commonui.components.ScaledContent
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.core.utils.datetime.prettifyDate
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@Composable
internal fun InnerReportCard(
modifier: Modifier = Modifier,
reason: String,
autoLoadImages: Boolean = true,
date: String? = null,
creator: UserModel? = null,
postLayout: PostLayout = PostLayout.Card,
options: List<Option> = emptyList(),
onOpenCreator: ((UserModel) -> Unit)? = null,
onOpen: (() -> Unit)? = null,
originalContent: (@Composable () -> Unit)? = null,
onOptionSelected: ((OptionId) -> Unit)? = null,
) {
Box(
modifier = modifier.let {
if (postLayout == PostLayout.Card) {
it.padding(horizontal = Spacing.xs)
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp),
shape = RoundedCornerShape(CornerSize.l),
).padding(Spacing.s)
} else {
it.background(MaterialTheme.colorScheme.background)
}
},
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
ReportHeader(
creator = creator,
autoLoadImages = autoLoadImages,
onOpenCreator = onOpenCreator,
)
ScaledContent {
PostCardBody(
modifier = Modifier.padding(
horizontal = Spacing.xs,
),
text = reason,
)
if (originalContent != null) {
Box(
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(CornerSize.l),
)
.padding(all = Spacing.s)
) {
originalContent()
}
}
}
ReportFooter(
date = date,
onOpenResolve = onOpen,
options = options,
onOptionSelected = onOptionSelected,
)
}
}
}
@Composable
private fun ReportHeader(
modifier: Modifier = Modifier,
creator: UserModel? = null,
autoLoadImages: Boolean = true,
iconSize: Dp = IconSize.s,
onOpenCreator: ((UserModel) -> Unit)? = null,
) {
val creatorName = creator?.name.orEmpty()
val creatorAvatar = creator?.avatar.orEmpty()
val creatorHost = creator?.host.orEmpty()
if (creatorName.isNotEmpty()) {
Row(
modifier = modifier
.onClick(
onClick = rememberCallback {
if (creator != null) {
onOpenCreator?.invoke(creator)
}
},
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
if (creatorAvatar.isNotEmpty() && autoLoadImages) {
CustomImage(
modifier = Modifier
.padding(Spacing.xxxs)
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = creatorAvatar,
quality = FilterQuality.Low,
contentDescription = null,
contentScale = ContentScale.FillBounds,
)
}
Text(
modifier = Modifier.padding(vertical = Spacing.xs),
text = buildString {
append(creatorName)
if (creatorHost.isNotEmpty()) {
append("@$creatorHost")
}
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
@Composable
private fun ReportFooter(
date: String? = null,
options: List<Option> = emptyList(),
onOpenResolve: (() -> Unit)? = null,
onOptionSelected: ((OptionId) -> Unit)? = null,
) {
val buttonModifier = Modifier.size(IconSize.m).padding(3.5.dp)
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
Box {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = buttonModifier,
imageVector = Icons.Default.Schedule,
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground,
)
Text(
text = date?.prettifyDate() ?: "",
)
if (options.isNotEmpty()) {
Icon(
modifier = buttonModifier
.padding(top = Spacing.xxs)
.onGloballyPositioned {
optionsOffset = it.positionInParent()
}
.onClick(
onClick = rememberCallback {
optionsExpanded = true
},
),
imageVector = Icons.Default.MoreHoriz,
contentDescription = null,
)
}
Spacer(modifier = Modifier.weight(1f))
Image(
modifier = buttonModifier
.onClick(
onClick = rememberCallback {
onOpenResolve?.invoke()
},
),
imageVector = Icons.Default.OpenInNew,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
)
}
CustomDropDown(
expanded = optionsExpanded,
onDismiss = {
optionsExpanded = false
},
offset = DpOffset(
x = optionsOffset.x.toLocalDp(),
y = optionsOffset.y.toLocalDp(),
),
) {
options.forEach { option ->
Text(
modifier = Modifier.padding(
horizontal = Spacing.m,
vertical = Spacing.s,
).onClick(
onClick = rememberCallback {
optionsExpanded = false
onOptionSelected?.invoke(option.id)
},
),
text = option.text,
)
}
}
}
}

View File

@ -0,0 +1,100 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
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.Option
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.OptionId
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardImage
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardTitle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostLinkBanner
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.imageUrl
@Composable
internal fun PostReportCard(
report: PostReportModel,
postLayout: PostLayout = PostLayout.Card,
modifier: Modifier = Modifier,
autoLoadImages: Boolean = true,
onOpen: (() -> Unit)? = null,
options: List<Option> = emptyList(),
onOptionSelected: ((OptionId) -> Unit)? = null,
) {
InnerReportCard(
modifier = modifier,
reason = report.reason.orEmpty(),
postLayout = postLayout,
creator = report.creator,
date = report.publishDate,
autoLoadImages = autoLoadImages,
options = options,
onOptionSelected = onOptionSelected,
onOpen = onOpen,
originalContent = {
Column {
report.originalTitle?.also { title ->
PostCardTitle(
modifier = Modifier.padding(
vertical = Spacing.xs,
horizontal = Spacing.xs,
),
text = title,
autoLoadImages = autoLoadImages,
)
}
report.imageUrl.takeIf { it.isNotEmpty() }?.also { imageUrl ->
PostCardImage(
modifier = Modifier
.padding(vertical = Spacing.xxs)
.clip(RoundedCornerShape(CornerSize.xl)),
imageUrl = imageUrl,
autoLoadImages = autoLoadImages,
)
}
report.originalText?.also { text ->
PostCardBody(
modifier = Modifier.padding(
vertical = Spacing.xs,
horizontal = Spacing.xs,
),
text = text,
autoLoadImages = autoLoadImages,
)
}
report.originalUrl?.also { url ->
val settingsRepository = remember { getSettingsRepository() }
val uriHandler = LocalUriHandler.current
val navigationCoordinator = remember { getNavigationCoordinator() }
PostLinkBanner(
modifier = Modifier
.padding(vertical = Spacing.xs)
.onClick(
onClick = {
if (settingsRepository.currentSettings.value.openUrlsInExternalBrowser) {
uriHandler.openUri(url)
} else {
navigationCoordinator.pushScreen(WebViewScreen(url))
}
},
),
url = url,
)
}
}
}
)
}

View File

@ -0,0 +1,63 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
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.shimmerEffect
@Composable
internal fun ReportCardPlaceHolder(
postLayout: PostLayout = PostLayout.Card,
) {
Column(
modifier = Modifier.let {
if (postLayout == PostLayout.Card) {
it.padding(horizontal = Spacing.xs).background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp),
shape = RoundedCornerShape(CornerSize.l),
).padding(Spacing.s)
} else {
it
}
},
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
Box(
modifier = Modifier
.height(IconSize.l)
.fillMaxWidth()
.clip(RoundedCornerShape(CornerSize.m))
.shimmerEffect()
)
Box(
modifier = Modifier
.padding(vertical = Spacing.xxxs)
.height(80.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(CornerSize.m))
.shimmerEffect()
)
Box(
modifier = Modifier
.height(IconSize.l)
.fillMaxWidth()
.clip(RoundedCornerShape(CornerSize.m))
.shimmerEffect()
)
}
}

View File

@ -0,0 +1,46 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostReportModel
enum class ReportListSection {
Posts,
Comments,
}
interface ReportListMviModel :
MviModel<ReportListMviModel.Intent, ReportListMviModel.UiState, ReportListMviModel.Effect>,
ScreenModel {
sealed interface Intent {
data object HapticIndication : Intent
data class ChangeSection(val value: ReportListSection) : Intent
data class ChangeUnresolvedOnly(val value: Boolean) : Intent
data object LoadNextPage : Intent
data object Refresh : Intent
data class ResolvePost(val id: Int) : Intent
data class ResolveComment(val id: Int) : Intent
}
data class UiState(
val section: ReportListSection = ReportListSection.Posts,
val unresolvedOnly: Boolean = true,
val refreshing: Boolean = false,
val loading: Boolean = false,
val initial: Boolean = true,
val asyncInProgress: Boolean = false,
val swipeActionsEnabled: Boolean = true,
val autoLoadImages: Boolean = true,
val postLayout: PostLayout = PostLayout.Card,
val canFetchMore: Boolean = true,
val postReports: List<PostReportModel> = emptyList(),
val commentReports: List<CommentReportModel> = emptyList(),
)
sealed interface Effect {
data object BackToTop : Effect
}
}

View File

@ -0,0 +1,490 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist
import androidx.compose.foundation.Image
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.ReportOff
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissValue
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextAlign
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.data.PostLayout
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.Option
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.OptionId
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.ProgressHud
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SwipeableCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getReportListViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.ReportListTypeSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostReportModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class ReportListScreen(
private val communityId: Int,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getReportListViewModel(communityId) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val notificationCenter = remember { getNotificationCenter() }
val navigationCoordinator = remember { getNavigationCoordinator() }
var rawContent by remember { mutableStateOf<Any?>(null) }
val scope = rememberCoroutineScope()
val settingsRepository = remember { getSettingsRepository() }
val settings by settingsRepository.currentSettings.collectAsState()
val lazyListState = rememberLazyListState()
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = rememberCallback(model) {
model.reduce(ReportListMviModel.Intent.Refresh)
},
)
LaunchedEffect(notificationCenter) {
notificationCenter.subscribe(NotificationCenterEvent.ChangeReportListType::class)
.onEach { evt ->
model.reduce(
ReportListMviModel.Intent.ChangeUnresolvedOnly(evt.unresolvedOnly)
)
}.launchIn(this)
}
LaunchedEffect(model) {
model.effects.onEach { effect ->
when (effect) {
ReportListMviModel.Effect.BackToTop -> {
scope.launch {
lazyListState.scrollToItem(0)
}
}
}
}.launchIn(this)
}
Scaffold(
modifier = Modifier.padding(Spacing.xxs),
topBar = {
TopAppBar(
scrollBehavior = scrollBehavior,
navigationIcon = {
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
navigationCoordinator.popScreen()
},
),
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
},
title = {
Column(modifier = Modifier.padding(horizontal = Spacing.s)) {
Text(
text = stringResource(MR.strings.report_list_title),
style = MaterialTheme.typography.titleMedium,
)
val text = when (uiState.unresolvedOnly) {
true -> stringResource(MR.strings.report_list_type_unresolved)
else -> stringResource(MR.strings.report_list_type_all)
}
Text(
modifier = Modifier.onClick(
onClick = rememberCallback {
val sheet = ReportListTypeSheet()
navigationCoordinator.showBottomSheet(sheet)
},
),
text = text,
style = MaterialTheme.typography.titleSmall,
)
}
},
)
},
) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues).let {
if (settings.hideNavigationBarWhileScrolling) {
it.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
it
}
},
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
SectionSelector(
modifier = Modifier.padding(vertical = Spacing.s),
titles = listOf(
stringResource(MR.strings.profile_section_posts),
stringResource(MR.strings.profile_section_comments),
),
currentSection = when (uiState.section) {
ReportListSection.Comments -> 1
else -> 0
},
onSectionSelected = {
val section = when (it) {
1 -> ReportListSection.Comments
else -> ReportListSection.Posts
}
model.reduce(ReportListMviModel.Intent.ChangeSection(section))
},
)
Box(
modifier = Modifier
.let {
if (settings.hideNavigationBarWhileScrolling) {
it.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
it
}
}
.pullRefresh(pullRefreshState),
) {
LazyColumn(
state = lazyListState,
) {
if (uiState.section == ReportListSection.Posts) {
if (uiState.postReports.isEmpty() && uiState.loading && uiState.initial) {
items(5) {
ReportCardPlaceHolder(uiState.postLayout)
if (uiState.postLayout != PostLayout.Card) {
Divider(modifier = Modifier.padding(vertical = Spacing.s))
} else {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
}
if (uiState.postReports.isEmpty() && !uiState.initial) {
item {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(MR.strings.message_empty_list),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
items(
uiState.postReports,
{ it.id.toString() + it.updateDate },
) { report ->
val endColor = MaterialTheme.colorScheme.secondary
SwipeableCard(
modifier = Modifier.fillMaxWidth(),
directions = setOf(DismissDirection.EndToStart),
enabled = uiState.swipeActionsEnabled,
backgroundColor = rememberCallbackArgs {
when (it) {
DismissValue.DismissedToStart -> endColor
else -> Color.Transparent
}
},
onGestureBegin = rememberCallback(model) {
model.reduce(ReportListMviModel.Intent.HapticIndication)
},
onDismissToStart = rememberCallback(model) {
model.reduce(
ReportListMviModel.Intent.ResolvePost(report.id),
)
},
swipeContent = { _ ->
val icon = when {
report.resolved -> Icons.Default.Report
else -> Icons.Default.ReportOff
}
Icon(
modifier = Modifier.padding(Spacing.xs),
imageVector = icon,
contentDescription = null,
tint = Color.White,
)
},
content = {
PostReportCard(
report = report,
postLayout = uiState.postLayout,
autoLoadImages = uiState.autoLoadImages,
onOpen = rememberCallback {
val screen = PostDetailScreen(
post = PostModel(id = report.postId),
isMod = true,
)
navigationCoordinator.pushScreen(screen)
},
options = buildList {
this += Option(
OptionId.SeeRaw,
stringResource(MR.strings.post_action_see_raw),
)
this += Option(
OptionId.ResolveReport,
if (report.resolved) {
stringResource(MR.strings.report_action_unresolve)
} else {
stringResource(MR.strings.report_action_resolve)
},
)
},
onOptionSelected = rememberCallbackArgs { optionId ->
when (optionId) {
OptionId.SeeRaw -> {
rawContent = report
}
OptionId.ResolveReport -> {
model.reduce(
ReportListMviModel.Intent.ResolvePost(
report.id
)
)
}
else -> Unit
}
},
)
},
)
if (uiState.postLayout != PostLayout.Card) {
Divider(modifier = Modifier.padding(vertical = Spacing.s))
} else {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
} else {
if (uiState.commentReports.isEmpty() && uiState.loading && uiState.initial) {
items(5) {
ReportCardPlaceHolder(uiState.postLayout)
if (uiState.postLayout != PostLayout.Card) {
Divider(modifier = Modifier.padding(vertical = Spacing.s))
} else {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
}
if (uiState.commentReports.isEmpty() && !uiState.initial) {
item {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(MR.strings.message_empty_list),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
items(
uiState.commentReports,
{ it.id.toString() + it.updateDate },
) { report ->
val endColor = MaterialTheme.colorScheme.secondary
SwipeableCard(
modifier = Modifier.fillMaxWidth(),
directions = setOf(DismissDirection.EndToStart),
enabled = uiState.swipeActionsEnabled,
backgroundColor = rememberCallbackArgs {
when (it) {
DismissValue.DismissedToStart -> endColor
else -> Color.Transparent
}
},
onGestureBegin = rememberCallback(model) {
model.reduce(ReportListMviModel.Intent.HapticIndication)
},
onDismissToStart = rememberCallback(model) {
model.reduce(
ReportListMviModel.Intent.ResolveComment(report.id),
)
},
swipeContent = { _ ->
val icon = when {
report.resolved -> Icons.Default.Report
else -> Icons.Default.ReportOff
}
Icon(
modifier = Modifier.padding(Spacing.xs),
imageVector = icon,
contentDescription = null,
tint = Color.White,
)
},
content = {
CommentReportCard(
report = report,
postLayout = uiState.postLayout,
autoLoadImages = uiState.autoLoadImages,
onOpen = rememberCallback {
val screen = PostDetailScreen(
post = PostModel(id = report.postId),
highlightCommentId = report.commentId,
isMod = true,
)
navigationCoordinator.pushScreen(screen)
},
options = buildList {
this += Option(
OptionId.SeeRaw,
stringResource(MR.strings.post_action_see_raw),
)
this += Option(
OptionId.ResolveReport,
if (report.resolved) {
stringResource(MR.strings.report_action_unresolve)
} else {
stringResource(MR.strings.report_action_resolve)
},
)
},
onOptionSelected = rememberCallbackArgs { optionId ->
when (optionId) {
OptionId.SeeRaw -> {
rawContent = report
}
OptionId.ResolveReport -> {
model.reduce(
ReportListMviModel.Intent.ResolveComment(
report.id
)
)
}
else -> Unit
}
},
)
},
)
if (uiState.postLayout != PostLayout.Card) {
Divider(modifier = Modifier.padding(vertical = Spacing.s))
} else {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(ReportListMviModel.Intent.LoadNextPage)
}
if (uiState.loading && !uiState.refreshing) {
Box(
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier.size(25.dp),
color = MaterialTheme.colorScheme.primary,
)
}
}
}
item {
Spacer(modifier = Modifier.height(Spacing.s))
}
}
if (uiState.asyncInProgress) {
ProgressHud()
}
PullRefreshIndicator(
refreshing = uiState.refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
)
}
}
}
if (rawContent != null) {
when (val content = rawContent) {
is PostReportModel -> {
RawContentDialog(
title = content.originalTitle,
date = content.publishDate,
url = content.originalUrl,
text = content.originalText,
onDismiss = {
rawContent = null
},
)
}
is CommentReportModel -> {
RawContentDialog(
date = content.publishDate,
text = content.originalText,
onDismiss = {
rawContent = null
},
)
}
}
}
}
}

View File

@ -0,0 +1,272 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.reportlist
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostReportModel
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.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class ReportListViewModel(
private val communityId: Int,
private val mvi: DefaultMviModel<ReportListMviModel.Intent, ReportListMviModel.UiState, ReportListMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
private val themeRepository: ThemeRepository,
private val settingsRepository: SettingsRepository,
private val hapticFeedback: HapticFeedback,
) : ReportListMviModel,
MviModel<ReportListMviModel.Intent, ReportListMviModel.UiState, ReportListMviModel.Effect> by mvi {
private var currentPage = 1
override fun onStarted() {
mvi.onStarted()
mvi.scope?.launch {
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)
settingsRepository.currentSettings.onEach { settings ->
mvi.updateState {
it.copy(
autoLoadImages = settings.autoLoadImages,
swipeActionsEnabled = settings.enableSwipeActions,
)
}
}.launchIn(this)
if (uiState.value.postReports.isEmpty()) {
refresh(initial = true)
}
}
}
override fun reduce(intent: ReportListMviModel.Intent) {
when (intent) {
is ReportListMviModel.Intent.ChangeSection -> changeSection(intent.value)
is ReportListMviModel.Intent.ChangeUnresolvedOnly -> changeUnresolvedOnly(intent.value)
ReportListMviModel.Intent.Refresh -> refresh()
ReportListMviModel.Intent.LoadNextPage -> mvi.scope?.launch(Dispatchers.IO) {
loadNextPage()
}
is ReportListMviModel.Intent.ResolveComment -> mvi.uiState.value.commentReports
.firstOrNull { it.id == intent.id }?.also {
resolve(it)
}
is ReportListMviModel.Intent.ResolvePost -> mvi.uiState.value.postReports
.firstOrNull { it.id == intent.id }?.also {
resolve(it)
}
ReportListMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
}
}
private fun changeSection(section: ReportListSection) {
currentPage = 1
mvi.updateState {
it.copy(
section = section,
canFetchMore = true,
)
}
refresh(initial = true)
}
private fun changeUnresolvedOnly(value: Boolean) {
mvi.updateState {
it.copy(unresolvedOnly = value)
}
refresh(initial = true)
}
private fun refresh(initial: Boolean = false) {
currentPage = 1
mvi.updateState {
it.copy(
canFetchMore = true,
refreshing = true,
initial = initial,
)
}
mvi.scope?.launch {
loadNextPage()
}
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
mvi.updateState { it.copy(refreshing = false) }
return
}
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value.orEmpty()
val refreshing = currentState.refreshing
val section = currentState.section
val unresolvedOnly = currentState.unresolvedOnly
if (section == ReportListSection.Posts) {
val itemList = postRepository.getReports(
auth = auth,
communityId = communityId,
page = currentPage,
unresolvedOnly = unresolvedOnly,
)
val commentReports =
if (currentPage == 1 && currentState.commentReports.isEmpty()) {
// this is needed because otherwise on first selector change
// the lazy column scrolls back to top (it must have an empty data set)
commentRepository.getReports(
auth = auth,
communityId = communityId,
page = currentPage,
unresolvedOnly = unresolvedOnly,
).orEmpty()
} else {
currentState.commentReports
}
mvi.updateState {
val postReports = if (refreshing) {
itemList.orEmpty()
} else {
it.postReports + itemList.orEmpty()
}
it.copy(
postReports = postReports,
commentReports = commentReports,
loading = false,
canFetchMore = itemList?.isEmpty() != true,
refreshing = false,
initial = false,
)
}
if (!itemList.isNullOrEmpty()) {
currentPage++
}
} else {
val itemList = commentRepository.getReports(
auth = auth,
communityId = communityId,
page = currentPage,
unresolvedOnly = unresolvedOnly,
)
mvi.updateState {
val commentReports = if (refreshing) {
itemList.orEmpty()
} else {
it.commentReports + itemList.orEmpty()
}
it.copy(
commentReports = commentReports,
loading = false,
canFetchMore = itemList?.isEmpty() != true,
refreshing = false,
initial = false,
)
}
if (!itemList.isNullOrEmpty()) {
currentPage++
}
}
}
}
private fun resolve(report: PostReportModel) {
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(asyncInProgress = true) }
val auth = identityRepository.authToken.value.orEmpty()
val newReport = postRepository.resolveReport(
reportId = report.id,
auth = auth,
resolved = !report.resolved
)
mvi.updateState { it.copy(asyncInProgress = false) }
if (newReport != null) {
if (uiState.value.unresolvedOnly && newReport.resolved) {
handleReporDelete(newReport)
} else {
handleReportUpdate(newReport)
}
}
}
}
private fun resolve(report: CommentReportModel) {
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(asyncInProgress = true) }
val auth = identityRepository.authToken.value.orEmpty()
val newReport = commentRepository.resolveReport(
reportId = report.id,
auth = auth,
resolved = !report.resolved
)
mvi.updateState { it.copy(asyncInProgress = false) }
if (newReport != null) {
if (uiState.value.unresolvedOnly && newReport.resolved) {
handleReporDelete(newReport)
} else {
handleReportUpdate(newReport)
}
}
}
}
private fun handleReportUpdate(report: PostReportModel) {
mvi.updateState {
it.copy(
postReports = it.postReports.map { r ->
if (r.id == report.id) {
report
} else {
r
}
}
)
}
}
private fun handleReportUpdate(report: CommentReportModel) {
mvi.updateState {
it.copy(
commentReports = it.commentReports.map { r ->
if (r.id == report.id) {
report
} else {
r
}
}
)
}
}
private fun handleReporDelete(report: PostReportModel) {
mvi.updateState {
it.copy(
postReports = it.postReports.filter { r -> r.id != report.id }
)
}
}
private fun handleReporDelete(report: CommentReportModel) {
mvi.updateState {
it.copy(
commentReports = it.commentReports.filter { r -> r.id != report.id }
)
}
}
}

View File

@ -18,6 +18,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
@ -46,8 +48,9 @@ actual fun getPostDetailViewModel(
post: PostModel,
otherInstance: String,
highlightCommentId: Int?,
isModerator: Boolean,
): PostDetailMviModel =
CommonUiViewModelHelper.getPostDetailModel(post, otherInstance, highlightCommentId)
CommonUiViewModelHelper.getPostDetailModel(post, otherInstance, highlightCommentId, isModerator)
actual fun getCommunityDetailViewModel(
community: CommunityModel,
@ -96,6 +99,15 @@ actual fun getCreateReportViewModel(
actual fun getSelectCommunityViewModel(): SelectCommunityMviModel =
CommonUiViewModelHelper.selectCommunityViewModel
actual fun getRemoveViewModel(
postId: Int?,
commentId: Int?,
): RemoveMviModel = CommonUiViewModelHelper.getRemoveModel(postId, commentId)
actual fun getReportListViewModel(
communityId: Int,
): ReportListMviModel = CommonUiViewModelHelper.getReportListViewModel(communityId)
object CommonUiViewModelHelper : KoinComponent {
val navigationCoordinator: NavigationCoordinator by inject()
@ -110,9 +122,10 @@ object CommonUiViewModelHelper : KoinComponent {
post: PostModel,
otherInstance: String,
highlightCommentId: Int?,
isModerator: Boolean,
): PostDetailMviModel {
val model: PostDetailMviModel by inject(
parameters = { parametersOf(post, otherInstance, highlightCommentId) },
parameters = { parametersOf(post, otherInstance, highlightCommentId, isModerator) },
)
return model
}
@ -182,6 +195,25 @@ object CommonUiViewModelHelper : KoinComponent {
)
return model
}
fun getRemoveModel(
postId: Int?,
commentId: Int?,
): RemoveMviModel {
val model: RemoveMviModel by inject(
parameters = { parametersOf(postId, commentId) }
)
return model
}
fun getReportListViewModel(
communityId: Int,
): ReportListMviModel {
val model: ReportListMviModel by inject(
parameters = { parametersOf(communityId) }
)
return model
}
}
@Composable

View File

@ -41,4 +41,7 @@ sealed interface NotificationCenterEvent {
data class MultiCommunityCreated(val model: MultiCommunityModel) : NotificationCenterEvent
data object CloseDialog : NotificationCenterEvent
data class SelectCommunity(val model: CommunityModel) : NotificationCenterEvent
data class PostRemoved(val model: PostModel) : NotificationCenterEvent
data class CommentRemoved(val model: CommentModel) : NotificationCenterEvent
data class ChangeReportListType(val unresolvedOnly: Boolean) : NotificationCenterEvent
}

View File

@ -18,6 +18,8 @@ data class CommentModel(
val updateDate: String? = null,
val comments: Int? = null,
val path: String = "",
val distinguished: Boolean = false,
val removed: Boolean = false,
@Transient
val expanded: Boolean = true,
@Transient

View File

@ -0,0 +1,14 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
data class CommentReportModel(
val id: Int = 0,
val creator: UserModel? = null,
val commentId: Int = 0,
val postId: Int = 0,
val originalText: String? = null,
val reason: String? = null,
val resolved: Boolean = false,
val resolver: UserModel? = null,
val publishDate: String? = null,
val updateDate: String? = null,
)

View File

@ -24,6 +24,9 @@ data class PostModel(
val nsfw: Boolean = false,
val read: Boolean = false,
val crossPosts: List<PostModel> = emptyList(),
val featuredCommunity: Boolean = false,
val removed: Boolean = false,
val locked: Boolean = false,
) : JavaSerializable
val PostModel.imageUrl: String

View File

@ -0,0 +1,23 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.looksLikeAnImage
data class PostReportModel(
val id: Int = 0,
val creator: UserModel? = null,
val postId: Int = 0,
val reason: String? = null,
val originalTitle: String? = null,
val originalText: String? = null,
val originalUrl: String? = null,
val thumbnailUrl: String? = null,
val resolved: Boolean = false,
val resolver: UserModel? = null,
val publishDate: String? = null,
val updateDate: String? = null,
)
val PostReportModel.imageUrl: String
get() = originalUrl?.takeIf { it.looksLikeAnImage }?.takeIf { it.isNotEmpty() } ?: run {
thumbnailUrl
}.orEmpty()

View File

@ -4,10 +4,14 @@ 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.RemoveCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ResolveCommentReportForm
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.CommentReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
@ -260,4 +264,77 @@ class CommentRepository(
authHeader = auth.toAuthHeader(),
)
}
suspend fun remove(
commentId: Int,
auth: String,
removed: Boolean,
reason: String,
): CommentModel? = runCatching {
val data = RemoveCommentForm(
commentId = commentId,
removed = removed,
reason = reason,
auth = auth,
)
val response = services.comment.remove(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.commentView?.toModel()
}.getOrNull()
suspend fun distinguish(
commentId: Int,
auth: String,
distinguished: Boolean,
): CommentModel? = runCatching {
val data = DistinguishCommentForm(
commentId = commentId,
distinguished = distinguished,
auth = auth,
)
val response = services.comment.distinguish(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.commentView?.toModel()
}.getOrNull()
suspend fun getReports(
auth: String,
communityId: Int,
page: Int,
limit: Int = PostRepository.DEFAULT_PAGE_SIZE,
unresolvedOnly: Boolean = true,
): List<CommentReportModel>? = runCatching {
val response = services.comment.listReports(
authHeader = auth.toAuthHeader(),
auth = auth,
communityId = communityId,
page = page,
limit = limit,
unresolvedOnly = unresolvedOnly
)
response.body()?.commentReports?.map {
it.toModel()
}
}.getOrNull()
suspend fun resolveReport(
reportId: Int,
auth: String,
resolved: Boolean,
): CommentReportModel? = runCatching {
val data = ResolveCommentReportForm(
reportId = reportId,
resolved = resolved,
auth = auth,
)
val response = services.comment.resolveReport(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.commentReportView?.toModel()
}.getOrNull()
}

View File

@ -10,6 +10,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
@ -101,6 +102,21 @@ class CommunityRepository(
response?.communityView?.toModel()
}.getOrNull()
suspend fun getModerators(
auth: String? = null,
id: Int? = null,
): List<UserModel> = runCatching {
val response = services.community.get(
authHeader = auth.toAuthHeader(),
auth = auth,
id = id,
).body()
response?.moderators?.map {
it.moderator.toModel()
}.orEmpty()
}.getOrElse { emptyList() }
suspend fun subscribe(
auth: String? = null,
id: Int,

View File

@ -5,11 +5,17 @@ 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.LockPostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPostAsReadForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostFeatureType
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.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toDto
@ -259,4 +265,95 @@ class PostRepository(
authHeader = auth.toAuthHeader(),
)
}
suspend fun featureInCommunity(
postId: Int,
auth: String,
featured: Boolean,
): PostModel? = runCatching {
val data = FeaturePostForm(
postId = postId,
auth = auth,
featured = featured,
featureType = PostFeatureType.Community,
)
val response = services.post.feature(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.postView?.toModel()
}.getOrNull()
suspend fun lock(
postId: Int,
auth: String,
locked: Boolean,
): PostModel? = runCatching {
val data = LockPostForm(
postId = postId,
auth = auth,
locked = locked,
)
val response = services.post.lock(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.postView?.toModel()
}.getOrNull()
suspend fun remove(
postId: Int,
auth: String,
reason: String,
removed: Boolean,
): PostModel? = runCatching {
val data = RemovePostForm(
postId = postId,
auth = auth,
removed = removed,
reason = reason,
)
val response = services.post.remove(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.postView?.toModel()
}.getOrNull()
suspend fun getReports(
auth: String,
communityId: Int,
page: Int,
limit: Int = DEFAULT_PAGE_SIZE,
unresolvedOnly: Boolean = true,
): List<PostReportModel>? = runCatching {
val response = services.post.listReports(
authHeader = auth.toAuthHeader(),
auth = auth,
communityId = communityId,
page = page,
limit = limit,
unresolvedOnly = unresolvedOnly
)
response.body()?.postReports?.map {
it.toModel()
}
}.getOrNull()
suspend fun resolveReport(
reportId: Int,
auth: String,
resolved: Boolean,
): PostReportModel? = runCatching {
val data = ResolvePostReportForm(
reportId = reportId,
auth = auth,
resolved = resolved,
)
val response = services.post.resolveReport(
form = data,
authHeader = auth.toAuthHeader(),
)
response.body()?.postReportView?.toModel()
}.getOrNull()
}

View File

@ -1,6 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReplyView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReportView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentSortType
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.Community
@ -12,6 +13,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.Person
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonAggregates
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonMentionView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostReportView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PrivateMessageView
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SearchType
@ -31,10 +33,12 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.TopWeek
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.TopYear
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SubscribedType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostReportModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PrivateMessageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
@ -117,6 +121,9 @@ internal fun PostView.toModel() = PostModel(
nsfw = post.nsfw,
embedVideoUrl = post.embedVideoUrl,
read = read,
featuredCommunity = post.featuredCommunity,
removed = post.removed,
locked = post.locked,
)
internal fun CommentView.toModel() = CommentModel(
@ -134,6 +141,8 @@ internal fun CommentView.toModel() = CommentModel(
postId = comment.postId,
comments = counts.childCount,
path = comment.path,
distinguished = comment.distinguished,
removed = comment.removed,
)
internal fun Community.toModel() = CommunityModel(
@ -180,6 +189,9 @@ internal fun PersonMentionView.toModel() = PersonMentionModel(
updateDate = post.updated,
nsfw = post.nsfw,
embedVideoUrl = post.embedVideoUrl,
featuredCommunity = post.featuredCommunity,
removed = post.removed,
locked = post.locked,
),
comment = CommentModel(
id = comment.id,
@ -188,6 +200,8 @@ internal fun PersonMentionView.toModel() = PersonMentionModel(
community = community.toModel(),
publishDate = comment.published,
updateDate = comment.updated,
distinguished = comment.distinguished,
removed = comment.removed,
),
creator = creator.toModel(),
community = community.toModel(),
@ -218,7 +232,10 @@ internal fun CommentReplyView.toModel() = PersonMentionModel(
publishDate = post.published,
updateDate = post.updated,
nsfw = post.nsfw,
embedVideoUrl = post.embedVideoUrl
embedVideoUrl = post.embedVideoUrl,
featuredCommunity = post.featuredCommunity,
removed = post.removed,
locked = post.locked,
),
comment = CommentModel(
id = comment.id,
@ -227,6 +244,8 @@ internal fun CommentReplyView.toModel() = PersonMentionModel(
community = community.toModel(),
publishDate = comment.published,
updateDate = comment.updated,
distinguished = comment.distinguished,
removed = comment.removed,
),
creator = creator.toModel(),
community = community.toModel(),
@ -265,3 +284,31 @@ private fun String.communityToInstanceUrl(): String {
}
return this.substring(0, index)
}
internal fun PostReportView.toModel() = PostReportModel(
id = postReport.id,
postId = post.id,
reason = postReport.reason,
creator = creator.toModel(),
publishDate = postReport.published,
resolved = postReport.resolved,
resolver = resolver?.toModel(),
originalText = postReport.originalPostBody,
originalTitle = postReport.originalPostName,
originalUrl = postReport.originalPostUrl,
thumbnailUrl = post.thumbnailUrl,
updateDate = postReport.updated,
)
internal fun CommentReportView.toModel() = CommentReportModel(
id = commentReport.id,
postId = comment.postId,
commentId = comment.id,
reason = commentReport.reason,
creator = creator.toModel(),
publishDate = commentReport.published,
resolved = commentReport.resolved,
resolver = resolver?.toModel(),
originalText = commentReport.originalCommentText,
updateDate = commentReport.updated,
)

View File

@ -246,4 +246,18 @@
<string name="settings_zombie_mode_scroll_amount">Zombie mode scroll amount</string>
<string name="settings_mark_as_read_while_scrolling">Mark posts as read while scrolling</string>
<string name="action_quote">Quote</string>
<string name="mod_action_open_reports">Open reports</string>
<string name="mod_action_mark_as_featured">Mark as featured</string>
<string name="mod_action_unmark_as_featured">Unmark as featured</string>
<string name="mod_action_lock">Lock</string>
<string name="mod_action_unlock">Unlock</string>
<string name="mod_action_remove">Remove</string>
<string name="mod_action_mark_as_distinguished">Mark as distinguished</string>
<string name="mod_action_unmark_as_distinguished">Unmark as distinguished</string>
<string name="report_list_title">Report list</string>
<string name="report_list_type_title">Report list type</string>
<string name="report_list_type_all">All</string>
<string name="report_list_type_unresolved">Unresolved</string>
<string name="report_action_resolve">Resolve</string>
<string name="report_action_unresolve">Unresolve</string>
</resources>

View File

@ -249,4 +249,19 @@
превъртате
</string>
<string name="action_quote">цитат</string>
<string name="mod_action_open_reports">Отваряне на отчети</string>
<string name="mod_action_mark_as_featured">Маркиране като представено</string>
<string name="mod_action_unmark_as_featured">Демаркиране като представено</string>
<string name="mod_action_lock">Заключване</string>
<string name="mod_action_unlock">Отключване</string>
<string name="mod_action_remove">Премахване</string>
<string name="mod_action_mark_as_distinguished">Маркиране като отличен</string>
<string name="mod_action_unmark_as_distinguished">Премахване на отметката като разграничен
</string>
<string name="report_list_title">Списък с отчети</string>
<string name="report_list_type_title">Тип списък на отчет</string>
<string name="report_list_type_all">Всички</string>
<string name="report_list_type_unresolved">Неразрешено</string>
<string name="report_action_resolve">Разрешаване</string>
<string name="report_action_unresolve">Отмяна на разрешаването</string>
</resources>

View File

@ -235,4 +235,18 @@
rolování
</string>
<string name="action_quote">Citát</string>
<string name="mod_action_open_reports">Otevřít přehledy</string>
<string name="mod_action_mark_as_featured">Označit jako doporučené</string>
<string name="mod_action_unmark_as_featured">Zrušit označení jako doporučené</string>
<string name="mod_action_lock">Zamknout</string>
<string name="mod_action_unlock">Odemknout</string>
<string name="mod_action_remove">Odebrat</string>
<string name="mod_action_mark_as_distinguished">Označit jako význačné</string>
<string name="mod_action_unmark_as_distinguished">Zrušit označení jako rozlišující</string>
<string name="report_list_title">Seznam přehledů</string>
<string name="report_list_type_title">Typ seznamu přehledů</string>
<string name="report_list_type_all">Vše</string>
<string name="report_list_type_unresolved">Nevyřešeno</string>
<string name="report_action_resolve">Vyřešit</string>
<string name="report_action_unresolve">Nevyřešit</string>
</resources>

View File

@ -224,4 +224,18 @@
<string name="settings_mark_as_read_while_scrolling">Marker indlæg som læst, mens du ruller
</string>
<string name="action_quote">Citere</string>
<string name="mod_action_open_reports">Åbne rapporter</string>
<string name="mod_action_mark_as_featured">Markér som fremhævet</string>
<string name="mod_action_unmark_as_featured">Fjern markering som fremhævet</string>
<string name="mod_action_lock">Lås</string>
<string name="mod_action_unlock">Lås op</string>
<string name="mod_action_remove">Fjern</string>
<string name="mod_action_mark_as_distinguished">Markér som distinguished</string>
<string name="mod_action_unmark_as_distinguished">Fjern markering som distinguished</string>
<string name="report_list_title">Rapportliste</string>
<string name="report_list_type_title">Rapportlistetype</string>
<string name="report_list_type_all">Alle</string>
<string name="report_list_type_unresolved">Ikke løst</string>
<string name="report_action_resolve">Løs</string>
<string name="report_action_unresolve">Løs op</string>
</resources>

View File

@ -228,4 +228,18 @@
gelesen
</string>
<string name="action_quote">Zitat</string>
<string name="mod_action_open_reports">Berichte öffnen</string>
<string name="mod_action_mark_as_featured">Als vorgestellt markieren</string>
<string name="mod_action_unmark_as_featured">Markierung als hervorgehoben aufheben</string>
<string name="mod_action_lock">Sperre</string>
<string name="mod_action_unlock">Entsperren</string>
<string name="mod_action_remove">Entfernen</string>
<string name="mod_action_mark_as_distinguished">Als ausgezeichnet markieren</string>
<string name="mod_action_unmark_as_distinguished">Markierung als ausgezeichnet aufheben</string>
<string name="report_list_title">Berichtsliste</string>
<string name="report_list_type_title">Berichtslistentyp</string>
<string name="report_list_type_all">Alle</string>
<string name="report_list_type_unresolved">Ungelöst</string>
<string name="report_action_resolve">Auflösen</string>
<string name="report_action_unresolve">Auflösung</string>
</resources>

View File

@ -232,4 +232,19 @@
την κύλιση
</string>
<string name="action_quote">Παράσου</string>
<string name="mod_action_open_reports">Άνοιγμα αναφορών</string>
<string name="mod_action_mark_as_featured">Επισήμανση ως επιλεγμένου</string>
<string name="mod_action_unmark_as_featured">Κατάργηση επισήμανσης ως επιλεγμένου</string>
<string name="mod_action_lock">Κλείδωμα</string>
<string name="mod_action_unlock">Ξεκλείδωμα</string>
<string name="mod_action_remove">Κατάργηση</string>
<string name="mod_action_mark_as_distinguished">Επισήμανση ως διακεκριμένου</string>
<string name="mod_action_unmark_as_distinguished">Κατάργηση επισήμανσης ως διακεκριμένου
</string>
<string name="report_list_title">Λίστα αναφορών</string>
<string name="report_list_type_title">Τύπος λίστας αναφοράς</string>
<string name="report_list_type_all">Όλα</string>
<string name="report_list_type_unresolved">Μη επιλύθηκε</string>
<string name="report_action_resolve">Επίλυση</string>
<string name="report_action_unresolve">Κατάργηση επίλυσης</string>
</resources>

View File

@ -230,4 +230,18 @@
desplazarse
</string>
<string name="action_quote">Citar</string>
<string name="mod_action_open_reports">Ver informes</string>
<string name="mod_action_mark_as_featured">Marcar como destacado</string>
<string name="mod_action_unmark_as_featured">Marcar como no destacado</string>
<string name="mod_action_lock">Bloquear</string>
<string name="mod_action_unlock">Desbloquear</string>
<string name="mod_action_remove">Suprimir</string>
<string name="mod_action_mark_as_distinguished">Marcar como distinguido</string>
<string name="mod_action_unmark_as_distinguished">Marcar como no distinguido</string>
<string name="report_list_title">Lista de informes</string>
<string name="report_list_type_title">Tipo lista de informes</string>
<string name="report_list_type_all">Todos</string>
<string name="report_list_type_unresolved">No resueltos</string>
<string name="report_action_resolve">Marcar como resuelto</string>
<string name="report_action_unresolve">Marcar como no resuelto</string>
</resources>

View File

@ -243,4 +243,18 @@
<string name="settings_mark_as_read_while_scrolling">Märkige postitused kerimise ajal loetuks
</string>
<string name="action_quote">Tsiteeri</string>
<string name="mod_action_open_reports">Ava aruanded</string>
<string name="mod_action_mark_as_featured">Märkige esiletõstetuks</string>
<string name="mod_action_unmark_as_featured">Tühista esiletõstetud märkimine</string>
<string name="mod_action_lock">Lukusta</string>
<string name="mod_action_unlock">Ava lukust</string>
<string name="mod_action_remove">Eemalda</string>
<string name="mod_action_mark_as_distinguished">Märgi eristatuks</string>
<string name="mod_action_unmark_as_distinguished">Tühista eristatuks märgitud</string>
<string name="report_list_title">Aruannete loend</string>
<string name="report_list_type_title">Aruande loendi tüüp</string>
<string name="report_list_type_all">Kõik</string>
<string name="report_list_type_unresolved">Lahendamata</string>
<string name="report_action_resolve">Lahenda</string>
<string name="report_action_unresolve">Tühista lahendamine</string>
</resources>

View File

@ -234,4 +234,18 @@
<string name="settings_mark_as_read_while_scrolling">Merkitse viestit luetuiksi vieritettäessä
</string>
<string name="action_quote">Lainata</string>
<string name="mod_action_open_reports">Avaa raportit</string>
<string name="mod_action_mark_as_featured">Merkitse suositeltavaksi</string>
<string name="mod_action_unmark_as_featured">Poista suositellun merkintä</string>
<string name="mod_action_lock">Lukitse</string>
<string name="mod_action_unlock">Avaa lukitus</string>
<string name="mod_action_remove">Poista</string>
<string name="mod_action_mark_as_distinguished">Merkitse erotetuksi</string>
<string name="mod_action_unmark_as_distinguished">Poista erotetuksi</string>
<string name="report_list_title">Raporttiluettelo</string>
<string name="report_list_type_title">Raporttiluettelon tyyppi</string>
<string name="report_list_type_all">Kaikki</string>
<string name="report_list_type_unresolved">Ratkaisematon</string>
<string name="report_action_resolve">Ratkaise</string>
<string name="report_action_unresolve">Ei ratkaise</string>
</resources>

View File

@ -228,4 +228,18 @@
défilement
</string>
<string name="action_quote">Citer</string>
<string name="mod_action_open_reports">Ouvrir les rapports</string>
<string name="mod_action_mark_as_featured">Marquer comme présenté</string>
<string name="mod_action_unmark_as_featured">Ne plus marquer comme présenté</string>
<string name="mod_action_lock">Verrouiller</string>
<string name="mod_action_unlock">Déverrouiller</string>
<string name="mod_action_remove">Supprimer</string>
<string name="mod_action_mark_as_distinguished">Marquer comme distingué</string>
<string name="mod_action_unmark_as_distinguished">Ne plus marquer comme distingué</string>
<string name="report_list_title">Liste des rapports</string>
<string name="report_list_type_title">Type de liste de rapports</string>
<string name="report_list_type_all">Tous</string>
<string name="report_list_type_unresolved">Non résolu</string>
<string name="report_action_resolve">Résoudre</string>
<string name="report_action_unresolve">Ne pas résoudre</string>
</resources>

View File

@ -256,4 +256,18 @@
scrollú
</string>
<string name="action_quote">Athfhriotail</string>
<string name="mod_action_open_reports">Oscail tuarascálacha</string>
<string name="mod_action_mark_as_featured">Marcáil mar atá i gceist</string>
<string name="mod_action_unmark_as_featured">Dímharc mar atá i gceist</string>
<string name="mod_action_lock">Glasáil</string>
<string name="mod_action_unlock">Díghlasáil</string>
<string name="mod_action_remove">Bain</string>
<string name="mod_action_mark_as_distinguished">Marcáil mar aitheanta</string>
<string name="mod_action_unmark_as_distinguished">Dímharcáil mar shainaitheanta</string>
<string name="report_list_title">Liosta tuairisce</string>
<string name="report_list_type_title">Cineál liosta tuairisce</string>
<string name="report_list_type_all">Gach</string>
<string name="report_list_type_unresolved">Gan réiteach</string>
<string name="report_action_resolve">Réitigh</string>
<string name="report_action_unresolve">Díréitigh</string>
</resources>

View File

@ -246,4 +246,18 @@
pomicanja
</string>
<string name="action_quote">Citat</string>
<string name="mod_action_open_reports">Otvori izvješća</string>
<string name="mod_action_mark_as_featured">Označi kao istaknuto</string>
<string name="mod_action_unmark_as_featured">Ukloni oznaku kao istaknuto</string>
<string name="mod_action_lock">Zaključaj</string>
<string name="mod_action_unlock">Otključaj</string>
<string name="mod_action_remove">Ukloni</string>
<string name="mod_action_mark_as_distinguished">Označi kao istaknuto</string>
<string name="mod_action_unmark_as_distinguished">Ukloni oznaku kao istaknuto</string>
<string name="report_list_title">Popis izvješća</string>
<string name="report_list_type_title">Vrsta popisa izvješća</string>
<string name="report_list_type_all">Sve</string>
<string name="report_list_type_unresolved">Neriješeno</string>
<string name="report_action_resolve">Riješi</string>
<string name="report_action_unresolve">Poništi rješavanje</string>
</resources>

View File

@ -243,4 +243,20 @@
görgetés közben
</string>
<string name="action_quote">Idézet</string>
<string name="mod_action_open_reports">Jelentések megnyitása</string>
<string name="mod_action_mark_as_featured">Megjelölés kiemeltként</string>
<string name="mod_action_unmark_as_featured">Kiemeltként való megjelölés eltávolítása</string>
<string name="mod_action_lock">Zárolás</string>
<string name="mod_action_unlock">Feloldás</string>
<string name="mod_action_remove">Eltávolítás</string>
<string name="mod_action_mark_as_distinguished">Megjelölés megkülönböztetettként</string>
<string name="mod_action_unmark_as_distinguished">Megkülönböztetettként való megjelölés
eltávolítása
</string>
<string name="report_list_title">Jelentéslista</string>
<string name="report_list_type_title">Jelentéslista típusa</string>
<string name="report_list_type_all">Mind</string>
<string name="report_list_type_unresolved">Feloldatlan</string>
<string name="report_action_resolve">Megoldás</string>
<string name="report_action_unresolve">Feloldás megszüntetése</string>
</resources>

View File

@ -227,4 +227,18 @@
<string name="settings_mark_as_read_while_scrolling">Segna post come letti durante lo scroll
</string>
<string name="action_quote">Cita</string>
<string name="mod_action_open_reports">Apri segnalazioni</string>
<string name="mod_action_mark_as_featured">Segna come fissato</string>
<string name="mod_action_unmark_as_featured">Segna come non fissato</string>
<string name="mod_action_lock">Blocca</string>
<string name="mod_action_unlock">Sblocca</string>
<string name="mod_action_remove">Rimuovi</string>
<string name="mod_action_mark_as_distinguished">Segna come distinto</string>
<string name="mod_action_unmark_as_distinguished">Segna come non distinto</string>
<string name="report_list_title">Elenco segnalazioni</string>
<string name="report_list_type_title">Tipo elenco segnalazioni</string>
<string name="report_list_type_all">Tutte</string>
<string name="report_list_type_unresolved">Non risolte</string>
<string name="report_action_resolve">Segna come risolto</string>
<string name="report_action_unresolve">Segna come non risolto</string>
</resources>

View File

@ -249,4 +249,18 @@
skaitytus
</string>
<string name="action_quote">Citata</string>
<string name="mod_action_open_reports">Atidaryti ataskaitas</string>
<string name="mod_action_mark_as_featured">Pažymėti kaip siūlomą</string>
<string name="mod_action_unmark_as_featured">Panaikinti kaip siūlomo žymėjimą</string>
<string name="mod_action_lock">Užrakinti</string>
<string name="mod_action_unlock">Atrakinti</string>
<string name="mod_action_remove">Pašalinti</string>
<string name="mod_action_mark_as_distinguished">Pažymėti kaip išskirtinį</string>
<string name="mod_action_unmark_as_distinguished">Panaikinti išskirtinio žymėjimą</string>
<string name="report_list_title">Ataskaitų sąrašas</string>
<string name="report_list_type_title">Ataskaitų sąrašo tipas</string>
<string name="report_list_type_all">Visi</string>
<string name="report_list_type_unresolved">Neišspręsta</string>
<string name="report_action_resolve">Išspręsti</string>
<string name="report_action_unresolve">Neišspręsti</string>
</resources>

View File

@ -248,4 +248,18 @@
laikā
</string>
<string name="action_quote">Citāts</string>
<string name="mod_action_open_reports">Atvērt pārskatus</string>
<string name="mod_action_mark_as_featured">Atzīmēt kā piedāvātu</string>
<string name="mod_action_unmark_as_featured">Noņemiet atzīmi no piedāvātā</string>
<string name="mod_action_lock">Bloķēt</string>
<string name="mod_action_unlock">Atbloķēt</string>
<string name="mod_action_remove">Noņemt</string>
<string name="mod_action_mark_as_distinguished">Atzīmēt kā atšķirīgu</string>
<string name="mod_action_unmark_as_distinguished">Noņemt atzīmes kā atšķirīgu</string>
<string name="report_list_title">Pārskatu saraksts</string>
<string name="report_list_type_title">Pārskatu saraksta veids</string>
<string name="report_list_type_all">Visi</string>
<string name="report_list_type_unresolved">Neatrisināts</string>
<string name="report_action_resolve">Atrisināt</string>
<string name="report_action_unresolve">Neatrisināts</string>
</resources>

View File

@ -235,4 +235,18 @@
scrollen
</string>
<string name="action_quote">Citaat</string>
<string name="mod_action_open_reports">Open rapporten</string>
<string name="mod_action_mark_as_featured">Markeren als aanbevolen</string>
<string name="mod_action_unmark_as_featured">Markeren als aanbevolen opheffen</string>
<string name="mod_action_lock">Vergrendelen</string>
<string name="mod_action_unlock">Ontgrendelen</string>
<string name="mod_action_remove">Verwijderen</string>
<string name="mod_action_mark_as_distinguished">Markeren als onderscheiden</string>
<string name="mod_action_unmark_as_distinguished">Markeren als onderscheidend</string>
<string name="report_list_title">Rapportlijst</string>
<string name="report_list_type_title">Rapportlijsttype</string>
<string name="report_list_type_all">Alles</string>
<string name="report_list_type_unresolved">Onopgelost</string>
<string name="report_action_resolve">Oplossen</string>
<string name="report_action_unresolve">Onopgelost</string>
</resources>

View File

@ -237,4 +237,18 @@
<string name="settings_mark_as_read_while_scrolling">Merk innlegg som lest mens du ruller
</string>
<string name="action_quote">Sitat</string>
<string name="mod_action_open_reports">Åpne rapporter</string>
<string name="mod_action_mark_as_featured">Merk som fremhevet</string>
<string name="mod_action_unmark_as_featured">Fjern merking som fremhevet</string>
<string name="mod_action_lock">Lås</string>
<string name="mod_action_unlock">Lås opp</string>
<string name="mod_action_remove">Fjern</string>
<string name="mod_action_mark_as_distinguished">Merk som distinguished</string>
<string name="mod_action_unmark_as_distinguished">Fjern merking som distinguished</string>
<string name="report_list_title">Rapportliste</string>
<string name="report_list_type_title">Rapportlistetype</string>
<string name="report_list_type_all">Alle</string>
<string name="report_list_type_unresolved">Uløst</string>
<string name="report_action_resolve">Løs</string>
<string name="report_action_unresolve">Løs opp</string>
</resources>

View File

@ -226,4 +226,18 @@
przewijania
</string>
<string name="action_quote">Cytat</string>
<string name="mod_action_open_reports">Otwórz raporty</string>
<string name="mod_action_mark_as_featured">Oznacz jako polecane</string>
<string name="mod_action_unmark_as_featured">Odznacz jako polecane</string>
<string name="mod_action_lock">Zablokuj</string>
<string name="mod_action_unlock">Odblokuj</string>
<string name="mod_action_remove">Usuń</string>
<string name="mod_action_mark_as_distinguished">Oznacz jako wyróżnione</string>
<string name="mod_action_unmark_as_distinguished">Odznacz jako wyróżnione</string>
<string name="report_list_title">Lista raportów</string>
<string name="report_list_type_title">Typ listy raportów</string>
<string name="report_list_type_all">Wszystkie</string>
<string name="report_list_type_unresolved">Nierozwiązany</string>
<string name="report_action_resolve">Rozwiąż</string>
<string name="report_action_unresolve">Cofnij rozwiązanie</string>
</resources>

View File

@ -225,4 +225,18 @@
rolar
</string>
<string name="action_quote">Citar</string>
<string name="mod_action_open_reports">Abrir relatórios</string>
<string name="mod_action_mark_as_featured">Marcar como destacado</string>
<string name="mod_action_unmark_as_featured">Desmarcar como destacado</string>
<string name="mod_action_lock">Bloquear</string>
<string name="mod_action_unlock">Desbloquear</string>
<string name="mod_action_remove">Remover</string>
<string name="mod_action_mark_as_distinguished">Marcar como distinto</string>
<string name="mod_action_unmark_as_distinguished">Desmarcar como distinto</string>
<string name="report_list_title">Lista de relatórios</string>
<string name="report_list_type_title">Tipo de lista de relatório</string>
<string name="report_list_type_all">Todos</string>
<string name="report_list_type_unresolved">Não resolvido</string>
<string name="report_action_resolve">Resolver</string>
<string name="report_action_unresolve">Não resolver</string>
</resources>

View File

@ -225,4 +225,18 @@
<string name="settings_mark_as_read_while_scrolling">Marchează postările ca citite la derulare
</string>
<string name="action_quote">Citează</string>
<string name="mod_action_open_reports">Deschide rapoarte</string>
<string name="mod_action_mark_as_featured">Marchează ca recomandat</string>
<string name="mod_action_unmark_as_featured">Anulează marcare ca recomandat</string>
<string name="mod_action_lock">Blochează</string>
<string name="mod_action_unlock">Deblochează</string>
<string name="mod_action_remove">Elimină</string>
<string name="mod_action_mark_as_distinguished">Marchează ca distins</string>
<string name="mod_action_unmark_as_distinguished">Anulează marcare ca distins</string>
<string name="report_list_title">Lista de rapoarte</string>
<string name="report_list_type_title">Tipul listei de rapoarte</string>
<string name="report_list_type_all">Toate</string>
<string name="report_list_type_unresolved">Nerezolvate</string>
<string name="report_action_resolve">Marchează ca rezolvat</string>
<string name="report_action_unresolve">Anulează marcare ca rezolvat</string>
</resources>

View File

@ -236,4 +236,18 @@
<string name="settings_mark_as_read_while_scrolling">Markera inlägg som lästa medan du rullar
</string>
<string name="action_quote">Citat</string>
<string name="mod_action_open_reports">Öppna rapporter</string>
<string name="mod_action_mark_as_featured">Markera som utvald</string>
<string name="mod_action_unmark_as_featured">Avmarkera som utvald</string>
<string name="mod_action_lock">Lås</string>
<string name="mod_action_unlock">Lås upp</string>
<string name="mod_action_remove">Ta bort</string>
<string name="mod_action_mark_as_distinguished">Markera som distinguished</string>
<string name="mod_action_unmark_as_distinguished">Avmarkera som distinguished</string>
<string name="report_list_title">Rapportlista</string>
<string name="report_list_type_title">Rapportlistatyp</string>
<string name="report_list_type_all">Alla</string>
<string name="report_list_type_unresolved">Olöst</string>
<string name="report_action_resolve">Lös</string>
<string name="report_action_unresolve">Avlös</string>
</resources>

View File

@ -237,4 +237,18 @@
rolovaní
</string>
<string name="action_quote">Citovať</string>
<string name="mod_action_open_reports">Otvoriť prehľady</string>
<string name="mod_action_mark_as_featured">Označiť ako odporúčané</string>
<string name="mod_action_unmark_as_featured">Zrušiť označenie ako odporúčané</string>
<string name="mod_action_lock">Uzamknúť</string>
<string name="mod_action_unlock">Odomknúť</string>
<string name="mod_action_remove">Odstrániť</string>
<string name="mod_action_mark_as_distinguished">Označiť ako odlíšené</string>
<string name="mod_action_unmark_as_distinguished">Zrušiť označenie ako odlíšené</string>
<string name="report_list_title">Zoznam prehľadov</string>
<string name="report_list_type_title">Typ zoznamu prehľadov</string>
<string name="report_list_type_all">Všetky</string>
<string name="report_list_type_unresolved">Nevyriešené</string>
<string name="report_action_resolve">Vyriešiť</string>
<string name="report_action_unresolve">Nevyriešiť</string>
</resources>

View File

@ -241,4 +241,18 @@
<string name="settings_mark_as_read_while_scrolling">Med pomikanjem označi objave kot prebrane
</string>
<string name="action_quote">Kvota</string>
<string name="mod_action_open_reports">Odpri poročila</string>
<string name="mod_action_mark_as_featured">Označi kot predstavljeno</string>
<string name="mod_action_unmark_as_featured">Odznači kot predstavljeno</string>
<string name="mod_action_lock">Zakleni</string>
<string name="mod_action_unlock">Odkleni</string>
<string name="mod_action_remove">Odstrani</string>
<string name="mod_action_mark_as_distinguished">Označi kot prepoznavno</string>
<string name="mod_action_unmark_as_distinguished">Odznači kot razločeno</string>
<string name="report_list_title">Seznam poročil</string>
<string name="report_list_type_title">Vrsta seznama poročil</string>
<string name="report_list_type_all">Vse</string>
<string name="report_list_type_unresolved">Nerazrešeno</string>
<string name="report_action_resolve">Reši</string>
<string name="report_action_unresolve">Prekliči razrešitev</string>
</resources>