Skip to content

Commit 3785404

Browse files
Display number of comments
1 parent 4cac111 commit 3785404

7 files changed

Lines changed: 199 additions & 87 deletions

File tree

app/src/main/java/org/schabi/newpipe/paging/CommentsSource.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ import org.schabi.newpipe.extractor.NewPipe
88
import org.schabi.newpipe.extractor.Page
99
import org.schabi.newpipe.extractor.comments.CommentsInfo
1010
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
11+
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
1112
import org.schabi.newpipe.util.NO_SERVICE_ID
1213

1314
class CommentsSource(
1415
serviceId: Int,
15-
private val url: String?,
16-
private val repliesPage: Page?
16+
private val url: String,
17+
private val repliesPage: Page?,
18+
private val commentInfo: CommentInfo? = null,
1719
) : PagingSource<Page, CommentsInfoItem>() {
20+
constructor(commentInfo: CommentInfo) : this(
21+
commentInfo.serviceId, commentInfo.url, commentInfo.nextPage, commentInfo
22+
)
23+
1824
init {
1925
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
2026
}
@@ -29,17 +35,11 @@ class CommentsSource(
2935
val info = CommentsInfo.getMoreItems(service, url, it)
3036
LoadResult.Page(info.items, null, info.nextPage)
3137
} ?: run {
32-
val info = CommentsInfo.getInfo(service, url)
33-
if (info.isCommentsDisabled) {
34-
LoadResult.Error(CommentsDisabledException())
35-
} else {
36-
LoadResult.Page(info.relatedItems, null, info.nextPage)
37-
}
38+
val info = commentInfo ?: CommentInfo(CommentsInfo.getInfo(service, url))
39+
LoadResult.Page(info.comments, null, info.nextPage)
3840
}
3941
}
4042
}
4143

4244
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
4345
}
44-
45-
class CommentsDisabledException : RuntimeException()

app/src/main/java/org/schabi/newpipe/ui/components/video/comment/Comment.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ fun Comment(comment: CommentsInfoItem) {
166166
.cachedIn(coroutineScope)
167167
}
168168

169-
CommentSection(parentComment = comment, commentsFlow = flow)
169+
Surface(color = MaterialTheme.colorScheme.background) {
170+
CommentSection(
171+
commentsFlow = flow,
172+
commentCount = comment.replyCount,
173+
parentComment = comment
174+
)
175+
}
170176
}
171177
}
172178
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.schabi.newpipe.ui.components.video.comment
2+
3+
import androidx.compose.runtime.Immutable
4+
import org.schabi.newpipe.extractor.Page
5+
import org.schabi.newpipe.extractor.comments.CommentsInfo
6+
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
7+
8+
@Immutable
9+
class CommentInfo(
10+
val serviceId: Int,
11+
val url: String,
12+
val comments: List<CommentsInfoItem>,
13+
val nextPage: Page?,
14+
val commentCount: Int,
15+
val isCommentsDisabled: Boolean
16+
) {
17+
constructor(commentsInfo: CommentsInfo) : this(
18+
commentsInfo.serviceId, commentsInfo.url, commentsInfo.relatedItems, commentsInfo.nextPage,
19+
commentsInfo.commentsCount, commentsInfo.isCommentsDisabled
20+
)
21+
}

app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentSection.kt

Lines changed: 121 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,19 @@ import androidx.compose.foundation.lazy.rememberLazyListState
77
import androidx.compose.material3.HorizontalDivider
88
import androidx.compose.material3.MaterialTheme
99
import androidx.compose.material3.Surface
10+
import androidx.compose.material3.Text
1011
import androidx.compose.runtime.Composable
11-
import androidx.compose.runtime.derivedStateOf
1212
import androidx.compose.runtime.getValue
13-
import androidx.compose.runtime.remember
1413
import androidx.compose.ui.Modifier
1514
import androidx.compose.ui.input.nestedscroll.nestedScroll
1615
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
16+
import androidx.compose.ui.res.pluralStringResource
17+
import androidx.compose.ui.text.font.FontWeight
1718
import androidx.compose.ui.tooling.preview.Preview
18-
import androidx.compose.ui.tooling.preview.PreviewParameter
19-
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
2019
import androidx.compose.ui.unit.dp
20+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2121
import androidx.lifecycle.viewmodel.compose.viewModel
2222
import androidx.paging.LoadState
23-
import androidx.paging.LoadStates
2423
import androidx.paging.PagingData
2524
import androidx.paging.compose.collectAsLazyPagingItems
2625
import kotlinx.coroutines.flow.Flow
@@ -30,106 +29,157 @@ import org.schabi.newpipe.R
3029
import org.schabi.newpipe.extractor.Page
3130
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
3231
import org.schabi.newpipe.extractor.stream.Description
33-
import org.schabi.newpipe.paging.CommentsDisabledException
3432
import org.schabi.newpipe.ui.components.common.LoadingIndicator
3533
import org.schabi.newpipe.ui.components.common.NoItemsMessage
3634
import org.schabi.newpipe.ui.theme.AppTheme
3735
import org.schabi.newpipe.viewmodels.CommentsViewModel
36+
import org.schabi.newpipe.viewmodels.util.Resource
3837

