Skip to content
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
Expand Up @@ -4,111 +4,84 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.os.bundleOf
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import androidx.paging.PagingDataAdapter
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.helpers.ClipboardHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.adapters.callbacks.DiffUtilItemCallback
import com.github.libretube.ui.fragments.CommentsRepliesFragment
import com.github.libretube.ui.viewholders.CommentsViewHolder
import com.github.libretube.ui.viewholders.CommentViewHolder
import com.github.libretube.util.HtmlParser
import com.github.libretube.util.LinkHandler
import com.github.libretube.util.TextUtils

class CommentPagingAdapter(
private val fragment: Fragment?,
private val videoId: String,
class CommentsPagingAdapter(
private val isReplies: Boolean,
private val channelAvatar: String?,
private val isRepliesAdapter: Boolean = false,
private val handleLink: ((url: String) -> Unit)?,
private val dismiss: () -> Unit
) : PagingDataAdapter<Comment, CommentsViewHolder>(
private val handleLink: (url: String) -> Unit,
private val saveToClipboard: (Comment) -> Unit,
private val navigateToChannel: (Comment) -> Unit,
private val navigateToReplies: ((Comment, String?) -> Unit)? = null,
) : PagingDataAdapter<Comment, CommentViewHolder>(
DiffUtilItemCallback(
areItemsTheSame = { oldItem, newItem -> oldItem.commentId == newItem.commentId },
areContentsTheSame = { _, _ -> true },
)
) {
private var clickEventConsumedByLinkHandler = false

private fun navigateToReplies(comment: Comment) {
if (clickEventConsumedByLinkHandler) {
clickEventConsumedByLinkHandler = false
return
}
private var clickEventConsumedByLinkHandler = false

val args = bundleOf(
IntentData.videoId to videoId,
IntentData.comment to comment,
IntentData.channelAvatar to channelAvatar
)
fragment!!.parentFragmentManager.commit {
replace<CommentsRepliesFragment>(R.id.commentFragContainer, args = args)
addToBackStack(null)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CommentsRowBinding.inflate(layoutInflater, parent, false)
return CommentViewHolder(binding)
}

override fun onBindViewHolder(holder: CommentsViewHolder, position: Int) {
val comment = getItem(position)!!

override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
holder.binding.apply {
val comment = getItem(position)!!
commentAuthor.text = comment.author
commentAuthor.setBackgroundResource(
if (comment.channelOwner) R.drawable.comment_channel_owner_bg else 0
)
comment.commentedTimeMillis?.let {
commentInfos.text = TextUtils.formatRelativeDate(it)
} ?: comment.commentedTime
commentInfos.text = comment.commentedTime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo that line, perhaps you accidentally changed it on rebase


commentText.movementMethod = LinkMovementMethodCompat.getInstance()
val linkHandler = LinkHandler {
clickEventConsumedByLinkHandler = true
handleLink?.invoke(it)
handleLink.invoke(it)
}
commentText.text = comment.commentText?.replace("</a>", "</a> ")
?.parseAsHtml(tagHandler = HtmlParser(linkHandler))

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

// clear the image view, to avoid it still being shown when android recycles the view
creatorReplyImageView.isGone = true
if (comment.creatorReplied && !channelAvatar.isNullOrBlank()) {
ImageHelper.loadImage(channelAvatar, creatorReplyImageView, true)
creatorReplyImageView.isVisible = true
} else {
creatorReplyImageView.setImageDrawable(null)
creatorReplyImageView.isVisible = false
}

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()
}
repliesCount.isVisible = !isReplies && comment.repliesPage != null
repliesCount.text = if (comment.replyCount > 0) comment.replyCount.formatShort() else null

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

if (isRepliesAdapter) {
repliesCount.isGone = true

if (isReplies) {
// highlight the comment that is being replied to
if (position == 0) {
root.setBackgroundColor(
Expand All @@ -126,27 +99,22 @@ class CommentPagingAdapter(
R.drawable.rounded_ripple
)
}
}

if (!isRepliesAdapter && comment.repliesPage != null) {
val onClickListener = View.OnClickListener { navigateToReplies(comment) }
} else {
val onClickListener = View.OnClickListener {
if (clickEventConsumedByLinkHandler) {
clickEventConsumedByLinkHandler = false
return@OnClickListener
}
navigateToReplies?.invoke(comment, channelAvatar)
}
root.setOnClickListener(onClickListener)
commentText.setOnClickListener(onClickListener)
}

root.setOnLongClickListener {
ClipboardHelper.save(
root.context,
text = comment.commentText.orEmpty().parseAsHtml().toString(),
notify = true
)
saveToClipboard(comment)
true
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CommentsRowBinding.inflate(layoutInflater, parent, false)
return CommentsViewHolder(binding)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,39 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import androidx.fragment.app.setFragmentResult
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.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.FragmentCommentsBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.ui.adapters.CommentPagingAdapter
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.adapters.CommentsPagingAdapter
import com.github.libretube.ui.models.CommentsViewModel
import com.github.libretube.ui.sheets.CommentsSheet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class CommentsMainFragment : Fragment(R.layout.fragment_comments) {
private var _binding: FragmentCommentsBinding? = null
private val binding get() = _binding!!

private val viewModel: CommentsViewModel by activityViewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentCommentsBinding.bind(view)
super.onViewCreated(view, savedInstanceState)

val binding = FragmentCommentsBinding.bind(view)
val layoutManager = LinearLayoutManager(requireContext())
binding.commentsRV.layoutManager = layoutManager
binding.commentsRV.setItemViewCacheSize(20)

val commentsSheet = parentFragment as? CommentsSheet
commentsSheet?.binding?.btnScrollToTop?.setOnClickListener {
// scroll back to the top / first comment
_binding?.commentsRV?.smoothScrollToPosition(POSITION_START)
layoutManager.startSmoothScroll(LinearSmoothScroller(view.context).also {
it.targetPosition = POSITION_START
})
viewModel.setCommentsPosition(POSITION_START)
}

Expand All @@ -49,63 +47,68 @@ class CommentsMainFragment : Fragment(R.layout.fragment_comments) {
if (newState != RecyclerView.SCROLL_STATE_IDLE) return

val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()

// hide or show the scroll to top button
commentsSheet?.binding?.btnScrollToTop?.isVisible = firstVisiblePosition != 0
viewModel.setCommentsPosition(firstVisiblePosition)

super.onScrollStateChanged(recyclerView, newState)
}
})

commentsSheet?.updateFragmentInfo(false, getString(R.string.comments))

val commentPagingAdapter = CommentPagingAdapter(
this,
viewModel.videoIdLiveData.value ?: return,
requireArguments().getString(IntentData.channelAvatar) ?: return,
val commentPagingAdapter = CommentsPagingAdapter(
false,
requireArguments().getString(IntentData.channelAvatar),
handleLink = {
setFragmentResult(
CommentsSheet.HANDLE_LINK_REQUEST_KEY,
bundleOf(IntentData.url to it)
bundleOf(IntentData.url to it),
)
}
) {
setFragmentResult(CommentsSheet.DISMISS_SHEET_REQUEST_KEY, bundleOf())
}
binding.commentsRV.adapter = commentPagingAdapter

var scrollPositionRestoreRequired = viewModel.currentCommentsPosition.value == 0
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
commentPagingAdapter.loadStateFlow.collect {
binding.progress.isVisible = it.refresh is LoadState.Loading

if (!scrollPositionRestoreRequired && it.refresh is LoadState.NotLoading) {
viewModel.currentCommentsPosition.value?.let { position ->
scrollPositionRestoreRequired = false

withContext(Dispatchers.Main) {
binding.commentsRV.scrollToPosition(position)
}
}
}

if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached && commentPagingAdapter.itemCount == 0) {
binding.errorTV.text = getString(R.string.no_comments_available)
binding.errorTV.isVisible = true
return@collect
}
},
saveToClipboard = { comment ->
viewModel.saveToClipboard(view.context, comment)
},
navigateToChannel = { comment ->
NavigationHelper.navigateChannel(view.context, comment.commentorUrl)
setFragmentResult(CommentsSheet.DISMISS_SHEET_REQUEST_KEY, Bundle.EMPTY)
},
navigateToReplies = { comment, channelAvatar ->
if (comment.repliesPage != null) {
val args = bundleOf(
IntentData.videoId to viewModel.videoIdLiveData.value,
IntentData.comment to comment,
IntentData.channelAvatar to channelAvatar
)
parentFragmentManager.commit {
viewModel.setLastOpenedCommentRepliesId(comment.commentId)
replace<CommentsRepliesFragment>(R.id.commentFragContainer, args = args)
addToBackStack(null)
}
}
},
)
binding.commentsRV.adapter = commentPagingAdapter

launch {
viewModel.commentsFlow.collect {
commentPagingAdapter.submitData(it)
}
commentPagingAdapter.addLoadStateListener { loadStates ->
binding.progress.isVisible = loadStates.refresh is LoadState.Loading

val refreshState = loadStates.source.refresh
if (refreshState is LoadState.NotLoading && commentPagingAdapter.itemCount > 0) {
viewModel.currentCommentsPosition.value?.let { position ->
binding.commentsRV.scrollToPosition(maxOf(position, POSITION_START))
}
}

if (loadStates.append is LoadState.NotLoading && loadStates.append.endOfPaginationReached && commentPagingAdapter.itemCount == 0) {
binding.errorTV.text = getString(R.string.no_comments_available)
binding.errorTV.isVisible = true
}
}

viewModel.currentCommentsPosition.observe(viewLifecycleOwner) {
// hide or show the scroll to top button
commentsSheet?.binding?.btnScrollToTop?.isVisible = it != 0
}

viewModel.commentsLiveData.observe(viewLifecycleOwner) {
commentPagingAdapter.submitData(lifecycle, it)
}

viewModel.commentCountLiveData.observe(viewLifecycleOwner) { commentCount ->
Expand All @@ -118,11 +121,6 @@ class CommentsMainFragment : Fragment(R.layout.fragment_comments) {
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

companion object {
private const val POSITION_START = 0
}
Expand Down
Loading
Loading