Skip to content
Draft
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 @@ -40,6 +40,11 @@ public void addFragment(final Fragment fragment, final String title) {
mFragmentTitleList.add(title);
}

public void addFragment(final Fragment fragment, final String title, final int position) {
mFragmentList.add(position, fragment);
mFragmentTitleList.add(position, title);
}

public void clearAllItems() {
mFragmentList.clear();
mFragmentTitleList.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.fragments.EmptyFragment
import org.schabi.newpipe.fragments.MainFragment
import org.schabi.newpipe.fragments.list.comments.CommentsFragment.Companion.getInstance
import org.schabi.newpipe.fragments.list.comments.LiveChatFragment
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment.Companion.getInstance
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
Expand Down Expand Up @@ -323,7 +324,10 @@ class VideoDetailFragment :
if (tabSettingsChanged) {
tabSettingsChanged = false
initTabs()
currentInfo?.let { updateTabs(it) }
currentInfo?.let {
updateTabs(it)
addLiveChatTabIfNeeded(it)
}
}

// Check if it was loading when the fragment was stopped/paused
Expand Down Expand Up @@ -912,6 +916,31 @@ class VideoDetailFragment :
}
}

private fun addLiveChatTabIfNeeded(streamInfo: StreamInfo) {
if (!streamInfo.hasLiveChat()) {
return
}
val continuation = streamInfo.liveChatContinuation ?: return
if (continuation.isEmpty()) {
return
}
if (pageAdapter.getItemPositionByTitle(LIVE_CHAT_TAB_TAG) != -1) {
return
}

// Append live chat tab at the end to avoid FragmentPagerAdapter
// position synchronization issues when inserting in the middle
pageAdapter.addFragment(
LiveChatFragment.getInstance(serviceId, url, continuation),
LIVE_CHAT_TAB_TAG
)
tabIcons.add(R.drawable.ic_live_tv)
tabContentDescriptions.add(R.string.live_chat_tab_description)
pageAdapter.notifyDataSetUpdate()
updateTabIconsAndContentDescriptions()
updateTabLayoutVisibility()
}

fun updateTabLayoutVisibility() {
if (nullableBinding == null) {
// If binding is null we do not need to and should not do anything with its object(s)
Expand Down Expand Up @@ -1418,6 +1447,7 @@ class VideoDetailFragment :
setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)

updateTabs(info)
addLiveChatTabIfNeeded(info)

binding.detailThumbnailPlayButton.animate(true, 200)
binding.detailVideoTitleView.text = title
Expand Down Expand Up @@ -2342,6 +2372,7 @@ class VideoDetailFragment :
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED"

private const val COMMENTS_TAB_TAG = "COMMENTS"
private const val LIVE_CHAT_TAB_TAG = "LIVE_CHAT"
private const val RELATED_TAB_TAG = "NEXT VIDEO"
private const val DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"
private const val EMPTY_TAB_TAG = "EMPTY TAB"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.schabi.newpipe.fragments.list.comments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.material3.Surface
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.compose.content
import org.schabi.newpipe.ui.components.video.comment.LiveChatSection
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.KEY_SERVICE_ID
import org.schabi.newpipe.util.KEY_URL
import org.schabi.newpipe.viewmodels.LiveChatViewModel

class LiveChatFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = content {
AppTheme {
Surface {
LiveChatSection()
}
}
}

companion object {
@JvmStatic
fun getInstance(serviceId: Int, url: String?, liveChatContinuation: String) = LiveChatFragment().apply {
arguments = bundleOf(
KEY_SERVICE_ID to serviceId,
KEY_URL to url,
LiveChatViewModel.KEY_LIVE_CHAT_CONTINUATION to liveChatContinuation
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ 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 comments: List<org.schabi.newpipe.extractor.comments.CommentsInfoItem>,
val nextPage: Page?,
val commentCount: Int,
val isCommentsDisabled: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.flowOf
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.ui.components.common.ErrorPanel
Expand Down Expand Up @@ -82,7 +81,6 @@ private fun CommentSection(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)

)
}
} else if (count == 0) {
Expand All @@ -95,7 +93,6 @@ private fun CommentSection(
)
}
} else {
// do not show anything if the comment count is unknown
if (count >= 0) {
item {
Text(
Expand All @@ -107,6 +104,7 @@ private fun CommentSection(
)
}
}

when (val refresh = comments.loadState.refresh) {
is LoadState.Loading -> {
item {
Expand Down Expand Up @@ -136,8 +134,19 @@ private fun CommentSection(
}

else -> {
items(comments.itemCount) {
Comment(comment = comments[it]!!) {}
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 @@ -191,7 +200,7 @@ private fun CommentSectionSuccessPreview() {
Description.PLAIN_TEXT
),
uploaderName = "Test",
replies = Page(""),
replies = org.schabi.newpipe.extractor.Page(""),
replyCount = 10
)
) + (2..10).map {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.schabi.newpipe.ui.components.video.comment

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
import org.schabi.newpipe.ui.components.common.LoadingIndicator
import org.schabi.newpipe.viewmodels.LiveChatViewModel

@Composable
fun LiveChatSection(liveChatViewModel: LiveChatViewModel = viewModel()) {
val liveChatItems by liveChatViewModel.liveChatItems.collectAsStateWithLifecycle()
val state = rememberLazyListState()
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val coroutineScope = rememberCoroutineScope()

// Track whether user is at the top of the list
val isAtTop by remember { derivedStateOf { state.firstVisibleItemIndex == 0 } }

// Track how many messages were seen while at the top
val lastSeenCount = remember { mutableStateOf(0) }
if (isAtTop && liveChatItems.isNotEmpty()) {
lastSeenCount.value = liveChatItems.size
}

val unreadCount = liveChatItems.size - lastSeenCount.value

Box(modifier = Modifier.fillMaxSize()) {
LazyColumnThemedScrollbar(state = state) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollInterop),
state = state
) {
item {
Text(
modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 4.dp),
text = "Live Chat",
style = MaterialTheme.typography.titleMedium
)
}

if (liveChatItems.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp),
contentAlignment = Alignment.Center
) {
LoadingIndicator()
}
}
} else {
items(liveChatItems.size, key = { liveChatItems[it].commentId }) {
Comment(comment = liveChatItems[it]) {}
}
}
}
}

// Floating button to jump to newest messages
AnimatedVisibility(
visible = !isAtTop && unreadCount > 0,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
enter = fadeIn(),
exit = fadeOut()
) {
FloatingActionButton(
onClick = {
coroutineScope.launch {
state.scrollToItem(0)
}
}
) {
BadgedBox(
badge = {
Badge(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
) {
Text(
text = unreadCount.toString(),
style = MaterialTheme.typography.labelSmall
)
}
}
) {
Icon(
imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = "Scroll to new messages"
)
}
}
}
}
}
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,9 +5,11 @@ 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.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
Expand All @@ -24,7 +26,8 @@ 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)
}
Expand All @@ -33,7 +36,7 @@ class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)

@OptIn(ExperimentalCoroutinesApi::class)
val comments = uiState
val comments: Flow<PagingData<org.schabi.newpipe.extractor.comments.CommentsInfoItem>> = uiState
.filterIsInstance<Resource.Success<CommentInfo>>()
.flatMapLatest {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
Expand Down
Loading
Loading