3938
@Composable
4039
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
41-
CommentSection(commentsFlow = commentsViewModel.comments)
40+
Surface(color = MaterialTheme.colorScheme.background) {
41+
val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
42+
CommentSection(state, commentsViewModel.comments)
43+
}
44+
}
45+
46+
@Composable
47+
private fun CommentSection(
48+
uiState: Resource<CommentInfo>,
49+
commentsFlow: Flow<PagingData<CommentsInfoItem>>
50+
) {
51+
when (uiState) {
52+
is Resource.Loading -> LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
53+
54+
is Resource.Success -> {
55+
val commentsInfo = uiState.data
56+
CommentSection(
57+
commentsFlow = commentsFlow,
58+
commentCount = commentsInfo.commentCount,
59+
isCommentsDisabled = commentsInfo.isCommentsDisabled
60+
)
61+
}
62+
63+
is Resource.Error -> {
64+
// This is not rendered as VideoDetailFragment handles errors
65+
}
66+
}
4267
}
4368

4469
@Composable
4570
fun CommentSection(
71+
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
72+
commentCount: Int,
4673
parentComment: CommentsInfoItem? = null,
47-
commentsFlow: Flow<PagingData<CommentsInfoItem>>
74+
isCommentsDisabled: Boolean = false,
4875
) {
4976
val comments = commentsFlow.collectAsLazyPagingItems()
50-
val itemCount by remember { derivedStateOf { comments.itemCount } }
5177
val nestedScrollInterop = rememberNestedScrollInteropConnection()
5278
val state = rememberLazyListState()
5379

54-
Surface(color = MaterialTheme.colorScheme.background) {
55-
LazyColumnScrollbar(state = state) {
56-
LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop), state = state) {
57-
if (parentComment != null) {
58-
item {
59-
CommentRepliesHeader(comment = parentComment)
60-
HorizontalDivider(thickness = 1.dp)
61-
}
80+
LazyColumnScrollbar(state = state) {
81+
LazyColumn(
82+
modifier = Modifier.nestedScroll(nestedScrollInterop),
83+
state = state
84+
) {
85+
if (parentComment != null) {
86+
item {
87+
CommentRepliesHeader(comment = parentComment)
88+
HorizontalDivider(thickness = 1.dp)
6289
}
90+
}
6391

64-
if (itemCount == 0) {
65-
item {
66-
val refresh = comments.loadState.refresh
67-
if (refresh is LoadState.Loading) {
68-
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
92+
if (comments.itemCount == 0) {
93+
item {
94+
val refresh = comments.loadState.refresh
95+
if (refresh is LoadState.Loading) {
96+
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
97+
} else {
98+
val message = if (refresh is LoadState.Error) {
99+
R.string.error_unable_to_load_comments
100+
} else if (isCommentsDisabled) {
101+
R.string.comments_are_disabled
69102
} else {
70-
val error = (refresh as? LoadState.Error)?.error
71-
val message = if (error is CommentsDisabledException) {
72-
R.string.comments_are_disabled
73-
} else {
74-
R.string.no_comments
75-
}
76-
NoItemsMessage(message)
103+
R.string.no_comments
77104
}
105+
NoItemsMessage(message)
78106
}
79-
} else {
80-
items(itemCount) {
81-
Comment(comment = comments[it]!!)
107+
}
108+
} else {
109+
// The number of replies is already shown in the main comment section
110+
if (parentComment == null) {
111+
item {
112+
Text(
113+
modifier = Modifier.padding(start = 8.dp),
114+
text = pluralStringResource(R.plurals.comments, commentCount, commentCount),
115+
fontWeight = FontWeight.Bold
116+
)
82117
}
83118
}
119+
120+
items(comments.itemCount) {
121+
Comment(comment = comments[it]!!)
122+
}
84123
}
85124
}
86125
}
87126
}
88127

89-
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> {
90-
private val notLoading = LoadState.NotLoading(true)
128+
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
129+
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
130+
@Composable
131+
private fun CommentSectionLoadingPreview() {
132+
AppTheme {
133+
Surface(color = MaterialTheme.colorScheme.background) {
134+
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
135+
}
136+
}
137+
}
91138

92-
override val values = sequenceOf(
93-
// Normal view
94-
PagingData.from(
95-
listOf(
96-
CommentsInfoItem(
97-
commentText = Description(
98-
"Comment 1\n\nThis line should be hidden by default.",
99-
Description.PLAIN_TEXT
100-
),
101-
uploaderName = "Test",
102-
replies = Page(""),
103-
replyCount = 10
104-
)
105-
) + (2..10).map {
106-
CommentsInfoItem(
107-
commentText = Description("Comment $it", Description.PLAIN_TEXT),
108-
uploaderName = "Test"
109-
)
110-
}
111-
),
112-
// Comments disabled
113-
PagingData.from(
114-
listOf<CommentsInfoItem>(),
115-
LoadStates(LoadState.Error(CommentsDisabledException()), notLoading, notLoading)
116-
),
117-
// No comments
118-
PagingData.from(
119-
listOf<CommentsInfoItem>(),
120-
LoadStates(notLoading, notLoading, notLoading)
139+
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
140+
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
141+
@Composable
142+
private fun CommentSectionSuccessPreview() {
143+
val comments = listOf(
144+
CommentsInfoItem(
145+
commentText = Description(
146+
"Comment 1\n\nThis line should be hidden by default.",
147+
Description.PLAIN_TEXT
148+
),
149+
uploaderName = "Test",
150+
replies = Page(""),
151+
replyCount = 10
121152
)
122-
)
153+
) + (2..10).map {
154+
CommentsInfoItem(
155+
commentText = Description("Comment $it", Description.PLAIN_TEXT),
156+
uploaderName = "Test"
157+
)
158+
}
159+
160+
AppTheme {
161+
Surface(color = MaterialTheme.colorScheme.background) {
162+
CommentSection(
163+
uiState = Resource.Success(
164+
CommentInfo(
165+
serviceId = 1, url = "", comments = comments, nextPage = null,
166+
commentCount = 10, isCommentsDisabled = false
167+
)
168+
),
169+
commentsFlow = flowOf(PagingData.from(comments))
170+
)
171+
}
172+
}
123173
}
124174

125175
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
126176
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
127177
@Composable
128-
private fun CommentSectionPreview(
129-
@PreviewParameter(CommentDataProvider::class) pagingData: PagingData<CommentsInfoItem>
130-
) {
178+
private fun CommentSectionErrorPreview() {
131179
AppTheme {
132-
CommentSection(commentsFlow = flowOf(pagingData))
180+
Surface(color = MaterialTheme.colorScheme.background) {
181+
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf())
182+
}
133183
}
134184
}
135185

@@ -153,6 +203,8 @@ private fun CommentRepliesPreview() {
153203
val flow = flowOf(PagingData.from(replies))
154204

155205
AppTheme {
156-
CommentSection(parentComment = comment, commentsFlow = flow)
206+
Surface(color = MaterialTheme.colorScheme.background) {
207+
CommentSection(parentComment = comment, commentsFlow = flow, commentCount = 10)
208+
}
157209
}
158210
}

app/src/main/java/org/schabi/newpipe/viewmodels/CommentsViewModel.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,39 @@ import androidx.lifecycle.viewModelScope
66
import androidx.paging.Pager
77
import androidx.paging.PagingConfig
88
import androidx.paging.cachedIn
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.flow.SharingStarted
12+
import kotlinx.coroutines.flow.filterIsInstance
13+
import kotlinx.coroutines.flow.flatMapLatest
14+
import kotlinx.coroutines.flow.flowOn
15+
import kotlinx.coroutines.flow.map
16+
import kotlinx.coroutines.flow.stateIn
17+
import org.schabi.newpipe.extractor.comments.CommentsInfo
918
import org.schabi.newpipe.paging.CommentsSource
10-
import org.schabi.newpipe.util.KEY_SERVICE_ID
19+
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
1120
import org.schabi.newpipe.util.KEY_URL
12-
import org.schabi.newpipe.util.NO_SERVICE_ID
21+
import org.schabi.newpipe.viewmodels.util.Resource
1322

1423
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
15-
private val serviceId = savedStateHandle[KEY_SERVICE_ID] ?: NO_SERVICE_ID
16-
private val url = savedStateHandle.get<String>(KEY_URL)
24+
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
25+
.map {
26+
try {
27+
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
28+
} catch (e: Exception) {
29+
Resource.Error(e)
30+
}
31+
}
32+
.flowOn(Dispatchers.IO)
33+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)
1734

18-
val comments = Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
19-
CommentsSource(serviceId, url, null)
20-
}.flow
35+
@OptIn(ExperimentalCoroutinesApi::class)
36+
val comments = uiState
37+
.filterIsInstance<Resource.Success<CommentInfo>>()
38+
.flatMapLatest {
39+
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
40+
CommentsSource(it.data)
41+
}.flow
42+
}
2143
.cachedIn(viewModelScope)
2244
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.schabi.newpipe.viewmodels.util
2+
3+
sealed class Resource<out T> {
4+
data object Loading : Resource<Nothing>()
5+
class Success<T>(val data: T) : Resource<T>()
6+
class Error(val throwable: Throwable) : Resource<Nothing>()
7+
}

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,4 +856,8 @@
856856
<string name="show_less">Show less</string>
857857
<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>
858858
<string name="auto_queue_description">Next</string>
859+
<plurals name="comments">
860+
<item quantity="one">%d comment</item>
861+
<item quantity="other">%d comments</item>
862+
</plurals>
859863
</resources>

0 commit comments

Comments
 (0)