feat: report post and comments

This commit is contained in:
Diego Beraldin 2023-10-23 23:37:02 +02:00
parent 42b1ef5a9a
commit 745755ff88
29 changed files with 759 additions and 211 deletions

View File

@ -0,0 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CommentReport(
@SerialName("id") val id: CommentReportId,
@SerialName("creator_id") val creatorId: PersonId,
@SerialName("comment_id") val commentId: CommentId,
@SerialName("original_comment_text") val originalCommentText: String,
@SerialName("reason") val reason: String,
@SerialName("resolved") val resolved: Boolean,
@SerialName("resolver_id") val resolverId: PersonId? = null,
@SerialName("published") val published: String,
@SerialName("updated") val updated: String? = null,
)

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 CommentReportResponse(
@SerialName("comment_report_view") val commentReportView: CommentReportView,
)

View File

@ -0,0 +1,18 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CommentReportView(
@SerialName("comment_report") val commentReport: CommentReport,
@SerialName("comment") val comment: Comment,
@SerialName("post") val post: Post,
@SerialName("community") val community: Community,
@SerialName("creator") val creator: Person,
@SerialName("comment_creator") val commentCreator: Person,
@SerialName("counts") val counts: CommentAggregates,
@SerialName("creator_banned_from_community") val creatorBannedFromCommunity: Boolean,
@SerialName("my_vote") val myVote: Int? = null,
@SerialName("resolver") val resolver: Person? = null,
)

View File

@ -12,4 +12,6 @@ typealias LocalUserId = Int
typealias CustomEmojiId = Int typealias CustomEmojiId = Int
typealias PersonMentionId = Int typealias PersonMentionId = Int
typealias CommentReplyId = Int typealias CommentReplyId = Int
typealias CommentReportId = Int
typealias PostReportId = Int
typealias PrivateMessageId = Int typealias PrivateMessageId = Int

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 CreateCommentReportForm(
@SerialName("comment_id") val commentId: CommentId,
@SerialName("reason") val reason: String,
@SerialName("auto") 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 CreatePostReportForm(
@SerialName("post_id") val postId: PostId,
@SerialName("reason") val reason: String,
@SerialName("auto") val auth: String,
)

View File

@ -0,0 +1,19 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PostReport(
@SerialName("id") val id: PostReportId,
@SerialName("creator_id") val creatorId: PersonId,
@SerialName("post_id") val postId: PostId,
@SerialName("original_post_name") val originalPostName: String,
@SerialName("original_post_url") val originalPostUrl: String? = null,
@SerialName("original_post_body") val originalPostBody: String? = null,
@SerialName("reason") val reason: String,
@SerialName("resolved") val resolved: Boolean,
@SerialName("resolver_id") val resolverId: PersonId? = null,
@SerialName("published") val published: String,
@SerialName("updated") val updated: String? = null,
)

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 PostReportResponse(
@SerialName("post_report_view") val postReportView: PostReportView,
)

View File

@ -0,0 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PostReportView(
@SerialName("post_report") val postReport: PostReport,
@SerialName("post") val post: Post,
@SerialName("community") val community: Community,
@SerialName("creator") val creator: Person,
@SerialName("post_creator") val postCreator: Person,
@SerialName("creator_banned_from_community") val creatorBannedFromCommunity: Boolean,
@SerialName("my_vote") val myVote: Int? = null,
@SerialName("counts") val counts: PostAggregates,
@SerialName("resolver") val resolver: Person? = null,
)

View File

@ -1,10 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.core.api.service package com.github.diegoberaldin.raccoonforlemmy.core.api.service
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReplyResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReplyResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReportResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentSortType import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentSortType
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreateCommentForm 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.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.DeleteCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditCommentForm 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.GetCommentResponse
@ -86,4 +88,11 @@ interface CommentService {
@Header("Authorization") authHeader: String? = null, @Header("Authorization") authHeader: String? = null,
@Body form: DeleteCommentForm, @Body form: DeleteCommentForm,
): Response<CommentResponse> ): Response<CommentResponse>
@POST("comment/report")
@Headers("Content-Type: application/json")
suspend fun createReport(
@Body form: CreateCommentReportForm,
@Header("Authorization") authHeader: String? = null,
): Response<CommentReportResponse>
} }

View File

@ -3,6 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.core.api.service
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReplyResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReplyResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostLikeForm 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.DeletePostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditPostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditPostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetPostResponse import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetPostResponse
@ -10,6 +11,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetPostsResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.ListingType
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPostAsReadForm 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.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.PostResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SavePostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SavePostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType
@ -97,4 +99,11 @@ interface PostService {
@Header("Authorization") authHeader: String? = null, @Header("Authorization") authHeader: String? = null,
@Body content: MultiPartFormDataContent, @Body content: MultiPartFormDataContent,
): Response<PictrsImages> ): Response<PictrsImages>
@POST("post/report")
@Headers("Content-Type: application/json")
suspend fun createReport(
@Header("Authorization") authHeader: String? = null,
@Body form: CreatePostReportForm,
): Response<PostReportResponse>
} }

