feat: comments targeted at database

This commit is contained in:
rebelonion 2024-02-15 12:44:52 -06:00
parent 1694a1cb24
commit a73c4cd678
17 changed files with 544 additions and 228 deletions

View file

@ -0,0 +1,332 @@
package ani.dantotsu.media.comments
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.ActivityCommentsBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import io.noties.markwon.editor.MarkwonEditor
import io.noties.markwon.editor.MarkwonEditorTextWatcher
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CommentsActivity : AppCompatActivity() {
lateinit var binding: ActivityCommentsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
binding = ActivityCommentsBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
//get the media id from the intent
val mediaId = intent.getIntExtra("mediaId", -1)
if (mediaId == -1) {
snackString("Invalid Media ID")
finish()
}
val adapter = GroupieAdapter()
val section = Section()
val markwon = buildMarkwon()
binding.commentUserAvatar.loadImage(Anilist.avatar)
binding.commentTitle.text = getText(R.string.comments)
val markwonEditor = MarkwonEditor.create(markwon)
binding.commentInput.addTextChangedListener(
MarkwonEditorTextWatcher.withProcess(
markwonEditor
)
)
var editing = false
var editingCommentId = -1
fun editCallback(comment: CommentItem) {
if (editingCommentId == comment.comment.commentId) {
editing = false
editingCommentId = -1
binding.commentInput.setText("")
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(binding.commentInput.windowToken, 0)
} else {
editing = true
editingCommentId = comment.comment.commentId
binding.commentInput.setText(comment.comment.content)
binding.commentInput.requestFocus()
binding.commentInput.setSelection(binding.commentInput.text.length)
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.commentInput, InputMethodManager.SHOW_IMPLICIT)
}
}
binding.commentsRefresh.setOnRefreshListener {
lifecycleScope.launch {
binding.commentsList.visibility = View.GONE
adapter.clear()
section.clear()
withContext(Dispatchers.IO) {
val comments = CommentsAPI.getCommentsForId(mediaId)
comments?.comments?.forEach {
withContext(Dispatchers.Main) {
section.add(CommentItem(it, buildMarkwon(), section) { comment ->
editCallback(comment)
})
}
}
}
adapter.add(section)
binding.commentsList.visibility = View.VISIBLE
binding.commentsRefresh.isRefreshing = false
}
}
var pagesLoaded = 1
var totalPages = 1
binding.commentsList.adapter = adapter
binding.commentsList.layoutManager = LinearLayoutManager(this)
lifecycleScope.launch {
binding.commentsProgressBar.visibility = View.VISIBLE
binding.commentsList.visibility = View.GONE
withContext(Dispatchers.IO) {
val comments = CommentsAPI.getCommentsForId(mediaId)
comments?.comments?.forEach {
withContext(Dispatchers.Main) {
section.add(CommentItem(it, buildMarkwon(), section) { comment ->
editCallback(comment)
})
}
}
totalPages = comments?.totalPages ?: 1
}
binding.commentsProgressBar.visibility = View.GONE
binding.commentsList.visibility = View.VISIBLE
adapter.add(section)
}
binding.commentSort.setOnClickListener {
val popup = PopupMenu(this, it)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.comment_sort_newest -> {
val groups = section.groups
groups.sortByDescending { comment ->
comment as CommentItem
comment.timestampToMillis(comment.comment.timestamp)
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
R.id.comment_sort_oldest -> {
val groups = section.groups
groups.sortBy { comment ->
comment as CommentItem
comment.timestampToMillis(comment.comment.timestamp)
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
R.id.comment_sort_highest_rated -> {
val groups = section.groups
groups.sortByDescending { comment ->
comment as CommentItem
comment.comment.upvotes - comment.comment.downvotes
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
R.id.comment_sort_lowest_rated -> {
val groups = section.groups
groups.sortBy { comment ->
comment as CommentItem
comment.comment.upvotes - comment.comment.downvotes
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
}
true
}
popup.inflate(R.menu.comments_sort_menu)
popup.show()
}
var fetching = false
//if we have scrolled to the bottom of the list, load more comments
binding.commentsList.addOnScrollListener(object :
androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
override fun onScrolled(
recyclerView: androidx.recyclerview.widget.RecyclerView,
dx: Int,
dy: Int
) {
super.onScrolled(recyclerView, dx, dy)
if (!recyclerView.canScrollVertically(1)) {
if (pagesLoaded < totalPages && !fetching) {
fetching = true
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val comments =
CommentsAPI.getCommentsForId(mediaId, pagesLoaded + 1)
comments?.comments?.forEach {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
it,
buildMarkwon(),
section
) { comment ->
editCallback(comment)
})
}
}
totalPages = comments?.totalPages ?: 1
}
pagesLoaded++
fetching = false
}
}
}
}
})
binding.commentInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: android.text.Editable?) {
if (binding.commentInput.text.length > 300) {
binding.commentInput.text.delete(300, binding.commentInput.text.length)
snackString("Comment cannot be longer than 300 characters")
}
}
})
binding.commentSend.setOnClickListener {
if (CommentsAPI.isBanned) {
snackString("You are banned from commenting :(")
return@setOnClickListener
}
binding.commentInput.text.toString().let {
if (it.isNotEmpty()) {
binding.commentInput.text.clear()
lifecycleScope.launch {
if (editing) {
val success = withContext(Dispatchers.IO) {
CommentsAPI.editComment(editingCommentId, it)
}
if (success) {
val groups = section.groups
groups.forEach { item ->
if (item is CommentItem) {
if (item.comment.commentId == editingCommentId) {
item.comment.content = it
item.notifyChanged()
snackString("Comment edited")
}
}
}
}
} else {
val success = withContext(Dispatchers.IO) {
CommentsAPI.comment(mediaId, null, it)
}
if (success != null)
section.add(0, CommentItem(success, buildMarkwon(), section) { comment ->
editCallback(comment)
})
}
}
} else {
snackString("Comment cannot be empty")
}
}
}
}
private fun buildMarkwon(): Markwon {
val markwon = Markwon.builder(this)
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(HtmlPlugin.create())
.usePlugin(TablePlugin.create(this))
.usePlugin(TaskListPlugin.create(this))
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
private val requestManager: RequestManager =
Glide.with(this@CommentsActivity).apply {
addDefaultRequestListener(object : RequestListener<Any> {
override fun onResourceReady(
resource: Any,
model: Any,
target: Target<Any>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.start()
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Any>,
isFirstResource: Boolean
): Boolean {
return false
}
})
}
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
return requestManager.load(drawable.destination)
}
override fun cancel(target: Target<*>) {
requestManager.clear(target)
}
}))
.build()
return markwon
}
}