mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 00:10:32 +05:30
refactor: Rewrite comments fragments using Paging library (#5589)
* refactor: Rewrite comments fragment using Paging library * Fix lint issue * refactor: Rewrite comment replies to use paging adapter * Add cachedIn step * Address review comments, display parent comment
This commit is contained in:
parent
5bef1766c5
commit
69cf74e590
@ -1,9 +1,8 @@
|
|||||||
package com.github.libretube.ui.adapters
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
@ -16,13 +15,13 @@ import androidx.core.view.updatePadding
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.fragment.app.replace
|
import androidx.fragment.app.replace
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.obj.Comment
|
import com.github.libretube.api.obj.Comment
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.databinding.CommentsRowBinding
|
import com.github.libretube.databinding.CommentsRowBinding
|
||||||
import com.github.libretube.extensions.formatShort
|
import com.github.libretube.extensions.formatShort
|
||||||
import com.github.libretube.extensions.toID
|
|
||||||
import com.github.libretube.helpers.ClipboardHelper
|
import com.github.libretube.helpers.ClipboardHelper
|
||||||
import com.github.libretube.helpers.ImageHelper
|
import com.github.libretube.helpers.ImageHelper
|
||||||
import com.github.libretube.helpers.NavigationHelper
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
@ -31,35 +30,18 @@ import com.github.libretube.ui.fragments.CommentsRepliesFragment
|
|||||||
import com.github.libretube.ui.viewholders.CommentsViewHolder
|
import com.github.libretube.ui.viewholders.CommentsViewHolder
|
||||||
import com.github.libretube.util.HtmlParser
|
import com.github.libretube.util.HtmlParser
|
||||||
import com.github.libretube.util.LinkHandler
|
import com.github.libretube.util.LinkHandler
|
||||||
import com.github.libretube.util.TextUtils
|
|
||||||
|
|
||||||
class CommentsAdapter(
|
class CommentPagingAdapter(
|
||||||
private val fragment: Fragment?,
|
private val fragment: Fragment?,
|
||||||
private val videoId: String,
|
private val videoId: String,
|
||||||
private val channelAvatar: String?,
|
private val channelAvatar: String?,
|
||||||
private val comments: MutableList<Comment>,
|
private val parentComment: Comment? = null,
|
||||||
private val isRepliesAdapter: Boolean = false,
|
|
||||||
private val handleLink: ((url: String) -> Unit)?,
|
private val handleLink: ((url: String) -> Unit)?,
|
||||||
private val dismiss: () -> Unit
|
private val dismiss: () -> Unit
|
||||||
) : RecyclerView.Adapter<CommentsViewHolder>() {
|
) : PagingDataAdapter<Comment, CommentsViewHolder>(CommentCallback) {
|
||||||
|
private val isRepliesAdapter = parentComment != null
|
||||||
|
|
||||||
fun clear() {
|
override fun getItemCount() = (if (isRepliesAdapter) 1 else 0) + super.getItemCount()
|
||||||
val size: Int = comments.size
|
|
||||||
comments.clear()
|
|
||||||
notifyItemRangeRemoved(0, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateItems(newItems: List<Comment>) {
|
|
||||||
val commentsSize = comments.size
|
|
||||||
comments.addAll(newItems)
|
|
||||||
notifyItemRangeInserted(commentsSize, newItems.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentsViewHolder {
|
|
||||||
val layoutInflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = CommentsRowBinding.inflate(layoutInflater, parent, false)
|
|
||||||
return CommentsViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToReplies(comment: Comment) {
|
private fun navigateToReplies(comment: Comment) {
|
||||||
val args = bundleOf(IntentData.videoId to videoId, IntentData.comment to comment)
|
val args = bundleOf(IntentData.videoId to videoId, IntentData.comment to comment)
|
||||||
@ -69,20 +51,25 @@ class CommentsAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: CommentsViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CommentsViewHolder, position: Int) {
|
||||||
val comment = comments[position]
|
val comment = if (parentComment != null) {
|
||||||
|
if (position == 0) parentComment else getItem(position - 1)!!
|
||||||
|
} else {
|
||||||
|
getItem(position)!!
|
||||||
|
}
|
||||||
holder.binding.apply {
|
holder.binding.apply {
|
||||||
commentAuthor.text = comment.author
|
commentAuthor.text = comment.author
|
||||||
commentAuthor.setBackgroundResource(
|
commentAuthor.setBackgroundResource(
|
||||||
if (comment.channelOwner) R.drawable.comment_channel_owner_bg else 0
|
if (comment.channelOwner) R.drawable.comment_channel_owner_bg else 0
|
||||||
)
|
)
|
||||||
commentInfos.text = TextUtils.SEPARATOR + comment.commentedTime
|
commentInfos.text = root.context
|
||||||
|
.getString(R.string.commentedTimeWithSeparator, comment.commentedTime)
|
||||||
|
|
||||||
commentText.movementMethod = LinkMovementMethodCompat.getInstance()
|
commentText.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||||
commentText.text = comment.commentText?.replace("</a>", "</a> ")
|
commentText.text = comment.commentText?.replace("</a>", "</a> ")
|
||||||
?.parseAsHtml(tagHandler = HtmlParser(LinkHandler(handleLink ?: {})))
|
?.parseAsHtml(tagHandler = HtmlParser(LinkHandler(handleLink ?: {})))
|
||||||
|
|
||||||
|
commentorImage.setImageDrawable(null)
|
||||||
ImageHelper.loadImage(comment.thumbnail, commentorImage, true)
|
ImageHelper.loadImage(comment.thumbnail, commentorImage, true)
|
||||||
likesTextView.text = comment.likeCount.formatShort()
|
likesTextView.text = comment.likeCount.formatShort()
|
||||||
|
|
||||||
@ -91,24 +78,24 @@ class CommentsAdapter(
|
|||||||
creatorReplyImageView.isVisible = true
|
creatorReplyImageView.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment.verified) verifiedImageView.isVisible = true
|
verifiedImageView.isVisible = comment.verified
|
||||||
if (comment.pinned) pinnedImageView.isVisible = true
|
pinnedImageView.isVisible = comment.pinned
|
||||||
if (comment.hearted) heartedImageView.isVisible = true
|
heartedImageView.isVisible = comment.hearted
|
||||||
if (comment.repliesPage != null) repliesCount.isVisible = true
|
repliesCount.isVisible = comment.repliesPage != null
|
||||||
if (comment.replyCount > 0L) {
|
if (comment.replyCount > 0L) {
|
||||||
repliesCount.text = comment.replyCount.formatShort()
|
repliesCount.text = comment.replyCount.formatShort()
|
||||||
}
|
}
|
||||||
|
|
||||||
commentorImage.setOnClickListener {
|
commentorImage.setOnClickListener {
|
||||||
NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
|
NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
|
||||||
dismiss.invoke()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRepliesAdapter) {
|
if (isRepliesAdapter) {
|
||||||
repliesCount.isGone = true
|
repliesCount.isGone = true
|
||||||
|
|
||||||
// highlight the comment that is being replied to
|
// highlight the comment that is being replied to
|
||||||
if (comment == comments.firstOrNull()) {
|
if (position == 0) {
|
||||||
root.setBackgroundColor(
|
root.setBackgroundColor(
|
||||||
ThemeHelper.getThemeColor(
|
ThemeHelper.getThemeColor(
|
||||||
root.context,
|
root.context,
|
||||||
@ -117,7 +104,7 @@ class CommentsAdapter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
root.updatePadding(top = 20)
|
root.updatePadding(top = 20)
|
||||||
root.updateLayoutParams<MarginLayoutParams> { bottomMargin = 20 }
|
root.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin = 20 }
|
||||||
} else {
|
} else {
|
||||||
root.background = AppCompatResources.getDrawable(
|
root.background = AppCompatResources.getDrawable(
|
||||||
root.context,
|
root.context,
|
||||||
@ -127,12 +114,9 @@ class CommentsAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isRepliesAdapter && comment.repliesPage != null) {
|
if (!isRepliesAdapter && comment.repliesPage != null) {
|
||||||
root.setOnClickListener {
|
val onClickListener = View.OnClickListener { navigateToReplies(comment) }
|
||||||
navigateToReplies(comment)
|
root.setOnClickListener(onClickListener)
|
||||||
}
|
commentText.setOnClickListener(onClickListener)
|
||||||
commentText.setOnClickListener {
|
|
||||||
navigateToReplies(comment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
ClipboardHelper.save(
|
ClipboardHelper.save(
|
||||||
@ -145,5 +129,17 @@ class CommentsAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = comments.size
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentsViewHolder {
|
||||||
|
val layoutInflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = CommentsRowBinding.inflate(layoutInflater, parent, false)
|
||||||
|
return CommentsViewHolder(binding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CommentCallback : DiffUtil.ItemCallback<Comment>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Comment, newItem: Comment): Boolean {
|
||||||
|
return oldItem.commentId == newItem.commentId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Comment, newItem: Comment) = true
|
||||||
}
|
}
|
@ -7,19 +7,22 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.databinding.FragmentCommentsBinding
|
import com.github.libretube.databinding.FragmentCommentsBinding
|
||||||
import com.github.libretube.extensions.formatShort
|
import com.github.libretube.extensions.formatShort
|
||||||
import com.github.libretube.ui.adapters.CommentsAdapter
|
import com.github.libretube.ui.adapters.CommentPagingAdapter
|
||||||
import com.github.libretube.ui.models.CommentsViewModel
|
import com.github.libretube.ui.models.CommentsViewModel
|
||||||
import com.github.libretube.ui.sheets.CommentsSheet
|
import com.github.libretube.ui.sheets.CommentsSheet
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CommentsMainFragment : Fragment() {
|
class CommentsMainFragment : Fragment() {
|
||||||
private var _binding: FragmentCommentsBinding? = null
|
private var _binding: FragmentCommentsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
private lateinit var commentsAdapter: CommentsAdapter
|
|
||||||
|
|
||||||
private val viewModel: CommentsViewModel by activityViewModels()
|
private val viewModel: CommentsViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@ -28,13 +31,13 @@ class CommentsMainFragment : Fragment() {
|
|||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
_binding = FragmentCommentsBinding.inflate(inflater, container, false)
|
_binding = FragmentCommentsBinding.inflate(inflater, container, false)
|
||||||
return _binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val binding = _binding ?: return
|
val binding = binding
|
||||||
val layoutManager = LinearLayoutManager(requireContext())
|
val layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.commentsRV.layoutManager = layoutManager
|
binding.commentsRV.layoutManager = layoutManager
|
||||||
binding.commentsRV.setItemViewCacheSize(20)
|
binding.commentsRV.setItemViewCacheSize(20)
|
||||||
@ -47,68 +50,50 @@ class CommentsMainFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.commentsRV.viewTreeObserver.addOnScrollChangedListener {
|
binding.commentsRV.viewTreeObserver.addOnScrollChangedListener {
|
||||||
val viewBinding = _binding ?: return@addOnScrollChangedListener
|
|
||||||
// save the last scroll position to become used next time when the sheet is opened
|
// save the last scroll position to become used next time when the sheet is opened
|
||||||
viewModel.currentCommentsPosition = layoutManager.findFirstVisibleItemPosition()
|
viewModel.currentCommentsPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
|
||||||
// hide or show the scroll to top button
|
// hide or show the scroll to top button
|
||||||
commentsSheet?.binding?.btnScrollToTop?.isVisible = viewModel.currentCommentsPosition != 0
|
commentsSheet?.binding?.btnScrollToTop?.isVisible = viewModel.currentCommentsPosition != 0
|
||||||
|
|
||||||
if (!viewBinding.commentsRV.canScrollVertically(1)) {
|
|
||||||
viewModel.fetchNextComments()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
commentsSheet?.updateFragmentInfo(false, getString(R.string.comments))
|
commentsSheet?.updateFragmentInfo(false, getString(R.string.comments))
|
||||||
|
|
||||||
commentsAdapter = CommentsAdapter(
|
val commentPagingAdapter = CommentPagingAdapter(
|
||||||
this,
|
this,
|
||||||
viewModel.videoId ?: return,
|
viewModel.videoIdLiveData.value ?: return,
|
||||||
viewModel.channelAvatar ?: return,
|
viewModel.channelAvatar ?: return,
|
||||||
viewModel.commentsPage.value?.comments.orEmpty().toMutableList(),
|
handleLink = viewModel.handleLink,
|
||||||
handleLink = viewModel.handleLink
|
|
||||||
) {
|
) {
|
||||||
viewModel.commentsSheetDismiss?.invoke()
|
viewModel.commentsSheetDismiss?.invoke()
|
||||||
}
|
}
|
||||||
binding.commentsRV.adapter = commentsAdapter
|
binding.commentsRV.adapter = commentPagingAdapter
|
||||||
|
|
||||||
if (viewModel.commentsPage.value?.comments.isNullOrEmpty()) {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.fetchComments()
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
} else {
|
launch {
|
||||||
binding.commentsRV.scrollToPosition(viewModel.currentCommentsPosition)
|
commentPagingAdapter.loadStateFlow.collect {
|
||||||
}
|
binding.progress.isVisible = it.refresh is LoadState.Loading
|
||||||
|
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached) {
|
||||||
_binding?.progress?.isVisible = it == true
|
binding.errorTV.text = getString(R.string.no_comments_available)
|
||||||
}
|
binding.errorTV.isVisible = true
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// listen for new comments to be loaded
|
launch {
|
||||||
viewModel.commentsPage.observe(viewLifecycleOwner) {
|
viewModel.commentsFlow.collect {
|
||||||
if (it == null) return@observe
|
commentPagingAdapter.submitData(it)
|
||||||
val viewBinding = _binding ?: return@observe
|
|
||||||
|
|
||||||
if (it.disabled) {
|
val commentCount = commentPagingAdapter.itemCount.toLong().formatShort()
|
||||||
viewBinding.errorTV.isVisible = true
|
commentsSheet?.updateFragmentInfo(
|
||||||
return@observe
|
false,
|
||||||
|
getString(R.string.comments_count, commentCount)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsSheet?.updateFragmentInfo(
|
|
||||||
false,
|
|
||||||
"${getString(R.string.comments)} (${it.commentCount.formatShort()})"
|
|
||||||
)
|
|
||||||
if (it.comments.isEmpty()) {
|
|
||||||
viewBinding.errorTV.text = getString(R.string.no_comments_available)
|
|
||||||
viewBinding.errorTV.isVisible = true
|
|
||||||
return@observe
|
|
||||||
}
|
|
||||||
|
|
||||||
// sometimes the received comments have the same size as the existing ones
|
|
||||||
// which causes comments.subList to throw InvalidArgumentException
|
|
||||||
if (commentsAdapter.itemCount > it.comments.size) return@observe
|
|
||||||
|
|
||||||
commentsAdapter.updateItems(
|
|
||||||
// only add the new comments to the recycler view
|
|
||||||
it.comments.subList(commentsAdapter.itemCount, it.comments.size)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -10,41 +9,35 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
|
||||||
import com.github.libretube.api.obj.Comment
|
import com.github.libretube.api.obj.Comment
|
||||||
import com.github.libretube.api.obj.CommentsPage
|
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.databinding.FragmentCommentsBinding
|
import com.github.libretube.databinding.FragmentCommentsBinding
|
||||||
import com.github.libretube.extensions.TAG
|
|
||||||
import com.github.libretube.extensions.formatShort
|
import com.github.libretube.extensions.formatShort
|
||||||
import com.github.libretube.extensions.parcelable
|
import com.github.libretube.extensions.parcelable
|
||||||
import com.github.libretube.ui.adapters.CommentsAdapter
|
import com.github.libretube.ui.adapters.CommentPagingAdapter
|
||||||
import com.github.libretube.ui.extensions.filterNonEmptyComments
|
|
||||||
import com.github.libretube.ui.models.CommentsViewModel
|
import com.github.libretube.ui.models.CommentsViewModel
|
||||||
import com.github.libretube.ui.sheets.CommentsSheet
|
import com.github.libretube.ui.sheets.CommentsSheet
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class CommentsRepliesFragment : Fragment() {
|
class CommentsRepliesFragment : Fragment() {
|
||||||
private var _binding: FragmentCommentsBinding? = null
|
private var _binding: FragmentCommentsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var repliesPage: CommentsPage
|
|
||||||
private lateinit var repliesAdapter: CommentsAdapter
|
|
||||||
private val viewModel: CommentsViewModel by activityViewModels()
|
private val viewModel: CommentsViewModel by activityViewModels()
|
||||||
|
|
||||||
private var isLoading = false
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
_binding = FragmentCommentsBinding.inflate(inflater, container, false)
|
_binding = FragmentCommentsBinding.inflate(inflater, container, false)
|
||||||
return _binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -53,18 +46,17 @@ class CommentsRepliesFragment : Fragment() {
|
|||||||
val videoId = arguments.getString(IntentData.videoId, "")
|
val videoId = arguments.getString(IntentData.videoId, "")
|
||||||
val comment = arguments.parcelable<Comment>(IntentData.comment)!!
|
val comment = arguments.parcelable<Comment>(IntentData.comment)!!
|
||||||
|
|
||||||
val binding = _binding ?: return
|
val binding = binding
|
||||||
|
|
||||||
val commentsSheet = parentFragment as? CommentsSheet
|
val commentsSheet = parentFragment as? CommentsSheet
|
||||||
commentsSheet?.binding?.btnScrollToTop?.isGone = true
|
commentsSheet?.binding?.btnScrollToTop?.isGone = true
|
||||||
|
|
||||||
repliesAdapter = CommentsAdapter(
|
val repliesAdapter = CommentPagingAdapter(
|
||||||
null,
|
null,
|
||||||
videoId,
|
videoId,
|
||||||
viewModel.channelAvatar,
|
viewModel.channelAvatar,
|
||||||
mutableListOf(comment),
|
comment,
|
||||||
true,
|
viewModel.handleLink,
|
||||||
viewModel.handleLink
|
|
||||||
) {
|
) {
|
||||||
viewModel.commentsSheetDismiss?.invoke()
|
viewModel.commentsSheetDismiss?.invoke()
|
||||||
}
|
}
|
||||||
@ -77,49 +69,27 @@ class CommentsRepliesFragment : Fragment() {
|
|||||||
binding.commentsRV.layoutManager = LinearLayoutManager(context)
|
binding.commentsRV.layoutManager = LinearLayoutManager(context)
|
||||||
binding.commentsRV.adapter = repliesAdapter
|
binding.commentsRV.adapter = repliesAdapter
|
||||||
|
|
||||||
binding.commentsRV.viewTreeObserver.addOnScrollChangedListener {
|
viewModel.selectedCommentLiveData.postValue(comment.repliesPage)
|
||||||
if (_binding?.commentsRV?.canScrollVertically(1) == false && ::repliesPage.isInitialized) {
|
|
||||||
if (repliesPage.nextpage == null) return@addOnScrollChangedListener
|
|
||||||
|
|
||||||
fetchReplies(videoId, repliesPage.nextpage!!)
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
launch {
|
||||||
|
repliesAdapter.loadStateFlow.collect {
|
||||||
|
binding.progress.isVisible = it.refresh is LoadState.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
viewModel.commentRepliesFlow.collect {
|
||||||
|
repliesAdapter.submitData(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadInitialReplies(videoId, comment.repliesPage.orEmpty())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadInitialReplies(videoId: String, nextPage: String) {
|
|
||||||
_binding?.progress?.isVisible = true
|
|
||||||
fetchReplies(videoId, nextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchReplies(videoId: String, nextPage: String) {
|
|
||||||
_binding?.progress?.isVisible = true
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (isLoading) return@launch
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
repliesPage = try {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getCommentsNextPage(videoId, nextPage)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG(), "IOException, you might not have internet connection")
|
|
||||||
return@launch
|
|
||||||
} finally {
|
|
||||||
_binding?.progress?.isGone = true
|
|
||||||
}
|
|
||||||
repliesPage.comments = repliesPage.comments.filterNonEmptyComments()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
repliesAdapter.updateItems(repliesPage.comments)
|
|
||||||
}
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -548,7 +548,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
// set the max height to not cover the currently playing video
|
// set the max height to not cover the currently playing video
|
||||||
commentsViewModel.handleLink = this::handleLink
|
commentsViewModel.handleLink = this::handleLink
|
||||||
updateMaxSheetHeight()
|
updateMaxSheetHeight()
|
||||||
commentsViewModel.videoId = videoId
|
commentsViewModel.videoIdLiveData.postValue(videoId)
|
||||||
commentsViewModel.channelAvatar = streams.uploaderAvatar
|
commentsViewModel.channelAvatar = streams.uploaderAvatar
|
||||||
CommentsSheet().show(childFragmentManager)
|
CommentsSheet().show(childFragmentManager)
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,45 @@
|
|||||||
package com.github.libretube.ui.models
|
package com.github.libretube.ui.models
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import androidx.paging.Pager
|
||||||
import com.github.libretube.api.obj.CommentsPage
|
import androidx.paging.PagingConfig
|
||||||
import com.github.libretube.extensions.TAG
|
import androidx.paging.cachedIn
|
||||||
import com.github.libretube.ui.extensions.filterNonEmptyComments
|
import com.github.libretube.ui.models.sources.CommentPagingSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.github.libretube.ui.models.sources.CommentRepliesPagingSource
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
|
||||||
class CommentsViewModel : ViewModel() {
|
class CommentsViewModel : ViewModel() {
|
||||||
val commentsPage = MutableLiveData<CommentsPage?>()
|
val videoIdLiveData = MutableLiveData<String>()
|
||||||
|
val selectedCommentLiveData = MutableLiveData<String>()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val commentsFlow = videoIdLiveData.asFlow()
|
||||||
|
.flatMapLatest {
|
||||||
|
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||||
|
CommentPagingSource(it)
|
||||||
|
}.flow
|
||||||
|
}
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val commentRepliesFlow = videoIdLiveData.asFlow()
|
||||||
|
.combine(selectedCommentLiveData.asFlow()) { videoId, comment -> videoId to comment }
|
||||||
|
.flatMapLatest { (videoId, commentPage) ->
|
||||||
|
Pager(PagingConfig(20, enablePlaceholders = false)) {
|
||||||
|
CommentRepliesPagingSource(videoId, commentPage)
|
||||||
|
}.flow
|
||||||
|
}
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
val commentSheetExpand = MutableLiveData<Boolean?>()
|
val commentSheetExpand = MutableLiveData<Boolean?>()
|
||||||
|
|
||||||
var videoId: String? = null
|
|
||||||
var channelAvatar: String? = null
|
var channelAvatar: String? = null
|
||||||
var handleLink: ((url: String) -> Unit)? = null
|
var handleLink: ((url: String) -> Unit)? = null
|
||||||
|
|
||||||
private var nextPage: String? = null
|
|
||||||
var isLoading = MutableLiveData<Boolean>()
|
|
||||||
var currentCommentsPosition = 0
|
var currentCommentsPosition = 0
|
||||||
var commentsSheetDismiss: (() -> Unit)? = null
|
var commentsSheetDismiss: (() -> Unit)? = null
|
||||||
|
|
||||||
@ -31,62 +49,7 @@ class CommentsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchComments() {
|
|
||||||
val videoId = videoId ?: return
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
val response = try {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getComments(videoId)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG(), e.toString())
|
|
||||||
return@launch
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPage = response.nextpage
|
|
||||||
response.comments = response.comments.filterNonEmptyComments()
|
|
||||||
commentsPage.postValue(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchNextComments() {
|
|
||||||
if (isLoading.value == true || nextPage == null || videoId == null) return
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
val response = try {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getCommentsNextPage(videoId!!, nextPage!!)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG(), e.toString())
|
|
||||||
return@launch
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val updatedPage = commentsPage.value?.apply {
|
|
||||||
comments += response.comments
|
|
||||||
.filterNonEmptyComments()
|
|
||||||
.filter { comment -> comments.none { it.commentId == comment.commentId } }
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPage = response.nextpage
|
|
||||||
commentsPage.postValue(updatedPage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
isLoading.value = false
|
|
||||||
nextPage = null
|
|
||||||
commentsPage.value = null
|
|
||||||
videoId = null
|
|
||||||
setCommentSheetExpand(null)
|
setCommentSheetExpand(null)
|
||||||
currentCommentsPosition = 0
|
currentCommentsPosition = 0
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.github.libretube.ui.models.sources
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.github.libretube.api.RetrofitInstance
|
||||||
|
import com.github.libretube.api.obj.Comment
|
||||||
|
|
||||||
|
class CommentPagingSource(private val videoId: String) : PagingSource<String, Comment>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<String, Comment>) = null
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||||
|
return try {
|
||||||
|
val result = params.key?.let {
|
||||||
|
RetrofitInstance.api.getCommentsNextPage(videoId, it)
|
||||||
|
} ?: RetrofitInstance.api.getComments(videoId)
|
||||||
|
LoadResult.Page(result.comments, null, result.nextpage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.github.libretube.ui.models.sources
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.github.libretube.api.RetrofitInstance
|
||||||
|
import com.github.libretube.api.obj.Comment
|
||||||
|
|
||||||
|
class CommentRepliesPagingSource(
|
||||||
|
private val videoId: String,
|
||||||
|
private val commentNextPage: String?,
|
||||||
|
) : PagingSource<String, Comment>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<String, Comment>) = null
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||||
|
return try {
|
||||||
|
val key = params.key.orEmpty().ifEmpty { commentNextPage.orEmpty() }
|
||||||
|
val result = RetrofitInstance.api.getCommentsNextPage(videoId, key)
|
||||||
|
LoadResult.Page(result.comments, null, result.nextpage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -66,10 +66,12 @@
|
|||||||
<string name="donate">Donate</string>
|
<string name="donate">Donate</string>
|
||||||
<string name="uploaderAndVideoCount">%1$s • %2$d videos</string>
|
<string name="uploaderAndVideoCount">%1$s • %2$d videos</string>
|
||||||
<string name="subscriberAndVideoCounts">%1$s subscribers • %2$d videos</string>
|
<string name="subscriberAndVideoCounts">%1$s subscribers • %2$d videos</string>
|
||||||
|
<string name="commentedTimeWithSeparator"> • %1$s</string>
|
||||||
<string name="videoCount">%1$d videos</string>
|
<string name="videoCount">%1$d videos</string>
|
||||||
<string name="noInternet">Connect to the Internet first.</string>
|
<string name="noInternet">Connect to the Internet first.</string>
|
||||||
<string name="retry">Retry</string>
|
<string name="retry">Retry</string>
|
||||||
<string name="comments">Comments</string>
|
<string name="comments">Comments</string>
|
||||||
|
<string name="comments_count">Comments (%1$s)</string>
|
||||||
<string name="replies">Replies</string>
|
<string name="replies">Replies</string>
|
||||||
<string name="channels">Channels</string>
|
<string name="channels">Channels</string>
|
||||||
<string name="all">All</string>
|
<string name="all">All</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user