View File

@ -11,6 +11,7 @@ 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.instanceinfo.InstanceInfoMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.NavigationCoordinator 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.PostDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
@ -122,3 +123,13 @@ actual fun getModalDrawerViewModel(): ModalDrawerMviModel {
val res: ModalDrawerMviModel by inject(ModalDrawerMviModel::class.java) val res: ModalDrawerMviModel by inject(ModalDrawerMviModel::class.java)
return res return res
} }
actual fun getCreateReportViewModel(
postId: Int?,
commentId: Int?,
): CreateReportMviModel {
val res: CreateReportMviModel by inject(CreateReportMviModel::class.java, parameters = {
parametersOf(postId, commentId)
})
return res
}

View File

@ -84,6 +84,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImag
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.instanceinfo.InstanceInfoScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.instanceinfo.InstanceInfoScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet 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.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -427,6 +428,7 @@ class CommunityDetailScreen(
options = buildList { options = buildList {
add(stringResource(MR.strings.post_action_share)) add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_hide)) add(stringResource(MR.strings.post_action_hide))
add(stringResource(MR.strings.post_action_report))
if (post.creator?.id == uiState.currentUserId && !isOnOtherInstance) { if (post.creator?.id == uiState.currentUserId && !isOnOtherInstance) {
add(stringResource(MR.strings.post_action_edit)) add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete)) add(stringResource(MR.strings.comment_action_delete))
@ -493,13 +495,13 @@ class CommunityDetailScreen(
}, },
onOptionSelected = { optionIdx -> onOptionSelected = { optionIdx ->
when (optionIdx) { when (optionIdx) {
3 -> model.reduce( 4 -> model.reduce(
CommunityDetailMviModel.Intent.DeletePost( CommunityDetailMviModel.Intent.DeletePost(
post.id post.id
) )
) )
2 -> { 3 -> {
notificationCenter.addObserver( notificationCenter.addObserver(
{ {
model.reduce(CommunityDetailMviModel.Intent.Refresh) model.reduce(CommunityDetailMviModel.Intent.Refresh)
@ -514,6 +516,14 @@ class CommunityDetailScreen(
) )
} }
2 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
1 -> model.reduce( 1 -> model.reduce(
CommunityDetailMviModel.Intent.Hide( CommunityDetailMviModel.Intent.Hide(
idx idx

View File

@ -23,6 +23,8 @@ 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.navigation.NavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailMviModel 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.postdetail.PostDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel 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.saveditems.SavedItemsViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel
@ -172,4 +174,14 @@ val commonUiModule = module {
settingsRepository = get(), settingsRepository = get(),
) )
} }
factory<CreateReportMviModel> {
CreateReportViewModel(
postId = it[0],
commentId = it[1],
mvi = DefaultMviModel(CreateReportMviModel.UiState()),
identityRepository = get(),
postRepository = get(),
commentRepository = get(),
)
}
} }

View File

@ -11,6 +11,7 @@ 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.instanceinfo.InstanceInfoMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.NavigationCoordinator 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.PostDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
@ -62,4 +63,9 @@ expect fun getInboxChatViewModel(otherUserId: Int): InboxChatMviModel
expect fun getSavedItemsViewModel(): SavedItemsMviModel expect fun getSavedItemsViewModel(): SavedItemsMviModel
expect fun getModalDrawerViewModel(): ModalDrawerMviModel expect fun getModalDrawerViewModel(): ModalDrawerMviModel
expect fun getCreateReportViewModel(
postId: Int? = null,
commentId: Int? = null,
): CreateReportMviModel

View File

@ -87,6 +87,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCo
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getPostDetailViewModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getPostDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -302,6 +303,7 @@ class PostDetailScreen(
}, },
options = buildList { options = buildList {
add(stringResource(MR.strings.post_action_share)) add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_report))
if (statePost.creator?.id == uiState.currentUserId && !isOnOtherInstance) { if (statePost.creator?.id == uiState.currentUserId && !isOnOtherInstance) {
add(stringResource(MR.strings.post_action_edit)) add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete)) add(stringResource(MR.strings.comment_action_delete))
@ -347,7 +349,9 @@ class PostDetailScreen(
}, },
onOptionSelected = { idx -> onOptionSelected = { idx ->
when (idx) { when (idx) {
1 -> { 3 -> model.reduce(PostDetailMviModel.Intent.DeletePost)
2 -> {
notificationCenter.addObserver( notificationCenter.addObserver(
{ {
model.reduce(PostDetailMviModel.Intent.RefreshPost) model.reduce(PostDetailMviModel.Intent.RefreshPost)
@ -360,7 +364,9 @@ class PostDetailScreen(
) )
} }
2 -> model.reduce(PostDetailMviModel.Intent.DeletePost) 1 -> {
bottomSheetNavigator.show(CreateReportScreen(postId = statePost.id))
}
else -> model.reduce(PostDetailMviModel.Intent.SharePost) else -> model.reduce(PostDetailMviModel.Intent.SharePost)
} }
@ -501,6 +507,7 @@ class PostDetailScreen(
separateUpAndDownVotes = uiState.separateUpAndDownVotes, separateUpAndDownVotes = uiState.separateUpAndDownVotes,
autoLoadImages = uiState.autoLoadImages, autoLoadImages = uiState.autoLoadImages,
options = buildList { options = buildList {
add(stringResource(MR.strings.post_action_report))
if (comment.creator?.id == uiState.currentUserId) { if (comment.creator?.id == uiState.currentUserId) {
add(stringResource(MR.strings.post_action_edit)) add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete)) add(stringResource(MR.strings.comment_action_delete))
@ -575,15 +582,15 @@ class PostDetailScreen(
) )
} }
}, },
onOptionSelected = { idx -> onOptionSelected = { optionId ->
when (idx) { when (optionId) {
1 -> model.reduce( 2 -> model.reduce(
PostDetailMviModel.Intent.DeleteComment( PostDetailMviModel.Intent.DeleteComment(
comment.id comment.id
) )
) )
else -> { 1 -> {
notificationCenter.addObserver( notificationCenter.addObserver(
{ {
model.reduce(PostDetailMviModel.Intent.Refresh) model.reduce(PostDetailMviModel.Intent.Refresh)
@ -598,6 +605,14 @@ class PostDetailScreen(
) )
) )
} }
else -> {
bottomSheetNavigator.show(
CreateReportScreen(
commentId = comment.id
)
)
}
} }
}) })
}, },

View File

@ -0,0 +1,27 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.report
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import dev.icerock.moko.resources.desc.StringDesc
interface CreateReportMviModel :
MviModel<CreateReportMviModel.Intent, CreateReportMviModel.UiState, CreateReportMviModel.Effect>,
ScreenModel {
sealed interface Intent {
data class SetText(val value: String) : Intent
data object Send : 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,166 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.report
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.material.icons.Icons
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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 cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
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.getCreateReportViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
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 CreateReportScreen(
private val postId: Int? = null,
private val commentId: Int? = null,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = rememberScreenModel {
getCreateReportViewModel(
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 bottomSheetNavigator = LocalBottomSheetNavigator.current
val notificationCenter = remember { getNotificationCenter() }
LaunchedEffect(model) {
model.effects.onEach {
when (it) {
is CreateReportMviModel.Effect.Failure -> {
snackbarHostState.showSnackbar(it.message ?: genericError)
}
CreateReportMviModel.Effect.Success -> {
bottomSheetNavigator.hide()
}
}
}.launchIn(this)
}
Box(
contentAlignment = Alignment.BottomCenter,
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.s),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = Modifier.fillMaxWidth().padding(top = Spacing.s),
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,
)
}
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(CreateReportMviModel.Intent.SetText(value))
},
isError = uiState.textError != null,
supportingText = {
if (uiState.textError != null) {
Text(
text = uiState.textError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
)
}
},
trailingIcon = {
IconButton(
content = {
Icon(
imageVector = Icons.Default.Send,
contentDescription = null,
)
},
onClick = {
model.reduce(CreateReportMviModel.Intent.Send)
},
)
})
Spacer(Modifier.height(Spacing.xxl))
}
if (uiState.loading) {
ProgressHud()
}
SnackbarHost(
modifier = Modifier.padding(bottom = Spacing.xxxl),
hostState = snackbarHostState
)
}
}
}

View File

@ -0,0 +1,66 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.report
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
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 CreateReportViewModel(
private val postId: Int?,
private val commentId: Int?,
private val mvi: DefaultMviModel<CreateReportMviModel.Intent, CreateReportMviModel.UiState, CreateReportMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
) : CreateReportMviModel,
MviModel<CreateReportMviModel.Intent, CreateReportMviModel.UiState, CreateReportMviModel.Effect> by mvi {
override fun reduce(intent: CreateReportMviModel.Intent) {
when (intent) {
is CreateReportMviModel.Intent.SetText -> {
mvi.updateState {
it.copy(text = intent.value)
}
}
CreateReportMviModel.Intent.Send -> 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.report(
postId = postId,
reason = text,
auth = auth,
)
} else if (commentId != null) {
commentRepository.report(
commentId = commentId,
reason = text,
auth = auth,
)
}
mvi.emitEffect(CreateReportMviModel.Effect.Success)
} catch (e: Throwable) {
val message = e.message
mvi.emitEffect(CreateReportMviModel.Effect.Failure(message))
} finally {
mvi.updateState { it.copy(loading = false) }
}
}
}
}

