Handle no comments and comments disabled scenarios
This commit is contained in:
parent
cc6f1ffd40
commit
8d4c608b52
|
@ -881,8 +881,7 @@ public final class VideoDetailFragment
|
||||||
tabContentDescriptions.clear();
|
tabContentDescriptions.clear();
|
||||||
|
|
||||||
if (shouldShowComments()) {
|
if (shouldShowComments()) {
|
||||||
pageAdapter.addFragment(
|
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
||||||
CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
|
||||||
tabIcons.add(R.drawable.ic_comment);
|
tabIcons.add(R.drawable.ic_comment);
|
||||||
tabContentDescriptions.add(R.string.comments_tab_description);
|
tabContentDescriptions.add(R.string.comments_tab_description);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,85 +69,83 @@ fun Comment(comment: CommentsInfoItem) {
|
||||||
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
var showReplies by rememberSaveable { mutableStateOf(false) }
|
var showReplies by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Row(
|
||||||
Row(
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.clickable { isExpanded = !isExpanded }
|
||||||
.clickable { isExpanded = !isExpanded }
|
.padding(all = 8.dp),
|
||||||
.padding(all = 8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
) {
|
||||||
) {
|
if (ImageStrategy.shouldLoadImages()) {
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
AsyncImage(
|
||||||
AsyncImage(
|
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
|
||||||
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
|
contentDescription = null,
|
||||||
contentDescription = null,
|
placeholder = painterResource(R.drawable.placeholder_person),
|
||||||
placeholder = painterResource(R.drawable.placeholder_person),
|
error = painterResource(R.drawable.placeholder_person),
|
||||||
error = painterResource(R.drawable.placeholder_person),
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.size(42.dp)
|
||||||
.size(42.dp)
|
.clip(CircleShape)
|
||||||
.clip(CircleShape)
|
.clickable {
|
||||||
.clickable {
|
NavigationHelper.openCommentAuthorIfPresent(
|
||||||
NavigationHelper.openCommentAuthorIfPresent(
|
context as FragmentActivity, comment
|
||||||
context as FragmentActivity, comment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
if (comment.isPinned) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ic_pin),
|
|
||||||
contentDescription = stringResource(R.string.detail_pinned_comment_view_description)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val nameAndDate = remember(comment) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
val date = Localization.relativeTimeOrTextual(
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
context, comment.uploadDate, comment.textualUploadDate
|
if (comment.isPinned) {
|
||||||
)
|
Image(
|
||||||
Localization.concatenateStrings(comment.uploaderName, date)
|
painter = painterResource(R.drawable.ic_pin),
|
||||||
}
|
contentDescription = stringResource(R.string.detail_pinned_comment_view_description)
|
||||||
Text(text = nameAndDate, color = MaterialTheme.colorScheme.secondary)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
val nameAndDate = remember(comment) {
|
||||||
text = rememberParsedText(comment.commentText),
|
val date = Localization.relativeTimeOrTextual(
|
||||||
// If the comment is expanded, we display all its content
|
context, comment.uploadDate, comment.textualUploadDate
|
||||||
// otherwise we only display the first two lines
|
)
|
||||||
maxLines = if (isExpanded) Int.MAX_VALUE else 2,
|
Localization.concatenateStrings(comment.uploaderName, date)
|
||||||
overflow = TextOverflow.Ellipsis,
|
}
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
Text(text = nameAndDate, color = MaterialTheme.colorScheme.secondary)
|
||||||
)
|
}
|
||||||
|
|
||||||
Row(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
text = rememberParsedText(comment.commentText),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
// If the comment is expanded, we display all its content
|
||||||
verticalAlignment = Alignment.CenterVertically
|
// otherwise we only display the first two lines
|
||||||
) {
|
maxLines = if (isExpanded) Int.MAX_VALUE else 2,
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_thumb_up),
|
||||||
|
contentDescription = stringResource(R.string.detail_likes_img_view_description)
|
||||||
|
)
|
||||||
|
Text(text = Localization.likeCount(context, comment.likeCount))
|
||||||
|
|
||||||
|
if (comment.isHeartedByUploader) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.ic_thumb_up),
|
painter = painterResource(R.drawable.ic_heart),
|
||||||
contentDescription = stringResource(R.string.detail_likes_img_view_description)
|
contentDescription = stringResource(R.string.detail_heart_img_view_description)
|
||||||
)
|
)
|
||||||
Text(text = Localization.likeCount(context, comment.likeCount))
|
|
||||||
|
|
||||||
if (comment.isHeartedByUploader) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ic_heart),
|
|
||||||
contentDescription = stringResource(R.string.detail_heart_img_view_description)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (comment.replies != null) {
|
if (comment.replies != null) {
|
||||||
TextButton(onClick = { showReplies = true }) {
|
TextButton(onClick = { showReplies = true }) {
|
||||||
val text = pluralStringResource(
|
val text = pluralStringResource(
|
||||||
R.plurals.replies, comment.replyCount, comment.replyCount.toString()
|
R.plurals.replies, comment.replyCount, comment.replyCount.toString()
|
||||||
)
|
)
|
||||||
Text(text = text)
|
Text(text = text)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +188,7 @@ fun CommentsInfoItem(
|
||||||
this.replyCount = replyCount
|
this.replyCount = replyCount
|
||||||
}
|
}
|
||||||
|
|
||||||
class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
|
private class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
|
||||||
override val values = sequenceOf(
|
override val values = sequenceOf(
|
||||||
Description("Hello world!<br><br>This line should be hidden by default.", Description.HTML),
|
Description("Hello world!<br><br>This line should be hidden by default.", Description.HTML),
|
||||||
Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
|
Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
|
||||||
|
@ -214,6 +212,8 @@ private fun CommentPreview(
|
||||||
)
|
)
|
||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
Comment(comment)
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
Comment(comment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,33 @@
|
||||||
package org.schabi.newpipe.fragments.list.comments
|
package org.schabi.newpipe.fragments.list.comments
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.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.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
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.compose.ui.unit.sp
|
||||||
|
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
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import my.nanihadesuka.compose.LazyColumnScrollbar
|
import my.nanihadesuka.compose.LazyColumnScrollbar
|
||||||
import my.nanihadesuka.compose.ScrollbarSettings
|
import my.nanihadesuka.compose.ScrollbarSettings
|
||||||
|
import org.schabi.newpipe.R
|
||||||
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.ui.theme.AppTheme
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
@ -23,38 +38,81 @@ fun CommentSection(
|
||||||
parentComment: CommentsInfoItem? = null,
|
parentComment: CommentsInfoItem? = null,
|
||||||
) {
|
) {
|
||||||
val replies = flow.collectAsLazyPagingItems()
|
val replies = flow.collectAsLazyPagingItems()
|
||||||
val listState = rememberLazyListState()
|
val itemCount by remember { derivedStateOf { replies.itemCount } }
|
||||||
|
|
||||||
LazyColumnScrollbar(state = listState, settings = ScrollbarSettings.Default) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
LazyColumn(state = listState) {
|
val refresh = replies.loadState.refresh
|
||||||
if (parentComment != null) {
|
if (itemCount == 0 && refresh !is LoadState.Loading) {
|
||||||
item {
|
NoCommentsMessage((refresh as? LoadState.Error)?.error)
|
||||||
CommentRepliesHeader(comment = parentComment)
|
} else {
|
||||||
HorizontalDivider(thickness = 1.dp)
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumnScrollbar(state = listState, settings = ScrollbarSettings.Default) {
|
||||||
|
LazyColumn(state = listState) {
|
||||||
|
if (parentComment != null) {
|
||||||
|
item {
|
||||||
|
CommentRepliesHeader(comment = parentComment)
|
||||||
|
HorizontalDivider(thickness = 1.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(itemCount) {
|
||||||
|
Comment(comment = replies[it]!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(replies.itemCount) {
|
|
||||||
Comment(comment = replies[it]!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NoCommentsMessage(error: Throwable?) {
|
||||||
|
val message = if (error is CommentsDisabledException) {
|
||||||
|
R.string.comments_are_disabled
|
||||||
|
} else {
|
||||||
|
R.string.no_comments
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(text = "(╯°-°)╯", fontSize = 35.sp)
|
||||||
|
Text(text = stringResource(id = message), fontSize = 24.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> {
|
||||||
|
private val notLoading = LoadState.NotLoading(true)
|
||||||
|
|
||||||
|
override val values = sequenceOf(
|
||||||
|
// Normal view
|
||||||
|
PagingData.from(
|
||||||
|
(1..100).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 CommentSectionPreview(
|
||||||
val comments = (1..100).map {
|
@PreviewParameter(CommentDataProvider::class) pagingData: PagingData<CommentsInfoItem>
|
||||||
CommentsInfoItem(
|
) {
|
||||||
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
|
||||||
uploaderName = "Test"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val flow = flowOf(PagingData.from(comments))
|
|
||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
CommentSection(flow = flow)
|
CommentSection(flow = flowOf(pagingData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class CommentsSource(
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.map {
|
.map {
|
||||||
if (it.isCommentsDisabled) {
|
if (it.isCommentsDisabled) {
|
||||||
LoadResult.Invalid()
|
LoadResult.Error(CommentsDisabledException())
|
||||||
} else {
|
} else {
|
||||||
LoadResult.Page(it.relatedItems, null, it.nextPage)
|
LoadResult.Page(it.relatedItems, null, it.nextPage)
|
||||||
}
|
}
|
||||||
|
@ -34,3 +34,5 @@ class CommentsSource(
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CommentsDisabledException : RuntimeException()
|
||||||
|
|
Loading…
Reference in New Issue