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 PersonMentionId = Int
typealias CommentReplyId = Int
typealias CommentReportId = Int
typealias PostReportId = 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
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.CommentSortType
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.EditCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.GetCommentResponse
@ -86,4 +88,11 @@ interface CommentService {
@Header("Authorization") authHeader: String? = null,
@Body form: DeleteCommentForm,
): 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.CreatePostForm
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.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.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.SavePostForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType
@ -97,4 +99,11 @@ interface PostService {
@Header("Authorization") authHeader: String? = null,
@Body content: MultiPartFormDataContent,
): 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.navigation.NavigationCoordinator
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.userdetail.UserDetailMviModel
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)
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.modals.SortBottomSheet
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.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -427,6 +428,7 @@ class CommunityDetailScreen(
options = buildList {
add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_hide))
add(stringResource(MR.strings.post_action_report))
if (post.creator?.id == uiState.currentUserId && !isOnOtherInstance) {
add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete))
@ -493,13 +495,13 @@ class CommunityDetailScreen(
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
3 -> model.reduce(
4 -> model.reduce(
CommunityDetailMviModel.Intent.DeletePost(
post.id
)
)
2 -> {
3 -> {
notificationCenter.addObserver(
{
model.reduce(CommunityDetailMviModel.Intent.Refresh)
@ -514,6 +516,14 @@ class CommunityDetailScreen(
)
}
2 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
1 -> model.reduce(
CommunityDetailMviModel.Intent.Hide(
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.postdetail.PostDetailMviModel
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.SavedItemsViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailMviModel
@ -172,4 +174,14 @@ val commonUiModule = module {
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.navigation.NavigationCoordinator
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.userdetail.UserDetailMviModel
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 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.image.ZoomableImageScreen
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.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -302,6 +303,7 @@ class PostDetailScreen(
},
options = buildList {
add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_report))
if (statePost.creator?.id == uiState.currentUserId && !isOnOtherInstance) {
add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete))
@ -347,7 +349,9 @@ class PostDetailScreen(
},
onOptionSelected = { idx ->
when (idx) {
1 -> {
3 -> model.reduce(PostDetailMviModel.Intent.DeletePost)
2 -> {
notificationCenter.addObserver(
{
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)
}
@ -501,6 +507,7 @@ class PostDetailScreen(
separateUpAndDownVotes = uiState.separateUpAndDownVotes,
autoLoadImages = uiState.autoLoadImages,
options = buildList {
add(stringResource(MR.strings.post_action_report))
if (comment.creator?.id == uiState.currentUserId) {
add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete))
@ -575,15 +582,15 @@ class PostDetailScreen(
)
}
},
onOptionSelected = { idx ->
when (idx) {
1 -> model.reduce(
onOptionSelected = { optionId ->
when (optionId) {
2 -> model.reduce(
PostDetailMviModel.Intent.DeleteComment(
comment.id
)
)
else -> {
1 -> {
notificationCenter.addObserver(
{
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.modals.SortBottomSheet
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.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
@ -390,6 +391,7 @@ class UserDetailScreen(
autoLoadImages = uiState.autoLoadImages,
options = buildList {
add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_report))
},
onUpVote = if (isOnOtherInstance) {
null
@ -454,6 +456,14 @@ class UserDetailScreen(
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
1 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
else -> model.reduce(
UserDetailMviModel.Intent.SharePost(idx)
)
@ -599,6 +609,20 @@ class UserDetailScreen(
onOpenCommunity = { 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(
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.navigation.NavigationCoordinator
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.userdetail.UserDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
@ -71,6 +72,11 @@ actual fun getSavedItemsViewModel(): SavedItemsMviModel =
actual fun getModalDrawerViewModel(): ModalDrawerMviModel =
CommonUiViewModelHelper.modalDrawerViewModel
actual fun getCreateReportViewModel(
postId: Int?,
commentId: Int?,
): CreateReportMviModel = CommonUiViewModelHelper.getCreateReportModel(postId, commentId)
object CommonUiViewModelHelper : KoinComponent {
val navigationCoordinator: NavigationCoordinator by inject()
@ -145,4 +151,14 @@ object CommonUiViewModelHelper : KoinComponent {
)
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.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.EditCommentForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SaveCommentForm
@ -247,4 +248,16 @@ class CommentRepository(
)
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.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.MarkPostAsReadForm
@ -246,4 +247,16 @@ class PostRepository(
e.printStackTrace()
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.SortBottomSheet
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.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -306,6 +307,7 @@ class PostListScreen : Screen {
options = buildList {
add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_hide))
add(stringResource(MR.strings.post_action_report))
if (post.creator?.id == uiState.currentUserId) {
add(stringResource(MR.strings.post_action_edit))
add(stringResource(MR.strings.comment_action_delete))
@ -367,13 +369,13 @@ class PostListScreen : Screen {
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
3 -> model.reduce(
4 -> model.reduce(
PostListMviModel.Intent.DeletePost(
post.id
)
)
2 -> {
3 -> {
notificationCenter.addObserver(
{
model.reduce(PostListMviModel.Intent.Refresh)
@ -388,6 +390,14 @@ class PostListScreen : Screen {
)
}
2 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
1 -> model.reduce(
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.material3.Button
import androidx.compose.material3.CircularProgressIndicator
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
@ -65,7 +65,6 @@ class LoginBottomSheet : Screen {
private const val HELP_URL = "https://join-lemmy.org/docs/users/01-getting-started.html"
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getLoginBottomSheetViewModel() }
@ -75,7 +74,6 @@ class LoginBottomSheet : Screen {
val snackbarHostState = remember { SnackbarHostState() }
val genericError = stringResource(MR.strings.message_generic_error)
val bottomSheetNavigator = LocalBottomSheetNavigator.current
val successfulLoginMessage = stringResource(MR.strings.message_login_successful)
LaunchedEffect(model) {
model.effects.onEach {
@ -87,7 +85,6 @@ class LoginBottomSheet : Screen {
}
LoginBottomSheetMviModel.Effect.LoginSuccess -> {
snackbarHostState.showSnackbar(message = successfulLoginMessage)
bottomSheetNavigator.hide()
}
}
@ -98,214 +95,223 @@ class LoginBottomSheet : Screen {
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
val settingsRepository = remember { getSettingsRepository() }
Column(
modifier = Modifier.padding(
top = Spacing.s,
start = Spacing.s,
end = Spacing.s,
bottom = Spacing.m,
),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
horizontalAlignment = Alignment.CenterHorizontally,
Box(
contentAlignment = Alignment.BottomCenter,
) {
Box {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BottomSheetHandle()
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()
},
Column(
modifier = Modifier.padding(
top = Spacing.s,
start = Spacing.s,
end = Spacing.s,
bottom = Spacing.m,
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
autoCorrect = false,
imeAction = ImeAction.Next,
),
onValueChange = { value ->
model.reduce(LoginBottomSheetMviModel.Intent.SetInstanceName(value))
},
supportingText = {
if (uiState.instanceNameError != null) {
verticalArrangement = Arrangement.spacedBy(Spacing.s),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BottomSheetHandle()
Text(
text = uiState.instanceNameError?.localized().orEmpty(),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(start = Spacing.s, top = Spacing.s),
text = stringResource(MR.strings.profile_button_login),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
},
)
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,
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,
)
}
},
)
}
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()
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))
},
),
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()
singleLine = true,
value = uiState.instanceName,
isError = uiState.instanceNameError != null,
keyboardActions = KeyboardActions(
onNext = {
usernameFocusRequester.requestFocus()
},
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
autoCorrect = false,
imeAction = ImeAction.Next,
),
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 {
VisualTransformation.None
}
},
imageVector = if (transformation == VisualTransformation.None) {
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,
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,
)
}
},
)
TextField(
modifier = Modifier.focusRequester(tokenFocusRequester),
label = {
TextField(
modifier = Modifier.focusRequester(tokenFocusRequester),
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(
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
verticalAlignment = Alignment.Bottom,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = stringResource(MR.strings.login_field_token))
Text(
text = stringResource(MR.strings.login_field_label_optional),
style = MaterialTheme.typography.labelSmall,
)
if (uiState.loading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
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.modals.SortBottomSheet
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.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
@ -317,6 +318,7 @@ class MultiCommunityScreen(
options = buildList {
add(stringResource(MR.strings.post_action_share))
add(stringResource(MR.strings.post_action_hide))
add(stringResource(MR.strings.post_action_report))
},
blurNsfw = uiState.blurNsfw,
onOpenCommunity = { community ->
@ -374,6 +376,14 @@ class MultiCommunityScreen(
},
onOptionSelected = { optionIdx ->
when (optionIdx) {
2 -> {
bottomSheetNavigator.show(
CreateReportScreen(
postId = post.id
)
)
}
1 -> model.reduce(MultiCommunityMviModel.Intent.Hide(idx))
else -> model.reduce(
MultiCommunityMviModel.Intent.SharePost(idx)

View File

@ -20,7 +20,6 @@
<string name="message_missing_field">Missing field</string>
<string name="message_invalid_field">Invalid field</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_operation_successful">Operation completed succcessfully</string>
@ -63,11 +62,16 @@
<string name="post_detail_load_more_comments">Load more comments…</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_hide">Hide</string>
<string name="post_action_report">Report…</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_posts">Posts</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_invalid_field">Campo no válido</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_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="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_hide">Esconder</string>
<string name="post_action_report">Crear informe…</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_posts">Publicaciones</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_invalid_field">Campo non valido</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_operation_successful">Operazione completata con successo</string>
@ -59,11 +58,16 @@
<string name="post_detail_load_more_comments">Carica altri commenti…</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_hide">Nascondi</string>
<string name="post_action_report">Segnala…</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_posts">Post</string>
<string name="explore_result_type_comments">Commenti</string>