View File

@ -84,6 +84,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getUserDetailVi
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet 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.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
@ -390,6 +391,7 @@ class UserDetailScreen(
autoLoadImages = uiState.autoLoadImages, autoLoadImages = uiState.autoLoadImages,
options = buildList { options = buildList {
add(stringResource(MR.strings.post_action_share)) add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_report))
}, },
onUpVote = if (isOnOtherInstance) { onUpVote = if (isOnOtherInstance) {
null null
@ -454,6 +456,14 @@ class UserDetailScreen(
}, },
onOptionSelected = { optionIdx -> onOptionSelected = { optionIdx ->
when (optionIdx) { when (optionIdx) {
1 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
else -> model.reduce( else -> model.reduce(
UserDetailMviModel.Intent.SharePost(idx) UserDetailMviModel.Intent.SharePost(idx)
) )
@ -599,6 +609,20 @@ class UserDetailScreen(
onOpenCommunity = { community -> onOpenCommunity = { community ->
navigator?.push(CommunityDetailScreen(community)) navigator?.push(CommunityDetailScreen(community))
}, },
options = buildList {
add(stringResource(MR.strings.post_action_report))
},
onOptionSelected = { optionId ->
when (optionId) {
else -> {
bottomSheetNavigator.show(
CreateReportScreen(
commentId = comment.id
)
)
}
}
},
) )
Divider( Divider(
modifier = Modifier.padding(vertical = Spacing.xxxs), modifier = Modifier.padding(vertical = Spacing.xxxs),

View File

@ -11,6 +11,7 @@ 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.instanceinfo.InstanceInfoMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.navigation.NavigationCoordinator 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.PostDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.saveditems.SavedItemsMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
@ -71,6 +72,11 @@ actual fun getSavedItemsViewModel(): SavedItemsMviModel =
actual fun getModalDrawerViewModel(): ModalDrawerMviModel = actual fun getModalDrawerViewModel(): ModalDrawerMviModel =
CommonUiViewModelHelper.modalDrawerViewModel CommonUiViewModelHelper.modalDrawerViewModel
actual fun getCreateReportViewModel(
postId: Int?,
commentId: Int?,
): CreateReportMviModel = CommonUiViewModelHelper.getCreateReportModel(postId, commentId)
object CommonUiViewModelHelper : KoinComponent { object CommonUiViewModelHelper : KoinComponent {
val navigationCoordinator: NavigationCoordinator by inject() val navigationCoordinator: NavigationCoordinator by inject()
@ -145,4 +151,14 @@ object CommonUiViewModelHelper : KoinComponent {
) )
return model return model
} }
fun getCreateReportModel(
postId: Int?,
commentId: Int?,
): CreateReportMviModel {
val model: CreateReportMviModel by inject(
parameters = { parametersOf(postId, commentId) }
)
return model
}
} }

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreateCommentForm 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.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.DeleteCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditCommentForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm
@ -247,4 +248,16 @@ class CommentRepository(
) )
services.comment.delete(authHeader = auth.toAuthHeader(), form = data) services.comment.delete(authHeader = auth.toAuthHeader(), form = data)
} }
suspend fun report(commentId: Int, reason: String, auth: String) {
val data = CreateCommentReportForm(
commentId = commentId,
reason = reason,
auth = auth,
)
services.comment.createReport(
form = data,
authHeader = auth.toAuthHeader(),
)
}
} }

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePostLikeForm 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.DeletePostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditPostForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.EditPostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPostAsReadForm import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPostAsReadForm
@ -246,4 +247,16 @@ class PostRepository(
e.printStackTrace() e.printStackTrace()
null null
} }
suspend fun report(postId: Int, reason: String, auth: String) {
val data = CreatePostReportForm(
postId = postId,
reason = reason,
auth = auth,
)
services.post.createReport(
form = data,
authHeader = auth.toAuthHeader(),
)
}
} }

