Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Rewrite comments fragments using Paging library #5589

Merged
merged 5 commits into from
Feb 11, 2024
Merged
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
@@ -1,9 +1,8 @@
package com.github.libretube.ui.adapters

import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.os.bundleOf
Expand All @@ -16,13 +15,13 @@
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
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.api.obj.Comment
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.CommentsRowBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ClipboardHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
Expand All @@ -31,35 +30,18 @@
import com.github.libretube.ui.viewholders.CommentsViewHolder
import com.github.libretube.util.HtmlParser
import com.github.libretube.util.LinkHandler
import com.github.libretube.util.TextUtils

class CommentsAdapter(
class CommentPagingAdapter(
private val fragment: Fragment?,
private val videoId: String,
private val channelAvatar: String?,
private val comments: MutableList<Comment>,
private val isRepliesAdapter: Boolean = false,
private val parentComment: Comment? = null,
private val handleLink: ((url: String) -> Unit)?,
private val dismiss: () -> Unit
) : RecyclerView.Adapter<CommentsViewHolder>() {
) : PagingDataAdapter<Comment, CommentsViewHolder>(CommentCallback) {
private val isRepliesAdapter = parentComment != null

fun clear() {
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)
}
override fun getItemCount() = (if (isRepliesAdapter) 1 else 0) + super.getItemCount()

private fun navigateToReplies(comment: Comment) {
val args = bundleOf(IntentData.videoId to videoId, IntentData.comment to comment)
Expand All @@ -69,20 +51,25 @@
}
}

