Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ class CommentInfo(
val comments: List<CommentsInfoItem>,
val nextPage: Page?,
val commentCount: Int,
val isCommentsDisabled: Boolean
val isCommentsDisabled: Boolean,
val isLiveChat: Boolean
) {
constructor(commentsInfo: CommentsInfo) : this(
commentsInfo.serviceId,
commentsInfo.url,
commentsInfo.relatedItems,
commentsInfo.nextPage,
commentsInfo.commentsCount,
commentsInfo.isCommentsDisabled
commentsInfo.isCommentsDisabled,
commentsInfo.isLiveChat
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -45,18 +46,28 @@ import org.schabi.newpipe.viewmodels.util.Resource
@Composable
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
CommentSection(state, commentsViewModel.comments)
val liveChatItems by commentsViewModel.liveChatItems.collectAsStateWithLifecycle()
CommentSection(state, commentsViewModel.comments, liveChatItems)
}

@Composable
private fun CommentSection(
uiState: Resource<CommentInfo>,
commentsFlow: Flow<PagingData<CommentsInfoItem>>
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
liveChatItems: List<CommentsInfoItem>
) {
val comments = commentsFlow.collectAsLazyPagingItems()
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state = rememberLazyListState()

// Auto-scroll to top when new live chat messages arrive
val isLiveChat = uiState is Resource.Success && uiState.data.isLiveChat
LaunchedEffect(liveChatItems.size) {
if (isLiveChat && liveChatItems.isNotEmpty()) {
state.scrollToItem(0)
}
}

LazyColumnThemedScrollbar(state = state) {
LazyColumn(
modifier = Modifier
Expand All @@ -75,7 +86,7 @@ private fun CommentSection(
val commentInfo = uiState.data
val count = commentInfo.commentCount

if (commentInfo.isCommentsDisabled) {
if (commentInfo.isCommentsDisabled && !commentInfo.isLiveChat) {
item {
EmptyStateComposable(
spec = EmptyStateSpec.DisabledComments,
Expand All @@ -85,7 +96,7 @@ private fun CommentSection(

)
}
} else if (count == 0) {
} else if (count == 0 && !commentInfo.isLiveChat) {
item {
EmptyStateComposable(
spec = EmptyStateSpec.NoComments,
Expand All @@ -95,8 +106,8 @@ private fun CommentSection(
)
}
} else {
// do not show anything if the comment count is unknown
if (count >= 0) {
// Show title for regular comments, but not for live chat
if (count >= 0 && !commentInfo.isLiveChat) {
item {
Text(
modifier = Modifier
Expand All @@ -107,37 +118,68 @@ private fun CommentSection(
)
}
}
when (val refresh = comments.loadState.refresh) {
is LoadState.Loading -> {

if (commentInfo.isLiveChat) {
// Live chat: render items directly without Paging 3
if (liveChatItems.isEmpty()) {
item {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
EmptyStateComposable(
spec = EmptyStateSpec.NoComments,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)
)
}
} else {
items(liveChatItems.size, key = { liveChatItems[it].commentId }) {
Comment(comment = liveChatItems[it]) {}
}
}
} else {
// Normal comments via Paging 3
when (val refresh = comments.loadState.refresh) {
is LoadState.Loading -> {
item {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
}

is LoadState.Error -> {
val errorInfo = ErrorInfo(
throwable = refresh.error,
userAction = UserAction.REQUESTED_COMMENTS,
request = "comments"
)
is LoadState.Error -> {
val errorInfo = ErrorInfo(
throwable = refresh.error,
userAction = UserAction.REQUESTED_COMMENTS,
request = "comments"
)

item {
Box(
modifier = Modifier
.fillMaxWidth()
) {
ErrorPanel(
errorInfo = errorInfo,
onRetry = { comments.retry() },
modifier = Modifier.align(Alignment.Center)
)
item {
Box(
modifier = Modifier
.fillMaxWidth()
) {
ErrorPanel(
errorInfo = errorInfo,
onRetry = { comments.retry() },
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}

else -> {
items(comments.itemCount) {
Comment(comment = comments[it]!!) {}
else -> {
if (comments.itemCount == 0) {
item {
EmptyStateComposable(
spec = EmptyStateSpec.NoComments,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)
)
}
} else {
items(comments.itemCount) {
Comment(comment = comments[it]!!) {}
}
}
}
}
}
Expand Down Expand Up @@ -175,7 +217,7 @@ private fun CommentSection(
private fun CommentSectionLoadingPreview() {
AppTheme {
Surface {
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf(), liveChatItems = emptyList())
}
}
}
Expand Down Expand Up @@ -211,10 +253,12 @@ private fun CommentSectionSuccessPreview() {
comments = comments,
nextPage = null,
commentCount = 10,
isCommentsDisabled = false
isCommentsDisabled = false,
isLiveChat = false
)
),
commentsFlow = flowOf(PagingData.from(comments))
commentsFlow = flowOf(PagingData.from(comments)),
liveChatItems = emptyList()
)
}
}
Expand All @@ -226,7 +270,7 @@ private fun CommentSectionSuccessPreview() {
private fun CommentSectionErrorPreview() {
AppTheme {
Surface {
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf())
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf(), liveChatItems = emptyList())
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/org/schabi/newpipe/util/InfoCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public enum Type {
CHANNEL_TAB,
COMMENTS,
PLAYLIST,
KIOSK,
KIOSK
}

public static InfoCache getInstance() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
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 kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.comments.CommentsInfo
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.paging.CommentsSource
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
import org.schabi.newpipe.util.KEY_URL
Expand All @@ -24,21 +33,60 @@ class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
.map {
try {
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
val info = CommentsInfo.getInfo(it)
Resource.Success(CommentInfo(info))
} catch (e: Exception) {
Resource.Error(e)
}
}
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)

// Separate flow for live chat items (not using Paging 3)
private val _liveChatItems = MutableStateFlow<List<CommentsInfoItem>>(emptyList())
val liveChatItems: StateFlow<List<CommentsInfoItem>> = _liveChatItems

@OptIn(ExperimentalCoroutinesApi::class)
val comments = uiState
val comments: Flow<PagingData<CommentsInfoItem>> = uiState
.filterIsInstance<Resource.Success<CommentInfo>>()
.flatMapLatest {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(it.data)
}.flow
val info = it.data
if (info.isLiveChat) {
_liveChatItems.value = info.comments
startLiveChatPolling(info)
// Return empty PagingData for live chat (items come from liveChatItems flow)
kotlinx.coroutines.flow.flowOf(androidx.paging.PagingData.empty())
} else {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(info)
}.flow
}
}
.cachedIn(viewModelScope)

private fun startLiveChatPolling(info: CommentInfo) {
var nextPage = info.nextPage

viewModelScope.launch(Dispatchers.IO) {
while (isActive) {
delay(3000)
if (nextPage == null) {
continue
}
try {
val result = CommentsInfo.getMoreItems(
NewPipe.getService(info.serviceId),
info.url,
nextPage
)
if (result.items.isNotEmpty()) {
_liveChatItems.value = result.items + _liveChatItems.value
}
nextPage = result.nextPage
} catch (e: Exception) {
// Silently ignore polling errors
}
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/settings_keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1513,4 +1513,5 @@
<item>@string/image_quality_medium_key</item>
<item>@string/image_quality_high_key</item>
</string-array>

</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -912,4 +912,5 @@
<string name="kao_dialog_warning">In August 2025, Google announced that as of September 2026, installing apps will require developer verification for all Android apps on certified devices, including those installed outside of the Play Store. Since the developers of NewPipe do not agree to this requirement, NewPipe will no longer work on certified Android devices after that time.</string>
<string name="kao_dialog_more_info">Details</string>
<string name="kao_solution">Solution</string>

</resources>
Loading