View File

@ -71,6 +71,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImag
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.ListingTypeBottomSheet import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.ListingTypeBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet 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.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -306,6 +307,7 @@ class PostListScreen : Screen {
options = buildList { options = buildList {
add(stringResource(MR.strings.post_action_share)) add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_hide)) add(stringResource(MR.strings.post_action_hide))
add(stringResource(MR.strings.post_action_report))
if (post.creator?.id == uiState.currentUserId) { if (post.creator?.id == uiState.currentUserId) {
add(stringResource(MR.strings.post_action_edit)) add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete)) add(stringResource(MR.strings.comment_action_delete))
@ -367,13 +369,13 @@ class PostListScreen : Screen {
}, },
onOptionSelected = { optionIdx -> onOptionSelected = { optionIdx ->
when (optionIdx) { when (optionIdx) {
3 -> model.reduce( 4 -> model.reduce(
PostListMviModel.Intent.DeletePost( PostListMviModel.Intent.DeletePost(
post.id post.id
) )
) )
2 -> { 3 -> {
notificationCenter.addObserver( notificationCenter.addObserver(
{ {
model.reduce(PostListMviModel.Intent.Refresh) model.reduce(PostListMviModel.Intent.Refresh)
@ -388,6 +390,14 @@ class PostListScreen : Screen {
) )
} }
2 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
1 -> model.reduce( 1 -> model.reduce(
PostListMviModel.Intent.Hide(idx) PostListMviModel.Intent.Hide(idx)
) )

View File

@ -18,10 +18,10 @@ import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
@ -65,7 +65,6 @@ class LoginBottomSheet : Screen {
private const val HELP_URL = "https://join-lemmy.org/docs/users/01-getting-started.html" private const val HELP_URL = "https://join-lemmy.org/docs/users/01-getting-started.html"
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
override fun Content() { override fun Content() {
val model = rememberScreenModel { getLoginBottomSheetViewModel() } val model = rememberScreenModel { getLoginBottomSheetViewModel() }
@ -75,7 +74,6 @@ class LoginBottomSheet : Screen {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val genericError = stringResource(MR.strings.message_generic_error) val genericError = stringResource(MR.strings.message_generic_error)
val bottomSheetNavigator = LocalBottomSheetNavigator.current val bottomSheetNavigator = LocalBottomSheetNavigator.current
val successfulLoginMessage = stringResource(MR.strings.message_login_successful)
LaunchedEffect(model) { LaunchedEffect(model) {
model.effects.onEach { model.effects.onEach {
@ -87,7 +85,6 @@ class LoginBottomSheet : Screen {
} }
LoginBottomSheetMviModel.Effect.LoginSuccess -> { LoginBottomSheetMviModel.Effect.LoginSuccess -> {
snackbarHostState.showSnackbar(message = successfulLoginMessage)
bottomSheetNavigator.hide() bottomSheetNavigator.hide()
} }
} }
@ -98,214 +95,223 @@ class LoginBottomSheet : Screen {
val navigator = remember { getNavigationCoordinator().getRootNavigator() } val navigator = remember { getNavigationCoordinator().getRootNavigator() }
val settingsRepository = remember { getSettingsRepository() } val settingsRepository = remember { getSettingsRepository() }
Column( Box(
modifier = Modifier.padding( contentAlignment = Alignment.BottomCenter,
top = Spacing.s,
start = Spacing.s,
end = Spacing.s,
bottom = Spacing.m,
),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Box { Column(
Column( modifier = Modifier.padding(
modifier = Modifier.fillMaxWidth(), top = Spacing.s,
horizontalAlignment = Alignment.CenterHorizontally, start = Spacing.s,
) { end = Spacing.s,
BottomSheetHandle() bottom = Spacing.m,
Text(
modifier = Modifier.padding(start = Spacing.s, top = Spacing.s),
text = stringResource(MR.strings.profile_button_login),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
IconButton(
modifier = Modifier.align(Alignment.TopEnd),
onClick = {
bottomSheetNavigator.hide()
handleUrl(
url = HELP_URL,
openExternal = settingsRepository.currentSettings.value.openUrlsInExternalBrowser,
uriHandler = uriHandler,
navigator = navigator
)
},
) {
Icon(
imageVector = Icons.Default.HelpOutline,
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
val instanceFocusRequester = remember { FocusRequester() }
val usernameFocusRequester = remember { FocusRequester() }
val passwordFocusRequester = remember { FocusRequester() }
val tokenFocusRequester = remember { FocusRequester() }
TextField(
modifier = Modifier.focusRequester(instanceFocusRequester),
label = {
Text(text = stringResource(MR.strings.login_field_instance_name))
},
singleLine = true,
value = uiState.instanceName,
isError = uiState.instanceNameError != null,
keyboardActions = KeyboardActions(
onNext = {
usernameFocusRequester.requestFocus()
},
), ),
keyboardOptions = KeyboardOptions( verticalArrangement = Arrangement.spacedBy(Spacing.s),
keyboardType = KeyboardType.Email, horizontalAlignment = Alignment.CenterHorizontally,
autoCorrect = false, ) {
imeAction = ImeAction.Next, Box {
), Column(
onValueChange = { value -> modifier = Modifier.fillMaxWidth(),
model.reduce(LoginBottomSheetMviModel.Intent.SetInstanceName(value)) horizontalAlignment = Alignment.CenterHorizontally,
}, ) {
supportingText = { BottomSheetHandle()
if (uiState.instanceNameError != null) {
Text( Text(
text = uiState.instanceNameError?.localized().orEmpty(), modifier = Modifier.padding(start = Spacing.s, top = Spacing.s),
color = MaterialTheme.colorScheme.error, text = stringResource(MR.strings.profile_button_login),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
) )
} }
}, IconButton(
) modifier = Modifier.align(Alignment.TopEnd),
onClick = {
TextField( bottomSheetNavigator.hide()
modifier = Modifier.focusRequester(usernameFocusRequester), handleUrl(
label = { url = HELP_URL,
Text(text = stringResource(MR.strings.login_field_user_name)) openExternal = settingsRepository.currentSettings.value.openUrlsInExternalBrowser,
}, uriHandler = uriHandler,
singleLine = true, navigator = navigator
value = uiState.username, )
isError = uiState.usernameError != null, },
keyboardActions = KeyboardActions( ) {
onNext = { Icon(
passwordFocusRequester.requestFocus() imageVector = Icons.Default.HelpOutline,
}, contentDescription = null,
), tint = MaterialTheme.colorScheme.onBackground,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
autoCorrect = false,
imeAction = ImeAction.Next,
),
onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetUsername(value))
},
supportingText = {
if (uiState.usernameError != null) {
Text(
text = uiState.usernameError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
) )
} }
}, }
)
var transformation: VisualTransformation by remember { val instanceFocusRequester = remember { FocusRequester() }
mutableStateOf(PasswordVisualTransformation()) val usernameFocusRequester = remember { FocusRequester() }
} val passwordFocusRequester = remember { FocusRequester() }
TextField( val tokenFocusRequester = remember { FocusRequester() }
modifier = Modifier.focusRequester(passwordFocusRequester),
label = { TextField(
Text(text = stringResource(MR.strings.login_field_password)) modifier = Modifier.focusRequester(instanceFocusRequester),
}, label = {
singleLine = true, Text(text = stringResource(MR.strings.login_field_instance_name))
value = uiState.password,
isError = uiState.passwordError != null,
keyboardActions = KeyboardActions(
onNext = {
tokenFocusRequester.requestFocus()
}, },
), singleLine = true,
keyboardOptions = KeyboardOptions( value = uiState.instanceName,
keyboardType = KeyboardType.Password, isError = uiState.instanceNameError != null,
imeAction = ImeAction.Next, keyboardActions = KeyboardActions(
), onNext = {
onValueChange = { value -> usernameFocusRequester.requestFocus()
model.reduce(LoginBottomSheetMviModel.Intent.SetPassword(value)) },
}, ),
visualTransformation = transformation, keyboardOptions = KeyboardOptions(
trailingIcon = { keyboardType = KeyboardType.Email,
Image( autoCorrect = false,
modifier = Modifier.onClick { imeAction = ImeAction.Next,
transformation = if (transformation == VisualTransformation.None) { ),
PasswordVisualTransformation() onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetInstanceName(value))
},
supportingText = {
if (uiState.instanceNameError != null) {
Text(
text = uiState.instanceNameError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
)
}
},
)
TextField(
modifier = Modifier.focusRequester(usernameFocusRequester),
label = {
Text(text = stringResource(MR.strings.login_field_user_name))
},
singleLine = true,
value = uiState.username,
isError = uiState.usernameError != null,
keyboardActions = KeyboardActions(
onNext = {
passwordFocusRequester.requestFocus()
},
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
autoCorrect = false,
imeAction = ImeAction.Next,
),
onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetUsername(value))
},
supportingText = {
if (uiState.usernameError != null) {
Text(
text = uiState.usernameError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
)
}
},
)
var transformation: VisualTransformation by remember {
mutableStateOf(PasswordVisualTransformation())
}
TextField(
modifier = Modifier.focusRequester(passwordFocusRequester),
label = {
Text(text = stringResource(MR.strings.login_field_password))
},
singleLine = true,
value = uiState.password,
isError = uiState.passwordError != null,
keyboardActions = KeyboardActions(
onNext = {
tokenFocusRequester.requestFocus()
},
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next,
),
onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetPassword(value))
},
visualTransformation = transformation,
trailingIcon = {
Image(
modifier = Modifier.onClick {
transformation = if (transformation == VisualTransformation.None) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
}
},
imageVector = if (transformation == VisualTransformation.None) {
Icons.Default.VisibilityOff
} else { } else {
VisualTransformation.None Icons.Default.Visibility
} },
}, contentDescription = null,
imageVector = if (transformation == VisualTransformation.None) { colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onBackground),
Icons.Default.VisibilityOff
} else {
Icons.Default.Visibility
},
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onBackground),
)
},
supportingText = {
if (uiState.passwordError != null) {
Text(
text = uiState.passwordError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
) )
} },
}, supportingText = {
) if (uiState.passwordError != null) {
Text(
text = uiState.passwordError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
)
}
},
)
TextField( TextField(
modifier = Modifier.focusRequester(tokenFocusRequester), modifier = Modifier.focusRequester(tokenFocusRequester),
label = { label = {
Row(
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
verticalAlignment = Alignment.Bottom,
) {
Text(text = stringResource(MR.strings.login_field_token))
Text(
text = stringResource(MR.strings.login_field_label_optional),
style = MaterialTheme.typography.labelSmall,
)
}
},
singleLine = true,
value = uiState.totp2faToken,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetTotp2faToken(value))
},
visualTransformation = PasswordVisualTransformation(),
)
Spacer(modifier = Modifier.height(Spacing.m))
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
model.reduce(LoginBottomSheetMviModel.Intent.Confirm)
},
) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(Spacing.s), horizontalArrangement = Arrangement.spacedBy(Spacing.s),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(text = stringResource(MR.strings.login_field_token)) if (uiState.loading) {
Text( CircularProgressIndicator(
text = stringResource(MR.strings.login_field_label_optional), modifier = Modifier.size(20.dp),
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary,
) )
}
Text(stringResource(MR.strings.button_confirm))
} }
},
singleLine = true,
value = uiState.totp2faToken,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetTotp2faToken(value))
},
visualTransformation = PasswordVisualTransformation(),
)
Spacer(modifier = Modifier.height(Spacing.m))
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
model.reduce(LoginBottomSheetMviModel.Intent.Confirm)
},
) {
Row(
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
if (uiState.loading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary,
)
}
Text(stringResource(MR.strings.button_confirm))
} }
Spacer(modifier = Modifier.height(Spacing.m))
} }
Spacer(modifier = Modifier.height(Spacing.m))
SnackbarHost(
modifier = Modifier.padding(bottom = Spacing.xxxl),
hostState = snackbarHostState
)
} }
} }
} }