@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CommentsViewHolder, position: Int) {
val comment = comments[position]
val comment = if (parentComment != null) {

Check failure on line 55 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 A multiline expression should start on a new line Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:55:23: error: A multiline expression should start on a new line (standard:multiline-expression-wrapping)
if (position == 0) parentComment else getItem(position - 1)!!
} else {
getItem(position)!!
}
holder.binding.apply {
commentAuthor.text = comment.author
commentAuthor.setBackgroundResource(
if (comment.channelOwner) R.drawable.comment_channel_owner_bg else 0
)
commentInfos.text = TextUtils.SEPARATOR + comment.commentedTime
commentInfos.text = root.context

Check failure on line 65 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 A multiline expression should start on a new line Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:65:33: error: A multiline expression should start on a new line (standard:multiline-expression-wrapping)
.getString(R.string.commentedTimeWithSeparator, comment.commentedTime)

commentText.movementMethod = LinkMovementMethodCompat.getInstance()
commentText.text = comment.commentText?.replace("</a>", "</a> ")
?.parseAsHtml(tagHandler = HtmlParser(LinkHandler(handleLink ?: {})))

commentorImage.setImageDrawable(null)
ImageHelper.loadImage(comment.thumbnail, commentorImage, true)
likesTextView.text = comment.likeCount.formatShort()

Expand All @@ -91,24 +78,24 @@
creatorReplyImageView.isVisible = true
}

if (comment.verified) verifiedImageView.isVisible = true
if (comment.pinned) pinnedImageView.isVisible = true
if (comment.hearted) heartedImageView.isVisible = true
if (comment.repliesPage != null) repliesCount.isVisible = true
verifiedImageView.isVisible = comment.verified
pinnedImageView.isVisible = comment.pinned
heartedImageView.isVisible = comment.hearted
repliesCount.isVisible = comment.repliesPage != null
if (comment.replyCount > 0L) {
repliesCount.text = comment.replyCount.formatShort()
}

commentorImage.setOnClickListener {
NavigationHelper.navigateChannel(root.context, comment.commentorUrl)
dismiss.invoke()
dismiss()
}

if (isRepliesAdapter) {
repliesCount.isGone = true

// highlight the comment that is being replied to
if (comment == comments.firstOrNull()) {
if (position == 0) {
root.setBackgroundColor(
ThemeHelper.getThemeColor(
root.context,
Expand All @@ -117,7 +104,7 @@
)

root.updatePadding(top = 20)
root.updateLayoutParams<MarginLayoutParams> { bottomMargin = 20 }
root.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin = 20 }
} else {
root.background = AppCompatResources.getDrawable(
root.context,
Expand All @@ -127,12 +114,9 @@
}

if (!isRepliesAdapter && comment.repliesPage != null) {
root.setOnClickListener {
navigateToReplies(comment)
}
commentText.setOnClickListener {
navigateToReplies(comment)
}
val onClickListener = View.OnClickListener { navigateToReplies(comment) }
root.setOnClickListener(onClickListener)
commentText.setOnClickListener(onClickListener)
}
root.setOnLongClickListener {
ClipboardHelper.save(
Expand All @@ -145,5 +129,17 @@
}
}

override fun getItemCount() = comments.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentsViewHolder {

Check failure on line 132 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Newline expected after opening parenthesis Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:132:37: error: Newline expected after opening parenthesis (standard:function-signature)

Check failure on line 132 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Parameter should start on a newline Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:132:56: error: Parameter should start on a newline (standard:function-signature)

Check failure on line 132 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Newline expected before closing parenthesis Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:132:69: error: Newline expected before closing parenthesis (standard:function-signature)
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 {

Check failure on line 140 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Newline expected after opening parenthesis Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:140:34: error: Newline expected after opening parenthesis (standard:function-signature)

Check failure on line 140 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Parameter should start on a newline Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:140:52: error: Parameter should start on a newline (standard:function-signature)

Check failure on line 140 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Newline expected before closing parenthesis Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:140:68: error: Newline expected before closing parenthesis (standard:function-signature)
return oldItem.commentId == newItem.commentId
}

override fun areContentsTheSame(oldItem: Comment, newItem: Comment) = true

Check failure on line 144 in app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Newline expected after opening parenthesis Raw Output: app/src/main/java/com/github/libretube/ui/adapters/CommentPagingAdapter.kt:144:37: error: Newline expected after opening parenthesis (standard:function-signature)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
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 com.github.libretube.R
import com.github.libretube.databinding.FragmentCommentsBinding
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.sheets.CommentsSheet
import kotlinx.coroutines.launch

class CommentsMainFragment : Fragment() {
private var _binding: FragmentCommentsBinding? = null

private lateinit var commentsAdapter: CommentsAdapter

private val binding get() = _binding!!
private val viewModel: CommentsViewModel by activityViewModels()

override fun onCreateView(
Expand All @@ -28,13 +31,13 @@ class CommentsMainFragment : Fragment() {
savedInstanceState: Bundle?
): View {
_binding = FragmentCommentsBinding.inflate(inflater, container, false)
return _binding!!.root
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val binding = _binding ?: return
val binding = binding
val layoutManager = LinearLayoutManager(requireContext())
binding.commentsRV.layoutManager = layoutManager
binding.commentsRV.setItemViewCacheSize(20)
Expand All @@ -47,68 +50,50 @@ class CommentsMainFragment : Fragment() {
}

binding.commentsRV.viewTreeObserver.addOnScrollChangedListener {
val viewBinding = _binding ?: return@addOnScrollChangedListener
// save the last scroll position to become used next time when the sheet is opened
viewModel.currentCommentsPosition = layoutManager.findFirstVisibleItemPosition()

// hide or show the scroll to top button
commentsSheet?.binding?.btnScrollToTop?.isVisible = viewModel.currentCommentsPosition != 0

if (!viewBinding.commentsRV.canScrollVertically(1)) {
viewModel.fetchNextComments()
}
}
commentsSheet?.updateFragmentInfo(false, getString(R.string.comments))

commentsAdapter = CommentsAdapter(
val commentPagingAdapter = CommentPagingAdapter(
this,
viewModel.videoId ?: return,
viewModel.videoIdLiveData.value ?: return,
viewModel.channelAvatar ?: return,
viewModel.commentsPage.value?.comments.orEmpty().toMutableList(),
handleLink = viewModel.handleLink
handleLink = viewModel.handleLink,
) {
viewModel.commentsSheetDismiss?.invoke()
}
binding.commentsRV.adapter = commentsAdapter

if (viewModel.commentsPage.value?.comments.isNullOrEmpty()) {
viewModel.fetchComments()
} else {
binding.commentsRV.scrollToPosition(viewModel.currentCommentsPosition)
}

viewModel.isLoading.observe(viewLifecycleOwner) {
_binding?.progress?.isVisible = it == true
}

// listen for new comments to be loaded
viewModel.commentsPage.observe(viewLifecycleOwner) {
if (it == null) return@observe
val viewBinding = _binding ?: return@observe

if (it.disabled) {
viewBinding.errorTV.isVisible = true
return@observe
binding.commentsRV.adapter = commentPagingAdapter

viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
commentPagingAdapter.loadStateFlow.collect {
binding.progress.isVisible = it.refresh is LoadState.Loading

if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached) {
binding.errorTV.text = getString(R.string.no_comments_available)
binding.errorTV.isVisible = true
return@collect
}
}
}

launch {
viewModel.commentsFlow.collect {
commentPagingAdapter.submitData(it)

val commentCount = commentPagingAdapter.itemCount.toLong().formatShort()
commentsSheet?.updateFragmentInfo(
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)
)
}
}

Expand Down
Loading
Loading