Dantotsu/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
TwistedUmbrellaX 37ec165319 chore: lint performance optimization
This includes shadowed variables, unnecessary parameters, layouts with string literals, items that cause performance bottlenecks, and the merge of extension types into only the necessary separate classes.
2024-03-22 14:18:15 -04:00

413 lines
No EOL
16 KiB
Kotlin

package ani.dantotsu.media.comments
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCommentsBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.BindableItem
import io.noties.markwon.Markwon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import kotlin.math.abs
import kotlin.math.sqrt
class CommentItem(val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) : BindableItem<ItemCommentsBinding>() {
lateinit var binding: ItemCommentsBinding
val adapter = GroupieAdapter()
private var subCommentIds: MutableList<Int> = mutableListOf()
val repliesSection = Section()
private var isEditing = false
var isReplying = false
private var repliesVisible = false
var MAX_DEPTH = 3
init {
adapter.add(repliesSection)
}
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
binding = viewBinding
setAnimation(binding.root.context, binding.root)
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity)
viewBinding.commentRepliesList.adapter = adapter
val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
markwon.setMarkdown(viewBinding.commentText, comment.content)
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
if (comment.tag == null) {
viewBinding.commentUserTagLayout.visibility = View.GONE
} else {
viewBinding.commentUserTagLayout.visibility = View.VISIBLE
viewBinding.commentUserTag.text = comment.tag.toString()
}
replying(isReplying) //sets default text
editing(isEditing)
if ((comment.replyCount ?: 0) > 0) {
viewBinding.commentTotalReplies.visibility = View.VISIBLE
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
viewBinding.commentTotalReplies.context.run {
viewBinding.commentTotalReplies.text = if (repliesVisible)
getString(R.string.hide_replies)
else
if (comment.replyCount == 1)
getString(R.string.view_reply)
else
getString(R.string.view_replies_count, comment.replyCount)
}
} else {
viewBinding.commentTotalReplies.visibility = View.GONE
viewBinding.commentRepliesDivider.visibility = View.GONE
}
viewBinding.commentReply.visibility = View.VISIBLE
viewBinding.commentTotalReplies.setOnClickListener {
if (repliesVisible) {
repliesSection.clear()
removeSubCommentIds()
viewBinding.commentTotalReplies.context.run {
viewBinding.commentTotalReplies.text = if (comment.replyCount == 1)
getString(R.string.view_reply)
else
getString(R.string.view_replies_count, comment.replyCount)
}
repliesVisible = false
} else {
viewBinding.commentTotalReplies.setText(R.string.hide_replies)
repliesSection.clear()
commentsFragment.viewReplyCallback(this)
repliesVisible = true
}
}
viewBinding.commentUserName.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentUserAvatar.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentText.setOnLongClickListener {
copyToClipboard(comment.content)
true
}
viewBinding.commentEdit.setOnClickListener {
editing(!isEditing)
commentsFragment.editCallback(this)
}
viewBinding.commentReply.setOnClickListener {
replying(!isReplying)
commentsFragment.replyTo(this, comment.username)
commentsFragment.replyCallback(this)
}
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
viewBinding.commentDelete.setOnClickListener {
dialogBuilder(getAppString(R.string.delete_comment), getAppString(R.string.delete_comment_confirm)) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString(R.string.comment_deleted)
parentSection.remove(this@CommentItem)
}
}
}
}
viewBinding.commentBanUser.setOnClickListener {
dialogBuilder(getAppString(R.string.ban_user), getAppString(R.string.ban_user_confirm)) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString(R.string.user_banned)
}
}
}
}
viewBinding.commentReport.setOnClickListener {
dialogBuilder(getAppString(R.string.report_comment), getAppString(R.string.report_comment_confirm)) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.reportComment(
comment.commentId,
comment.username,
commentsFragment.mediaName,
comment.userId
)
if (success) {
snackString(R.string.comment_reported)
}
}
}
}
//fill the icon if the user has liked the comment
setVoteButtons(viewBinding)
viewBinding.commentUpVote.setOnClickListener {
val voteType = if (comment.userVoteType == 1) 0 else 1
val previousVoteType = comment.userVoteType
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.vote(comment.commentId, voteType)
if (success) {
comment.userVoteType = voteType
if (previousVoteType == -1) {
comment.downvotes -= 1
}
comment.upvotes += if (voteType == 1) 1 else -1
notifyChanged()
}
}
}
viewBinding.commentDownVote.setOnClickListener {
val voteType = if (comment.userVoteType == -1) 0 else -1
val previousVoteType = comment.userVoteType
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.vote(comment.commentId, voteType)
if (success) {
comment.userVoteType = voteType
if (previousVoteType == 1) {
comment.upvotes -= 1
}
comment.downvotes += if (voteType == -1) 1 else -1
notifyChanged()
}
}
}
viewBinding.commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
viewBinding.commentUserAvatar.setOnLongClickListener {
ImageViewDialog.newInstance(
commentsFragment.activity,
"${comment.username}'s [Cover]",
comment.profilePictureUrl
)
}
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
viewBinding.commentUserName.text = comment.username
val userColor = "[${levelColor.second}]"
viewBinding.commentUserLevel.text = userColor
viewBinding.commentUserLevel.setTextColor(levelColor.first)
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
}
override fun getLayout(): Int {
return R.layout.item_comments
}
fun containsGif(): Boolean {
return comment.content.contains(".gif")
}
override fun initializeViewBinding(view: View): ItemCommentsBinding {
return ItemCommentsBinding.bind(view)
}
fun replying(isReplying: Boolean) {
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
this.isReplying = isReplying
}
fun editing(isEditing: Boolean) {
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit)
this.isEditing = isEditing
}
fun registerSubComment(id: Int) {
subCommentIds.add(id)
}
private fun removeSubCommentIds(){
subCommentIds.forEach { id ->
@Suppress("UNCHECKED_CAST")
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
val commentToRemove = parentComments.find { it.comment.commentId == id }
commentToRemove?.let {
it.removeSubCommentIds()
parentSection.remove(it)
}
}
subCommentIds.clear()
}
private fun setVoteButtons(viewBinding: ItemCommentsBinding) {
when (comment.userVoteType) {
1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentUpVote.alpha = 1f
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
-1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentDownVote.alpha = 1f
}
else -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
}
}
@SuppressLint("SimpleDateFormat")
private fun formatTimestamp(timestamp: String): String {
return try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
val currentDate = Date()
val diff = currentDate.time - (parsedDate?.time ?: 0)
val days = diff / (24 * 60 * 60 * 1000)
val hours = diff / (60 * 60 * 1000) % 24
val minutes = diff / (60 * 1000) % 60
return when {
days > 0 -> "${days}d"
hours > 0 -> "${hours}h"
minutes > 0 -> "${minutes}m"
else -> "now"
}
} catch (e: Exception) {
"now"
}
}
companion object {
@SuppressLint("SimpleDateFormat")
fun timestampToMillis(timestamp: String): Long {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
return parsedDate?.time ?: 0
}
}
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
var color = Color.parseColor(colorString)
val ratio = getContrastRatio(color, backgroundColor)
if (ratio < 4.5) {
color = adjustColorForContrast(color, backgroundColor)
}
return Pair(color, level)
}
/**
* Builds the dialog for yes/no confirmation
* no doesn't do anything, yes calls the callback
* @param title the title of the dialog
* @param message the message of the dialog
* @param callback the callback to call when the user clicks yes
*/
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { dialog, _ ->
callback()
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
private val usernameColors: Array<String> = arrayOf(
"#9932cc",
"#a020f0",
"#8b008b",
"#7b68ee",
"#da70d6",
"#dda0dd",
"#ffe4b5",
"#f0e68c",
"#ffb6c1",
"#fa8072",
"#b03060",
"#ff1493",
"#ff00ff",
"#ff69b4",
"#dc143c",
"#8b0000",
"#ff0000",
"#a0522d",
"#f4a460",
"#b8860b",
"#ffa500",
"#d2691e",
"#ff6347",
"#808000",
"#ffd700",
"#ffff54",
"#8fbc8f",
"#3cb371",
"#008000",
"#00fa9a",
"#98fb98",
"#00ff00",
"#adff2f",
"#32cd32",
"#556b2f",
"#9acd32",
"#7fffd4",
"#2f4f4f",
"#5f9ea0",
"#87ceeb",
"#00bfff",
"#00ffff",
"#1e90ff",
"#4682b4",
"#0000ff",
"#0000cd",
"#00008b",
"#191970",
"#ffffff",
)
}