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:
Isira Seneviratne 2024-02-11 18:08:06 +05:30 committed by GitHub
parent 5bef1766c5
commit 69cf74e590
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 175 additions and 215 deletions

View File

@ -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
} }

View File

@ -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)
)
} }
} }

View File

@ -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
}
}
} }

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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>