Handle no comments and comments disabled scenarios

This commit is contained in:
Isira Seneviratne 2024-06-23 10:53:22 +05:30
parent cc6f1ffd40
commit 8d4c608b52
4 changed files with 154 additions and 95 deletions

View File

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

View File

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

View File

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

View File

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