mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-25 04:22:30 +01:00
Display number of comments
This commit is contained in:
parent
4cac111b66
commit
3785404618
@ -8,13 +8,19 @@ import org.schabi.newpipe.extractor.NewPipe
|
|||||||
import org.schabi.newpipe.extractor.Page
|
import org.schabi.newpipe.extractor.Page
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||||
|
|
||||||
class CommentsSource(
|
class CommentsSource(
|
||||||
serviceId: Int,
|
serviceId: Int,
|
||||||
private val url: String?,
|
private val url: String,
|
||||||
private val repliesPage: Page?
|
private val repliesPage: Page?,
|
||||||
|
private val commentInfo: CommentInfo? = null,
|
||||||
) : PagingSource<Page, CommentsInfoItem>() {
|
) : PagingSource<Page, CommentsInfoItem>() {
|
||||||
|
constructor(commentInfo: CommentInfo) : this(
|
||||||
|
commentInfo.serviceId, commentInfo.url, commentInfo.nextPage, commentInfo
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
|
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
|
||||||
}
|
}
|
||||||
@ -29,17 +35,11 @@ class CommentsSource(
|
|||||||
val info = CommentsInfo.getMoreItems(service, url, it)
|
val info = CommentsInfo.getMoreItems(service, url, it)
|
||||||
LoadResult.Page(info.items, null, info.nextPage)
|
LoadResult.Page(info.items, null, info.nextPage)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
val info = CommentsInfo.getInfo(service, url)
|
val info = commentInfo ?: CommentInfo(CommentsInfo.getInfo(service, url))
|
||||||
if (info.isCommentsDisabled) {
|
LoadResult.Page(info.comments, null, info.nextPage)
|
||||||
LoadResult.Error(CommentsDisabledException())
|
|
||||||
} else {
|
|
||||||
LoadResult.Page(info.relatedItems, null, info.nextPage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommentsDisabledException : RuntimeException()
|
|
||||||
|
@ -166,7 +166,13 @@ fun Comment(comment: CommentsInfoItem) {
|
|||||||
.cachedIn(coroutineScope)
|
.cachedIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
CommentSection(parentComment = comment, commentsFlow = flow)
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(
|
||||||
|
commentsFlow = flow,
|
||||||
|
commentCount = comment.replyCount,
|
||||||
|
parentComment = comment
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video.comment
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CommentInfo(
|
||||||
|
val serviceId: Int,
|
||||||
|
val url: String,
|
||||||
|
val comments: List<CommentsInfoItem>,
|
||||||
|
val nextPage: Page?,
|
||||||
|
val commentCount: Int,
|
||||||
|
val isCommentsDisabled: Boolean
|
||||||
|
) {
|
||||||
|
constructor(commentsInfo: CommentsInfo) : this(
|
||||||
|
commentsInfo.serviceId, commentsInfo.url, commentsInfo.relatedItems, commentsInfo.nextPage,
|
||||||
|
commentsInfo.commentsCount, commentsInfo.isCommentsDisabled
|
||||||
|
)
|
||||||
|
}
|
@ -7,20 +7,19 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.LoadStates
|
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -30,106 +29,157 @@ import org.schabi.newpipe.R
|
|||||||
import org.schabi.newpipe.extractor.Page
|
import org.schabi.newpipe.extractor.Page
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
import org.schabi.newpipe.extractor.stream.Description
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
import org.schabi.newpipe.paging.CommentsDisabledException
|
|
||||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
||||||
|
import org.schabi.newpipe.viewmodels.util.Resource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
|
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
|
||||||
CommentSection(commentsFlow = commentsViewModel.comments)
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
CommentSection(state, commentsViewModel.comments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CommentSection(
|
||||||
|
uiState: Resource<CommentInfo>,
|
||||||
|
commentsFlow: Flow<PagingData<CommentsInfoItem>>
|
||||||
|
) {
|
||||||
|
when (uiState) {
|
||||||
|
is Resource.Loading -> LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||||
|
|
||||||
|
is Resource.Success -> {
|
||||||
|
val commentsInfo = uiState.data
|
||||||
|
CommentSection(
|
||||||
|
commentsFlow = commentsFlow,
|
||||||
|
commentCount = commentsInfo.commentCount,
|
||||||
|
isCommentsDisabled = commentsInfo.isCommentsDisabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Error -> {
|
||||||
|
// This is not rendered as VideoDetailFragment handles errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentSection(
|
fun CommentSection(
|
||||||
|
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
|
||||||
|
commentCount: Int,
|
||||||
parentComment: CommentsInfoItem? = null,
|
parentComment: CommentsInfoItem? = null,
|
||||||
commentsFlow: Flow<PagingData<CommentsInfoItem>>
|
isCommentsDisabled: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val comments = commentsFlow.collectAsLazyPagingItems()
|
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||||
val itemCount by remember { derivedStateOf { comments.itemCount } }
|
|
||||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
|
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
LazyColumnScrollbar(state = state) {
|
||||||
LazyColumnScrollbar(state = state) {
|
LazyColumn(
|
||||||
LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop), state = state) {
|
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||||
if (parentComment != null) {
|
state = state
|
||||||
|
) {
|
||||||
|
if (parentComment != null) {
|
||||||
|
item {
|
||||||
|
CommentRepliesHeader(comment = parentComment)
|
||||||
|
HorizontalDivider(thickness = 1.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comments.itemCount == 0) {
|
||||||
|
item {
|
||||||
|
val refresh = comments.loadState.refresh
|
||||||
|
if (refresh is LoadState.Loading) {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||||
|
} else {
|
||||||
|
val message = if (refresh is LoadState.Error) {
|
||||||
|
R.string.error_unable_to_load_comments
|
||||||
|
} else if (isCommentsDisabled) {
|
||||||
|
R.string.comments_are_disabled
|
||||||
|
} else {
|
||||||
|
R.string.no_comments
|
||||||
|
}
|
||||||
|
NoItemsMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The number of replies is already shown in the main comment section
|
||||||
|
if (parentComment == null) {
|
||||||
item {
|
item {
|
||||||
CommentRepliesHeader(comment = parentComment)
|
Text(
|
||||||
HorizontalDivider(thickness = 1.dp)
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
text = pluralStringResource(R.plurals.comments, commentCount, commentCount),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemCount == 0) {
|
items(comments.itemCount) {
|
||||||
item {
|
Comment(comment = comments[it]!!)
|
||||||
val refresh = comments.loadState.refresh
|
|
||||||
if (refresh is LoadState.Loading) {
|
|
||||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
|
||||||
} else {
|
|
||||||
val error = (refresh as? LoadState.Error)?.error
|
|
||||||
val message = if (error is CommentsDisabledException) {
|
|
||||||
R.string.comments_are_disabled
|
|
||||||
} else {
|
|
||||||
R.string.no_comments
|
|
||||||
}
|
|
||||||
NoItemsMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items(itemCount) {
|
|
||||||
Comment(comment = comments[it]!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> {
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
private val notLoading = LoadState.NotLoading(true)
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
override val values = sequenceOf(
|
private fun CommentSectionLoadingPreview() {
|
||||||
// Normal view
|
AppTheme {
|
||||||
PagingData.from(
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
listOf(
|
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
|
||||||
CommentsInfoItem(
|
}
|
||||||
commentText = Description(
|
}
|
||||||
"Comment 1\n\nThis line should be hidden by default.",
|
|
||||||
Description.PLAIN_TEXT
|
|
||||||
),
|
|
||||||
uploaderName = "Test",
|
|
||||||
replies = Page(""),
|
|
||||||
replyCount = 10
|
|
||||||
)
|
|
||||||
) + (2..10).map {
|
|
||||||
CommentsInfoItem(
|
|
||||||
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
|
||||||
uploaderName = "Test"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
// Comments disabled
|
|
||||||
PagingData.from(
|
|
||||||
listOf<CommentsInfoItem>(),
|
|
||||||
LoadStates(LoadState.Error(CommentsDisabledException()), notLoading, notLoading)
|
|
||||||
),
|
|
||||||
// No comments
|
|
||||||
PagingData.from(
|
|
||||||
listOf<CommentsInfoItem>(),
|
|
||||||
LoadStates(notLoading, notLoading, notLoading)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
private fun CommentSectionPreview(
|
private fun CommentSectionSuccessPreview() {
|
||||||
@PreviewParameter(CommentDataProvider::class) pagingData: PagingData<CommentsInfoItem>
|
val comments = listOf(
|
||||||
) {
|
CommentsInfoItem(
|
||||||
|
commentText = Description(
|
||||||
|
"Comment 1\n\nThis line should be hidden by default.",
|
||||||
|
Description.PLAIN_TEXT
|
||||||
|
),
|
||||||
|
uploaderName = "Test",
|
||||||
|
replies = Page(""),
|
||||||
|
replyCount = 10
|
||||||
|
)
|
||||||
|
) + (2..10).map {
|
||||||
|
CommentsInfoItem(
|
||||||
|
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
||||||
|
uploaderName = "Test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
CommentSection(commentsFlow = flowOf(pagingData))
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(
|
||||||
|
uiState = Resource.Success(
|
||||||
|
CommentInfo(
|
||||||
|
serviceId = 1, url = "", comments = comments, nextPage = null,
|
||||||
|
commentCount = 10, isCommentsDisabled = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
commentsFlow = flowOf(PagingData.from(comments))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun CommentSectionErrorPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +203,8 @@ private fun CommentRepliesPreview() {
|
|||||||
val flow = flowOf(PagingData.from(replies))
|
val flow = flowOf(PagingData.from(replies))
|
||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
CommentSection(parentComment = comment, commentsFlow = flow)
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
CommentSection(parentComment = comment, commentsFlow = flow, commentCount = 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,17 +6,39 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
import org.schabi.newpipe.paging.CommentsSource
|
import org.schabi.newpipe.paging.CommentsSource
|
||||||
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||||
import org.schabi.newpipe.util.KEY_URL
|
import org.schabi.newpipe.util.KEY_URL
|
||||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
import org.schabi.newpipe.viewmodels.util.Resource
|
||||||
|
|
||||||
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||||
private val serviceId = savedStateHandle[KEY_SERVICE_ID] ?: NO_SERVICE_ID
|
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
|
||||||
private val url = savedStateHandle.get<String>(KEY_URL)
|
.map {
|
||||||
|
try {
|
||||||
|
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Resource.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)
|
||||||
|
|
||||||
val comments = Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
CommentsSource(serviceId, url, null)
|
val comments = uiState
|
||||||
}.flow
|
.filterIsInstance<Resource.Success<CommentInfo>>()
|
||||||
|
.flatMapLatest {
|
||||||
|
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||||
|
CommentsSource(it.data)
|
||||||
|
}.flow
|
||||||
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.schabi.newpipe.viewmodels.util
|
||||||
|
|
||||||
|
sealed class Resource<out T> {
|
||||||
|
data object Loading : Resource<Nothing>()
|
||||||
|
class Success<T>(val data: T) : Resource<T>()
|
||||||
|
class Error(val throwable: Throwable) : Resource<Nothing>()
|
||||||
|
}
|
@ -856,4 +856,8 @@
|
|||||||
<string name="show_less">Show less</string>
|
<string name="show_less">Show less</string>
|
||||||
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
|
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
|
||||||
<string name="auto_queue_description">Next</string>
|
<string name="auto_queue_description">Next</string>
|
||||||
|
<plurals name="comments">
|
||||||
|
<item quantity="one">%d comment</item>
|
||||||
|
<item quantity="other">%d comments</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user