View File

@ -73,6 +73,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCo
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet 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.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.report.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -317,6 +318,7 @@ class MultiCommunityScreen(
options = buildList { options = buildList {
add(stringResource(MR.strings.post_action_share)) add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_hide)) add(stringResource(MR.strings.post_action_hide))
add(stringResource(MR.strings.post_action_report))
}, },
blurNsfw = uiState.blurNsfw, blurNsfw = uiState.blurNsfw,
onOpenCommunity = { community -> onOpenCommunity = { community ->
@ -374,6 +376,14 @@ class MultiCommunityScreen(
}, },
onOptionSelected = { optionIdx -> onOptionSelected = { optionIdx ->
when (optionIdx) { when (optionIdx) {
2 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
1 -> model.reduce(MultiCommunityMviModel.Intent.Hide(idx)) 1 -> model.reduce(MultiCommunityMviModel.Intent.Hide(idx))
else -> model.reduce( else -> model.reduce(
MultiCommunityMviModel.Intent.SharePost(idx) MultiCommunityMviModel.Intent.SharePost(idx)

View File

@ -20,7 +20,6 @@
<string name="message_missing_field">Missing field</string> <string name="message_missing_field">Missing field</string>
<string name="message_invalid_field">Invalid field</string> <string name="message_invalid_field">Invalid field</string>
<string name="message_image_loading_error">Image loading error</string> <string name="message_image_loading_error">Image loading error</string>
<string name="message_login_successful">🎉 Login successful! 🎉</string>
<string name="message_empty_list">No items to display</string> <string name="message_empty_list">No items to display</string>
<string name="message_operation_successful">Operation completed succcessfully</string> <string name="message_operation_successful">Operation completed succcessfully</string>
@ -63,11 +62,16 @@
<string name="post_detail_load_more_comments">Load more comments…</string> <string name="post_detail_load_more_comments">Load more comments…</string>
<string name="comment_action_delete">Delete</string> <string name="comment_action_delete">Delete</string>
<string name="post_action_share">Share</string> <string name="post_action_share">Share</string>
<string name="post_action_edit">Edit</string> <string name="post_action_edit">Edit</string>
<string name="post_action_hide">Hide</string> <string name="post_action_hide">Hide</string>
<string name="post_action_report">Report…</string>
<string name="post_detail_cross_posts">also posted to:</string> <string name="post_detail_cross_posts">also posted to:</string>
<string name="create_report_title_post">Report post</string>
<string name="create_report_title_comment">Report comment</string>
<string name="create_report_placeholder">Report text (optional)</string>
<string name="explore_result_type_all">All</string> <string name="explore_result_type_all">All</string>
<string name="explore_result_type_posts">Posts</string> <string name="explore_result_type_posts">Posts</string>
<string name="explore_result_type_comments">Comments</string> <string name="explore_result_type_comments">Comments</string>

View File

@ -16,7 +16,6 @@
<string name="message_missing_field">Campo obligatorio</string> <string name="message_missing_field">Campo obligatorio</string>
<string name="message_invalid_field">Campo no válido</string> <string name="message_invalid_field">Campo no válido</string>
<string name="message_image_loading_error">No ha sido posible descargar la imagen</string> <string name="message_image_loading_error">No ha sido posible descargar la imagen</string>
<string name="message_login_successful">🎉 ¡Acceso completado con éxito! 🎉</string>
<string name="message_empty_list">Ningún elemento para mostrar</string> <string name="message_empty_list">Ningún elemento para mostrar</string>
<string name="message_operation_successful">Operación completada con éxito</string> <string name="message_operation_successful">Operación completada con éxito</string>
@ -59,11 +58,16 @@
<string name="post_detail_load_more_comments">Descarga más comentarios…</string> <string name="post_detail_load_more_comments">Descarga más comentarios…</string>
<string name="comment_action_delete">Eliminar</string> <string name="comment_action_delete">Eliminar</string>
<string name="post_action_share">Compartir</string> <string name="post_action_share">Compartir</string>
<string name="post_action_edit">Modificar</string> <string name="post_action_edit">Modificar</string>
<string name="post_action_hide">Esconder</string> <string name="post_action_hide">Esconder</string>
<string name="post_action_report">Crear informe…</string>
<string name="post_detail_cross_posts">también publicado en:</string> <string name="post_detail_cross_posts">también publicado en:</string>
<string name="create_report_title_post">Crear informe sobre publicación</string>
<string name="create_report_title_comment">Crear informe sobre comentario</string>
<string name="create_report_placeholder">Texto del informe (opcional)</string>
<string name="explore_result_type_all">Todos</string> <string name="explore_result_type_all">Todos</string>
<string name="explore_result_type_posts">Publicaciones</string> <string name="explore_result_type_posts">Publicaciones</string>
<string name="explore_result_type_comments">Comentarios</string> <string name="explore_result_type_comments">Comentarios</string>

View File

@ -16,7 +16,6 @@
<string name="message_missing_field">Campo obbligatorio</string> <string name="message_missing_field">Campo obbligatorio</string>
<string name="message_invalid_field">Campo non valido</string> <string name="message_invalid_field">Campo non valido</string>
<string name="message_image_loading_error">Errore caricamento immagine</string> <string name="message_image_loading_error">Errore caricamento immagine</string>
<string name="message_login_successful">🎉 Login effettuato con successo! 🎉</string>
<string name="message_empty_list">Nessun elemento da visualizzare</string> <string name="message_empty_list">Nessun elemento da visualizzare</string>
<string name="message_operation_successful">Operazione completata con successo</string> <string name="message_operation_successful">Operazione completata con successo</string>
@ -59,11 +58,16 @@
<string name="post_detail_load_more_comments">Carica altri commenti…</string> <string name="post_detail_load_more_comments">Carica altri commenti…</string>
<string name="comment_action_delete">Elimina</string> <string name="comment_action_delete">Elimina</string>
<string name="post_action_share">Condividi</string> <string name="post_action_share">Condividi</string>
<string name="post_action_edit">Modifica</string> <string name="post_action_edit">Modifica</string>
<string name="post_action_hide">Nascondi</string> <string name="post_action_hide">Nascondi</string>
<string name="post_action_report">Segnala…</string>
<string name="post_detail_cross_posts">postato anche in:</string> <string name="post_detail_cross_posts">postato anche in:</string>
<string name="create_report_title_post">Segnala post</string>
<string name="create_report_title_comment">Segnala commento</string>
<string name="create_report_placeholder">Testo segnalazione (opzionale)</string>
<string name="explore_result_type_all">Tutti</string> <string name="explore_result_type_all">Tutti</string>
<string name="explore_result_type_posts">Post</string> <string name="explore_result_type_posts">Post</string>
<string name="explore_result_type_comments">Commenti</string> <string name="explore_result_type_comments">Commenti</string>