From 0c2e2db1dcd9c18eec639dbc652b65536fbf9fea Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Mon, 19 Feb 2024 01:28:11 -0600 Subject: [PATCH] feat: basic replying --- app/build.gradle | 2 - .../connections/comments/CommentsAPI.kt | 37 +++- .../dantotsu/media/comments/CommentItem.kt | 45 ++++- .../media/comments/CommentsActivity.kt | 188 +++++++++++++----- app/src/main/res/layout/activity_comments.xml | 61 +++--- app/src/main/res/layout/item_comments.xml | 27 ++- 6 files changed, 245 insertions(+), 115 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4c7abcbe..198e5cb7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,8 +25,6 @@ android { versionName "2.2.0" versionCode 220000000 signingConfig signingConfigs.debug - buildConfigField("String", "APP_SECRET", apikeyProperties['APP_SECRET']) - buildConfigField("String", "USER_ID_ENCRYPT_KEY", apikeyProperties['USER_ID_ENCRYPT_KEY']) } flavorDimensions "store" diff --git a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt index 34dd94cc..c589ce06 100644 --- a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt +++ b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt @@ -46,6 +46,24 @@ object CommentsAPI { return parsed } + suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? { + val url = "$address/comments/parent/$id/$page" + val request = requestBuilder() + val json = request.get(url) + if (!json.text.startsWith("{")) return null + val res = json.code == 200 + if (!res && json.code != 404) { + errorReason(json.code, json.text) + } + val parsed = try { + Json.decodeFromString(json.text) + } catch (e: Exception) { + println("comments: $e") + return null + } + return parsed + } + suspend fun vote(commentId: Int, voteType: Int): Boolean { val url = "$address/comments/vote/$commentId/$voteType" val request = requestBuilder() @@ -155,16 +173,13 @@ object CommentsAPI { } private fun headerBuilder(): Map { - return if (authToken != null) { - mapOf( - "appauth" to BuildConfig.APP_SECRET, - "Authorization" to authToken!! - ) - } else { - mapOf( - "appauth" to BuildConfig.APP_SECRET, - ) + val map = mutableMapOf( + "appauth" to "6*45Qp%W2RS@t38jkXoSKY588Ynj%n" + ) + if (authToken != null) { + map["Authorization"] = authToken!! } + return map } private fun requestBuilder(): Requests { @@ -250,7 +265,9 @@ data class Comment( val isMod: Boolean? = null, @SerialName("is_admin") @Serializable(with = NumericBooleanSerializer::class) - val isAdmin: Boolean? = null + val isAdmin: Boolean? = null, + @SerialName("reply_count") + val replyCount: Int? = null ) @Serializable diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt index d5641f48..3b4c467a 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt @@ -2,6 +2,7 @@ package ani.dantotsu.media.comments import android.annotation.SuppressLint import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.R import ani.dantotsu.connections.comments.Comment import ani.dantotsu.connections.comments.CommentsAPI @@ -25,11 +26,23 @@ import java.util.TimeZone class CommentItem(val comment: Comment, private val markwon: Markwon, private val section: Section, - private val editCallback: (CommentItem) -> Unit + private val editCallback: (CommentItem) -> Unit, + private val viewReplyCallback: (CommentItem) -> Unit, + private val replyCallback: (CommentItem) -> Unit ) : BindableItem() { + var binding: ItemCommentsBinding? = null + val adapter = GroupieAdapter() + val repliesSection = Section() + + init { + adapter.add(repliesSection) + } @SuppressLint("SetTextI18n") override fun bind(viewBinding: ItemCommentsBinding, position: Int) { + binding = viewBinding + viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(currActivity()) + viewBinding.commentRepliesList.adapter = adapter val isUserComment = CommentsAPI.userId == comment.userId val node = markwon.parse(comment.content) val spanned = markwon.render(node) @@ -37,16 +50,25 @@ class CommentItem(val comment: Comment, 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.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE - viewBinding.commentReply.visibility = View.GONE //TODO: implement reply - viewBinding.commentTotalReplies.visibility = View.GONE //TODO: implement reply - viewBinding.commentReply.setOnClickListener { - + if ((comment.replyCount ?: 0) > 0) { + viewBinding.commentTotalReplies.visibility = View.VISIBLE + viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}" + } else { + viewBinding.commentTotalReplies.visibility = View.GONE + } + viewBinding.commentReply.visibility = View.VISIBLE + viewBinding.commentTotalReplies.setOnClickListener { + viewBinding.commentTotalReplies.visibility = View.GONE + viewReplyCallback(this) } var isEditing = false + var isReplying = false viewBinding.commentEdit.setOnClickListener { if (!isEditing) { viewBinding.commentEdit.text = currActivity()!!.getString(R.string.cancel) isEditing = true + isReplying = false + viewBinding.commentReply.text = "Reply" editCallback(this) } else { viewBinding.commentEdit.text = currActivity()!!.getString(R.string.edit) @@ -55,6 +77,19 @@ class CommentItem(val comment: Comment, } } + viewBinding.commentReply.setOnClickListener { + if (!isReplying) { + viewBinding.commentReply.text = currActivity()!!.getString(R.string.cancel) + isReplying = true + isEditing = false + viewBinding.commentEdit.text = currActivity()!!.getString(R.string.edit) + replyCallback(this) + } else { + viewBinding.commentReply.text = "Reply" + isReplying = false + 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 { diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentsActivity.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentsActivity.kt index 2b8c2476..bd5a364d 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentsActivity.kt @@ -76,7 +76,11 @@ class CommentsActivity : AppCompatActivity() { binding.commentReplyToContainer.visibility = View.GONE //TODO: implement reply var editing = false var editingCommentId = -1 + var replying = false + var replyingComment: CommentItem? = null fun editCallback(comment: CommentItem) { + replying = false + replyingComment = null if (editingCommentId == comment.comment.commentId) { editing = false editingCommentId = -1 @@ -95,6 +99,42 @@ class CommentsActivity : AppCompatActivity() { } + fun replyCallback(comment: CommentItem) { + editing = false + editingCommentId = -1 + if (replyingComment?.comment?.commentId == comment.comment.commentId) { + replying = false + replyingComment = null + binding.commentInput.setText("") + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(binding.commentInput.windowToken, 0) + } else { + replying = true + replyingComment = comment + 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) + } + } + + fun viewReplyCallback(comment: CommentItem) { + lifecycleScope.launch { + val replies = withContext(Dispatchers.IO) { + CommentsAPI.getRepliesFromId(comment.comment.commentId) + } + replies?.comments?.forEach { + comment.repliesSection.add(CommentItem(it, buildMarkwon(), comment.repliesSection, { comment -> + editCallback(comment) + }, { comment -> + viewReplyCallback(comment) + }, { comment -> + replyCallback(comment) + })) + } + } + } + binding.commentsRefresh.setOnRefreshListener { lifecycleScope.launch { binding.commentsList.visibility = View.GONE @@ -123,9 +163,13 @@ class CommentsActivity : AppCompatActivity() { } sorted?.forEach { withContext(Dispatchers.Main) { - section.add(CommentItem(it, buildMarkwon(), section) { comment -> + section.add(CommentItem(it, buildMarkwon(), section, { comment -> editCallback(comment) - }) + }, { comment -> + viewReplyCallback(comment) + }, { comment -> + replyCallback(comment) + })) } } } @@ -165,9 +209,13 @@ class CommentsActivity : AppCompatActivity() { } sorted?.forEach { withContext(Dispatchers.Main) { - section.add(CommentItem(it, buildMarkwon(), section) { comment -> + section.add(CommentItem(it, buildMarkwon(), section, { comment -> editCallback(comment) - }) + }, { comment -> + viewReplyCallback(comment) + }, { comment -> + replyCallback(comment) + })) } } totalPages = comments?.totalPages ?: 1 @@ -254,10 +302,14 @@ class CommentsActivity : AppCompatActivity() { CommentItem( it, buildMarkwon(), - section - ) { comment -> - editCallback(comment) - }) + section, { comment -> + editCallback(comment) + }, { comment -> + viewReplyCallback(comment) + }, { comment -> + replyCallback(comment) + }) + ) } } totalPages = comments?.totalPages ?: 1 @@ -290,6 +342,76 @@ class CommentsActivity : AppCompatActivity() { snackString("You are banned from commenting :(") return@setOnClickListener } + fun onSuccess() { + 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 + val dateFormat = + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + dateFormat.timeZone = + TimeZone.getTimeZone("UTC") + item.comment.timestamp = + dateFormat.format(System.currentTimeMillis()) + item.notifyChanged() + snackString("Comment edited") + } + } + + } + } + } else { + val success = withContext(Dispatchers.IO) { + CommentsAPI.comment(mediaId, if (replying) replyingComment!!.comment.commentId else null, it) + } + if (success != null) + if (replying) { + replyingComment?.repliesSection?.add( + 0, + CommentItem( + success, + buildMarkwon(), + replyingComment?.repliesSection!!, { comment -> + editCallback(comment) + }, { comment -> + viewReplyCallback(comment) + }, { comment -> + replyCallback(comment) + }) + ) + } else { + section.add( + 0, + CommentItem( + success, + buildMarkwon(), + section, { comment -> + editCallback(comment) + }, { comment -> + viewReplyCallback(comment) + }, { comment -> + replyCallback(comment) + }) + ) + } + } + } + } else { + snackString("Comment cannot be empty") + } + } + } + val firstComment = PrefManager.getVal(PrefName.FirstComment) if (firstComment) { //show a dialog explaining the rules @@ -311,53 +433,7 @@ class CommentsActivity : AppCompatActivity() { .setPositiveButton("I Understand") { dialog, _ -> dialog.dismiss() PrefManager.setVal(PrefName.FirstComment, false) - 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 - val dateFormat = - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - dateFormat.timeZone = - TimeZone.getTimeZone("UTC") - item.comment.timestamp = - dateFormat.format(System.currentTimeMillis()) - 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") - } - } + onSuccess() } .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() @@ -365,6 +441,8 @@ class CommentsActivity : AppCompatActivity() { val dialog = alertDialog.show() dialog?.window?.setDimAmount(0.8f) + } else { + onSuccess() } } } diff --git a/app/src/main/res/layout/activity_comments.xml b/app/src/main/res/layout/activity_comments.xml index b02eee5f..04f2a927 100644 --- a/app/src/main/res/layout/activity_comments.xml +++ b/app/src/main/res/layout/activity_comments.xml @@ -1,29 +1,31 @@ + android:fitsSystemWindows="true"> + + - + + + android:background="@color/nav_bg" + android:orientation="vertical" + android:windowSoftInputMode="adjustResize"> + + @@ -135,19 +140,19 @@ android:id="@+id/commentInput" android:layout_width="0dp" android:layout_height="match_parent" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" android:layout_weight="1" - android:textSize="12sp" + android:autofillHints="The One Piece is real" + android:background="@drawable/card_outline" android:fontFamily="@font/poppins_semi_bold" android:hint="Add a comment..." android:inputType="textMultiLine" - android:padding="8dp" - android:background="@drawable/card_outline" - android:layout_marginEnd="8dp" - android:layout_marginStart="8dp" android:maxLength="300" android:maxLines="15" - tools:ignore="HardcodedText" - android:autofillHints="The One Piece is real" /> + android:padding="8dp" + android:textSize="12sp" + tools:ignore="HardcodedText" /> @@ -185,19 +186,6 @@ tools:ignore="ContentDescription" /> - - - - + + \ No newline at end of file