diff --git a/app/src/main/java/com/github/libretube/api/obj/Comment.kt b/app/src/main/java/com/github/libretube/api/obj/Comment.kt index d21018e88..3b779b09f 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Comment.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Comment.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class Comment( val author: String, val commentId: String, - val commentText: String, + val commentText: String?, val commentedTime: String, val commentorUrl: String, val repliesPage: String? = null, diff --git a/app/src/main/java/com/github/libretube/constants/IntentData.kt b/app/src/main/java/com/github/libretube/constants/IntentData.kt index c10d30549..843d7c797 100644 --- a/app/src/main/java/com/github/libretube/constants/IntentData.kt +++ b/app/src/main/java/com/github/libretube/constants/IntentData.kt @@ -18,4 +18,5 @@ object IntentData { const val downloading = "downloading" const val openAudioPlayer = "openAudioPlayer" const val fragmentToOpen = "fragmentToOpen" + const val replyPage = "replyPage" } diff --git a/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt index aa72c25f0..72e9fa107 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt @@ -1,42 +1,33 @@ package com.github.libretube.ui.adapters import android.annotation.SuppressLint -import android.util.Log +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button import android.widget.Toast import androidx.core.text.parseAsHtml -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.github.libretube.R -import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.obj.Comment -import com.github.libretube.api.obj.CommentsPage +import com.github.libretube.constants.IntentData import com.github.libretube.databinding.CommentsRowBinding -import com.github.libretube.extensions.TAG import com.github.libretube.extensions.formatShort +import com.github.libretube.ui.fragments.CommentsRepliesFragment import com.github.libretube.ui.viewholders.CommentsViewHolder import com.github.libretube.util.ClipboardHelper import com.github.libretube.util.ImageHelper import com.github.libretube.util.NavigationHelper import com.github.libretube.util.TextUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class CommentsAdapter( + private val fragment: Fragment?, private val videoId: String, private val comments: MutableList, private val isRepliesAdapter: Boolean = false, private val dismiss: () -> Unit ) : RecyclerView.Adapter() { - - private var isLoading = false - private lateinit var repliesPage: CommentsPage - fun clear() { val size: Int = comments.size comments.clear() @@ -59,15 +50,8 @@ class CommentsAdapter( override fun onBindViewHolder(holder: CommentsViewHolder, position: Int) { val comment = comments[position] holder.binding.apply { - if (isRepliesAdapter) { - root.scaleX = REPLIES_ADAPTER_SCALE - root.scaleY = REPLIES_ADAPTER_SCALE - commentorImage.scaleX = REPLIES_ADAPTER_SCALE - commentorImage.scaleY = REPLIES_ADAPTER_SCALE - } - commentInfos.text = comment.author + TextUtils.SEPARATOR + comment.commentedTime - commentText.text = comment.commentText.parseAsHtml() + commentText.text = comment.commentText?.parseAsHtml() ImageHelper.loadImage(comment.thumbnail, commentorImage) likesTextView.text = comment.likeCount.formatShort() @@ -85,79 +69,31 @@ class CommentsAdapter( dismiss.invoke() } - repliesRecView.layoutManager = LinearLayoutManager(root.context) - val repliesAdapter = CommentsAdapter(videoId, mutableListOf(), true, dismiss) - repliesRecView.adapter = repliesAdapter if (!isRepliesAdapter && comment.repliesPage != null) { + val repliesFragment = CommentsRepliesFragment().apply { + arguments = Bundle().apply { + putString(IntentData.videoId, videoId) + putString(IntentData.replyPage, comment.repliesPage) + } + } root.setOnClickListener { - showMoreReplies(comment.repliesPage, showMore, repliesAdapter) + fragment!!.parentFragmentManager + .beginTransaction() + .replace(R.id.commentFragContainer, repliesFragment) + .addToBackStack(null) + .commit() } } root.setOnLongClickListener { - ClipboardHelper(root.context).save(comment.commentText) + ClipboardHelper(root.context).save(comment.commentText ?: "") Toast.makeText(root.context, R.string.copied, Toast.LENGTH_SHORT).show() true } } } - private fun showMoreReplies( - nextPage: String, - showMoreBtn: Button, - repliesAdapter: CommentsAdapter - ) { - when (repliesAdapter.itemCount) { - 0 -> { - fetchReplies(nextPage) { - repliesAdapter.updateItems(it.comments) - if (repliesPage.nextpage == null) { - showMoreBtn.visibility = View.GONE - return@fetchReplies - } - showMoreBtn.visibility = View.VISIBLE - showMoreBtn.setOnClickListener { view -> - if (repliesPage.nextpage == null) { - view.visibility = View.GONE - return@setOnClickListener - } - fetchReplies( - repliesPage.nextpage!! - ) { - repliesAdapter.updateItems(repliesPage.comments) - } - } - } - } - else -> { - repliesAdapter.clear() - showMoreBtn.visibility = View.GONE - } - } - } - override fun getItemCount(): Int { return comments.size } - - private fun fetchReplies(nextPage: String, onFinished: (CommentsPage) -> Unit) { - CoroutineScope(Dispatchers.IO).launch { - if (isLoading) return@launch - isLoading = true - repliesPage = try { - RetrofitInstance.api.getCommentsNextPage(videoId, nextPage) - } catch (e: Exception) { - Log.e(TAG(), "IOException, you might not have internet connection") - return@launch - } - withContext(Dispatchers.Main) { - onFinished.invoke(repliesPage) - } - isLoading = false - } - } - - companion object { - private const val REPLIES_ADAPTER_SCALE = 0.9f - } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/CommentsMainFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/CommentsMainFragment.kt new file mode 100644 index 000000000..bf8c5020d --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/fragments/CommentsMainFragment.kt @@ -0,0 +1,76 @@ +package com.github.libretube.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.libretube.R +import com.github.libretube.databinding.FragmentCommentsBinding +import com.github.libretube.ui.adapters.CommentsAdapter +import com.github.libretube.ui.models.CommentsViewModel + +class CommentsMainFragment : Fragment() { + private lateinit var binding: FragmentCommentsBinding + private lateinit var commentsAdapter: CommentsAdapter + + private val viewModel: CommentsViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentCommentsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.commentsRV.layoutManager = LinearLayoutManager(requireContext()) + binding.commentsRV.setItemViewCacheSize(20) + + binding.commentsRV.viewTreeObserver + .addOnScrollChangedListener { + if (!binding.commentsRV.canScrollVertically(1)) { + viewModel.fetchNextComments() + } + } + + commentsAdapter = CommentsAdapter( + this, + viewModel.videoId!!, + viewModel.commentsPage.value?.comments.orEmpty().toMutableList() + ) { + viewModel.commentsSheetDismiss?.invoke() + } + binding.commentsRV.adapter = commentsAdapter + + if (viewModel.commentsPage.value?.comments.orEmpty().isEmpty()) { + binding.progress.visibility = View.VISIBLE + viewModel.fetchComments() + } + + // listen for new comments to be loaded + viewModel.commentsPage.observe(viewLifecycleOwner) { + it ?: return@observe + binding.progress.visibility = View.GONE + if (it.disabled == true) { + binding.errorTV.visibility = View.VISIBLE + return@observe + } + if (it.comments.isEmpty()) { + binding.errorTV.text = getString(R.string.no_comments_available) + binding.errorTV.visibility = View.VISIBLE + return@observe + } + commentsAdapter.updateItems( + // only add the new comments to the recycler view + it.comments.subList(commentsAdapter.itemCount, it.comments.size) + ) + } + } +} diff --git a/app/src/main/java/com/github/libretube/ui/fragments/CommentsRepliesFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/CommentsRepliesFragment.kt new file mode 100644 index 000000000..33f61757b --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/fragments/CommentsRepliesFragment.kt @@ -0,0 +1,106 @@ +package com.github.libretube.ui.fragments + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.libretube.api.RetrofitInstance +import com.github.libretube.api.obj.CommentsPage +import com.github.libretube.constants.IntentData +import com.github.libretube.databinding.FragmentCommentsBinding +import com.github.libretube.extensions.TAG +import com.github.libretube.ui.adapters.CommentsAdapter +import com.github.libretube.ui.models.CommentsViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class CommentsRepliesFragment : Fragment() { + private lateinit var binding: FragmentCommentsBinding + private lateinit var repliesPage: CommentsPage + private lateinit var repliesAdapter: CommentsAdapter + private val viewModel: CommentsViewModel by activityViewModels() + + private var isLoading = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentCommentsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val videoId = arguments?.getString(IntentData.videoId) ?: "" + val nextPage = arguments?.getString(IntentData.replyPage) ?: "" + + repliesAdapter = CommentsAdapter(null, videoId, mutableListOf(), true) { + viewModel.commentsSheetDismiss?.invoke() + } + + binding.commentsRV.layoutManager = LinearLayoutManager(view.context) + binding.commentsRV.adapter = repliesAdapter + + binding.commentsRV.viewTreeObserver + .addOnScrollChangedListener { + if (!binding.commentsRV.canScrollVertically(1) && + ::repliesPage.isInitialized && + repliesPage.nextpage != null + ) { + fetchReplies(videoId, repliesPage.nextpage!!) { + repliesAdapter.updateItems(repliesPage.comments) + } + } + } + + loadInitialReplies(videoId, nextPage, repliesAdapter) + } + + private fun loadInitialReplies( + videoId: String, + nextPage: String, + repliesAdapter: CommentsAdapter + ) { + when (repliesAdapter.itemCount) { + 0 -> { + binding.progress.visibility = View.VISIBLE + fetchReplies(videoId, nextPage) { + repliesAdapter.updateItems(it.comments) + binding.progress.visibility = View.GONE + } + } + else -> { + repliesAdapter.clear() + } + } + } + + private fun fetchReplies( + videoId: String, + nextPage: String, + onFinished: (CommentsPage) -> Unit + ) { + CoroutineScope(Dispatchers.IO).launch { + if (isLoading) return@launch + isLoading = true + repliesPage = try { + RetrofitInstance.api.getCommentsNextPage(videoId, nextPage) + } catch (e: Exception) { + Log.e(TAG(), "IOException, you might not have internet connection") + return@launch + } + withContext(Dispatchers.Main) { + onFinished.invoke(repliesPage) + } + isLoading = false + } + } +} diff --git a/app/src/main/java/com/github/libretube/ui/models/CommentsViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/CommentsViewModel.kt index 016b00ff0..4d142fc79 100644 --- a/app/src/main/java/com/github/libretube/ui/models/CommentsViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/CommentsViewModel.kt @@ -20,6 +20,7 @@ class CommentsViewModel : ViewModel() { var videoId: String? = null var maxHeight: Int = 0 + var commentsSheetDismiss: (() -> Unit)? = null fun fetchComments() { videoId ?: return diff --git a/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt index d6f35d7bb..f3f5f1d4e 100644 --- a/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt +++ b/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt @@ -1,25 +1,24 @@ package com.github.libretube.ui.sheets +import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver -import androidx.core.view.updateLayoutParams +import android.view.WindowManager import androidx.fragment.app.activityViewModels -import androidx.recyclerview.widget.LinearLayoutManager import com.github.libretube.R import com.github.libretube.databinding.CommentsSheetBinding -import com.github.libretube.extensions.dpToPx -import com.github.libretube.ui.adapters.CommentsAdapter +import com.github.libretube.ui.fragments.CommentsMainFragment +import com.github.libretube.ui.fragments.CommentsRepliesFragment import com.github.libretube.ui.models.CommentsViewModel class CommentsSheet : ExpandedBottomSheet() { private lateinit var binding: CommentsSheetBinding - - private lateinit var commentsAdapter: CommentsAdapter - - private val viewModel: CommentsViewModel by activityViewModels() + private val commentsViewModel: CommentsViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, @@ -33,56 +32,83 @@ class CommentsSheet : ExpandedBottomSheet() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.dragHandle.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - binding.dragHandle.viewTreeObserver.removeOnGlobalLayoutListener(this) - // limit the recyclerview height to not cover the video - binding.commentsRV.updateLayoutParams { - height = viewModel.maxHeight - (binding.dragHandle.height + 20.dpToPx().toInt()) + commentsViewModel.commentsSheetDismiss = this::dismiss + + binding.apply { + dragHandle.viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + dragHandle.viewTreeObserver.removeOnGlobalLayoutListener(this) + + // limit the recyclerview height to not cover the video + binding.standardBottomSheet.layoutParams = + binding.commentFragContainer.layoutParams.apply { + height = commentsViewModel.maxHeight + } } - } - }) + }) - binding.commentsRV.layoutManager = LinearLayoutManager(requireContext()) - binding.commentsRV.setItemViewCacheSize(20) - - binding.commentsRV.viewTreeObserver - .addOnScrollChangedListener { - if (!binding.commentsRV.canScrollVertically(1)) { - viewModel.fetchNextComments() + btnBack.setOnClickListener { + if (childFragmentManager.backStackEntryCount > 0) { + childFragmentManager.popBackStack() } } - commentsAdapter = CommentsAdapter( - viewModel.videoId!!, - viewModel.commentsPage.value?.comments.orEmpty().toMutableList() - ) { - dialog?.dismiss() - } - binding.commentsRV.adapter = commentsAdapter - - if (viewModel.commentsPage.value?.comments.orEmpty().isEmpty()) { - binding.progress.visibility = View.VISIBLE - viewModel.fetchComments() + btnClose.setOnClickListener { dismiss() } } - // listen for new comments to be loaded - viewModel.commentsPage.observe(viewLifecycleOwner) { - it ?: return@observe - binding.progress.visibility = View.GONE - if (it.disabled == true) { - binding.errorTV.visibility = View.VISIBLE - return@observe - } - if (it.comments.isEmpty()) { - binding.errorTV.text = getString(R.string.no_comments_available) - binding.errorTV.visibility = View.VISIBLE - return@observe - } - commentsAdapter.updateItems( - // only add the new comments to the recycler view - it.comments.subList(commentsAdapter.itemCount, it.comments.size) - ) + childFragmentManager.apply { + addOnBackStackChangedListener(this@CommentsSheet::onFragmentChanged) + + beginTransaction() + .replace(R.id.commentFragContainer, CommentsMainFragment()) + .runOnCommit(this@CommentsSheet::onFragmentChanged) + .commit() } } + + private fun onFragmentChanged() { + childFragmentManager.findFragmentById(R.id.commentFragContainer)?.let { + when (it) { + is CommentsRepliesFragment -> { + binding.btnBack.visibility = View.VISIBLE + binding.commentsTitle.text = getString(R.string.replies) + } + else -> { + binding.btnBack.visibility = View.GONE + binding.commentsTitle.text = getString(R.string.comments) + } + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + commentsViewModel.commentsSheetDismiss = null + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + + dialog.apply { + setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (childFragmentManager.backStackEntryCount > 0) { + childFragmentManager.popBackStack() + return@setOnKeyListener true + } + } + return@setOnKeyListener false + } + + window?.let { + it.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL) + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + } + + setCanceledOnTouchOutside(false) + } + + return dialog + } } diff --git a/app/src/main/res/layout/comments_row.xml b/app/src/main/res/layout/comments_row.xml index b48377435..abad110c8 100644 --- a/app/src/main/res/layout/comments_row.xml +++ b/app/src/main/res/layout/comments_row.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/rounded_ripple"> + android:background="?selectableItemBackground"> @@ -47,8 +48,8 @@ android:layout_height="wrap_content" android:ellipsize="end" android:maxLines="2" - android:textSize="15sp" - android:textStyle="bold" + android:textSize="14sp" + android:textColor="@color/text_color_secondary" tools:text="Author and Time" /> @@ -120,7 +123,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="6dp" - tools:text="LikeCount" /> + tools:text="ReplyCount" /> @@ -128,26 +131,5 @@ - - - - diff --git a/app/src/main/res/layout/comments_sheet.xml b/app/src/main/res/layout/comments_sheet.xml index 6beb68f0c..ecf9a0357 100644 --- a/app/src/main/res/layout/comments_sheet.xml +++ b/app/src/main/res/layout/comments_sheet.xml @@ -1,6 +1,7 @@ @@ -9,7 +10,6 @@ style="@style/Widget.Material3.BottomSheet" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingBottom="20dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + + + + + + + + + + + - - - - - - - - + android:layout_height="0dp" + android:layout_weight="1" /> diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml new file mode 100644 index 000000000..4b841a7f3 --- /dev/null +++ b/app/src/main/res/layout/fragment_comments.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..f4247d820 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + #BFBFBF + #3A3A3A + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4886b7498..e23e7aa9c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,6 +3,8 @@ #AA000000 #EEFFFFFF #0061A6 + #505050 + #CCCCCC #0058CB #FFFFFF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 78c142d15..53d5cef34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,6 +72,7 @@ Connect to the Internet first. Retry Comments + Replies Choose search filter Channels All