Display number of comments

This commit is contained in:
Isira Seneviratne
2024-08-30 08:46:02 +05:30
parent 4cac111b66
commit 3785404618
7 changed files with 203 additions and 91 deletions
@@ -8,13 +8,19 @@ import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfo
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
import org.schabi.newpipe.util.NO_SERVICE_ID
class CommentsSource(
serviceId: Int,
private val url: String?,
private val repliesPage: Page?
private val url: String,
private val repliesPage: Page?,
private val commentInfo: CommentInfo? = null,
) : PagingSource<Page, CommentsInfoItem>() {
constructor(commentInfo: CommentInfo) : this(
commentInfo.serviceId, commentInfo.url, commentInfo.nextPage, commentInfo
)
init {
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
}
@@ -29,17 +35,11 @@ class CommentsSource(
val info = CommentsInfo.getMoreItems(service, url, it)
LoadResult.Page(info.items, null, info.nextPage)
} ?: run {
val info = CommentsInfo.getInfo(service, url)
if (info.isCommentsDisabled) {
LoadResult.Error(CommentsDisabledException())
} else {
LoadResult.Page(info.relatedItems, null, info.nextPage)
}
val info = commentInfo ?: CommentInfo(CommentsInfo.getInfo(service, url))
LoadResult.Page(info.comments, null, info.nextPage)
}
}
}
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
}
class CommentsDisabledException : RuntimeException()
@@ -166,7 +166,13 @@ fun Comment(comment: CommentsInfoItem) {
.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.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
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.comments.CommentsInfoItem
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.NoItemsMessage
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.viewmodels.CommentsViewModel
import org.schabi.newpipe.viewmodels.util.Resource
@Composable
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
fun CommentSection(
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
commentCount: Int,
parentComment: CommentsInfoItem? = null,
commentsFlow: Flow<PagingData<CommentsInfoItem>>
isCommentsDisabled: Boolean = false,
) {
val comments = commentsFlow.collectAsLazyPagingItems()
val itemCount by remember { derivedStateOf { comments.itemCount } }
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state = rememberLazyListState()
Surface(color = MaterialTheme.colorScheme.background) {
LazyColumnScrollbar(state = state) {
LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop), state = state) {
if (parentComment != null) {
LazyColumnScrollbar(state = state) {
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
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 {
CommentRepliesHeader(comment = parentComment)
HorizontalDivider(thickness = 1.dp)
Text(
modifier = Modifier.padding(start = 8.dp),
text = pluralStringResource(R.plurals.comments, commentCount, commentCount),
fontWeight = FontWeight.Bold
)
}
}
if (itemCount == 0) {
item {
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]!!)
}
items(comments.itemCount) {
Comment(comment = comments[it]!!)
}
}
}
}
}
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> {
private val notLoading = LoadState.NotLoading(true)
override val values = sequenceOf(
// Normal view
PagingData.from(
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"
)
}
),
// 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 = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CommentSectionLoadingPreview() {
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
}
}
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CommentSectionPreview(
@PreviewParameter(CommentDataProvider::class) pagingData: PagingData<CommentsInfoItem>
) {
private fun CommentSectionSuccessPreview() {
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 {
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))
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.PagingConfig
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.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.NO_SERVICE_ID
import org.schabi.newpipe.viewmodels.util.Resource
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val serviceId = savedStateHandle[KEY_SERVICE_ID] ?: NO_SERVICE_ID
private val url = savedStateHandle.get<String>(KEY_URL)
val uiState = savedStateHandle.getStateFlow(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)) {
CommentsSource(serviceId, url, null)
}.flow
@OptIn(ExperimentalCoroutinesApi::class)
val comments = uiState
.filterIsInstance<Resource.Success<CommentInfo>>()
.flatMapLatest {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(it.data)
}.flow
}
.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>()
}