feat(creator-channel): 커뮤니티 답글 화면을 구현한다

This commit is contained in:
2026-07-09 00:53:48 +09:00
parent d10373d265
commit 6125b9f7d1
2 changed files with 322 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.text.InputType
import android.text.method.ScrollingMovementMethod
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityReplyBinding
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.ui.CreatorChannelCommunityReplyAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
import org.koin.androidx.viewmodel.ext.android.viewModel
class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannelCommunityReplyBinding>(
ActivityCreatorChannelCommunityReplyBinding::inflate
) {
private val viewModel: CreatorChannelCommunityReplyViewModel by viewModel()
private val replyAdapter = CreatorChannelCommunityReplyAdapter(
onModifyClick = { reply -> showModifyReplyDialog(reply.replyId, reply.comment) },
onDeleteClick = { reply -> showDeleteReplyDialog(reply.replyId) }
)
private var postId: Long = 0L
override fun setupView() {
postId = intent.getLongExtra(EXTRA_POST_ID, 0L)
val parentComment = parentCommentFromIntent()
if (postId <= 0L || parentComment.commentId <= 0L) {
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
finish()
return
}
binding.btnCreatorChannelCommunityReplyBack.setOnClickListener { finish() }
binding.rvCreatorChannelCommunityReply.layoutManager = LinearLayoutManager(this)
binding.rvCreatorChannelCommunityReply.adapter = replyAdapter
binding.rvCreatorChannelCommunityReply.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0 && !recyclerView.canScrollVertically(1)) {
viewModel.loadMoreReplies()
}
}
}
)
binding.etCreatorChannelCommunityReply.movementMethod = ScrollingMovementMethod()
binding.etCreatorChannelCommunityReply.doAfterTextChanged { editable ->
viewModel.updateReplyInput(editable?.toString().orEmpty())
}
binding.btnCreatorChannelCommunityReplySend.setOnClickListener { viewModel.submitReply() }
viewModel.replyStateLiveData.observe(this) { state -> render(state) }
viewModel.replyChangedEventLiveData.observe(this) { changed ->
if (changed == true) {
setResult(Activity.RESULT_OK)
viewModel.consumeReplyChangedEvent()
}
}
viewModel.parentCommentChangedEventLiveData.observe(this) { changed ->
if (changed == true) {
setResult(Activity.RESULT_OK)
viewModel.consumeParentCommentChangedEvent()
}
}
viewModel.parentCommentDeletedEventLiveData.observe(this) { deleted ->
if (deleted == true) {
setResult(Activity.RESULT_OK)
viewModel.consumeParentCommentDeletedEvent()
finish()
}
}
viewModel.toastLiveData.observe(this) { event ->
event.consume()?.let { toast ->
val message = toast.message ?: toast.resId?.let(::getString)
message?.let(::showToast)
}
}
viewModel.loadReplies(postId, parentComment)
}
private fun render(state: CreatorChannelCommunityReplyUiState) {
when (state) {
is CreatorChannelCommunityReplyUiState.Content -> renderContent(state)
}
}
private fun renderContent(content: CreatorChannelCommunityReplyUiState.Content) = with(binding) {
CreatorChannelCommunityCommentAdapter.bindComment(
layoutCreatorChannelCommunityReplyParent,
content.parentComment,
onReplyClick = {},
onModifyClick = { comment -> showModifyParentCommentDialog(comment.comment) },
onDeleteClick = { showDeleteParentCommentDialog() }
)
layoutCreatorChannelCommunityReplyParent.layoutCreatorChannelCommunityCommentLatestReply.isVisible = false
replyAdapter.submitItems(content.replies)
if (etCreatorChannelCommunityReply.text.toString() != content.replyInput) {
etCreatorChannelCommunityReply.setText(content.replyInput)
etCreatorChannelCommunityReply.setSelection(content.replyInput.length)
}
updateSendButton(content.replyInput.trim().isNotEmpty() && !content.isSendingReply)
}
private fun showModifyReplyDialog(replyId: Long, comment: String) {
val editText = EditText(this).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
minLines = 3
setText(comment)
setSelection(comment.length)
}
AlertDialog.Builder(this)
.setTitle(R.string.menu_modify_action)
.setView(editText)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.confirm) { _, _ ->
val modifiedComment = editText.text?.toString().orEmpty()
if (modifiedComment.trim() == comment.trim()) {
Toast.makeText(this, R.string.audio_content_comment_no_change, Toast.LENGTH_SHORT).show()
} else {
viewModel.modifyReply(replyId, modifiedComment)
}
}
.show()
}
private fun showModifyParentCommentDialog(comment: String) {
val editText = EditText(this).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
minLines = 3
setText(comment)
setSelection(comment.length)
}
AlertDialog.Builder(this)
.setTitle(R.string.menu_modify_action)
.setView(editText)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.confirm) { _, _ ->
val modifiedComment = editText.text?.toString().orEmpty()
if (modifiedComment.trim() == comment.trim()) {
Toast.makeText(this, R.string.audio_content_comment_no_change, Toast.LENGTH_SHORT).show()
} else {
viewModel.modifyParentComment(modifiedComment)
}
}
.show()
}
private fun showDeleteParentCommentDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.confirm_delete_title)
.setMessage(R.string.confirm_delete_message)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.confirm) { _, _ -> viewModel.deleteParentComment() }
.show()
}
private fun showDeleteReplyDialog(replyId: Long) {
AlertDialog.Builder(this)
.setTitle(R.string.confirm_delete_title)
.setMessage(R.string.confirm_delete_message)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.confirm) { _, _ -> viewModel.deleteReply(replyId) }
.show()
}
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityReplySend) {
isEnabled = enabled
setBackgroundResource(
if (enabled) {
R.drawable.bg_creator_channel_community_send_enabled
} else {
R.drawable.bg_creator_channel_community_send_disabled
}
)
setImageResource(if (enabled) R.drawable.ic_new_arrow_up_white else R.drawable.ic_new_arrow_up_gray)
}
private fun parentCommentFromIntent(): CreatorChannelCommunityCommentUiModel {
return CreatorChannelCommunityCommentUiModel(
commentId = intent.getLongExtra(EXTRA_COMMENT_ID, 0L),
memberId = intent.getLongExtra(EXTRA_MEMBER_ID, 0L),
memberNickname = intent.getStringExtra(EXTRA_MEMBER_NICKNAME).orEmpty(),
memberProfileUrl = intent.getStringExtra(EXTRA_MEMBER_PROFILE_URL),
createdAtUtc = intent.getStringExtra(EXTRA_CREATED_AT_UTC).orEmpty(),
createdAtText = intent.getStringExtra(EXTRA_CREATED_AT_TEXT).orEmpty(),
comment = intent.getStringExtra(EXTRA_COMMENT).orEmpty(),
likeCount = intent.getIntExtra(EXTRA_LIKE_COUNT, 0),
replyCount = intent.getIntExtra(EXTRA_REPLY_COUNT, 0),
isLiked = intent.getBooleanExtra(EXTRA_IS_LIKED, false),
isMine = intent.getBooleanExtra(EXTRA_IS_MINE, false),
isCreatorOwner = intent.getBooleanExtra(EXTRA_IS_CREATOR_OWNER, false),
latestReply = null
)
}
companion object {
private const val EXTRA_POST_ID = "extra_post_id"
private const val EXTRA_COMMENT_ID = "extra_comment_id"
private const val EXTRA_MEMBER_ID = "extra_member_id"
private const val EXTRA_MEMBER_NICKNAME = "extra_member_nickname"
private const val EXTRA_MEMBER_PROFILE_URL = "extra_member_profile_url"
private const val EXTRA_CREATED_AT_UTC = "extra_created_at_utc"
private const val EXTRA_CREATED_AT_TEXT = "extra_created_at_text"
private const val EXTRA_COMMENT = "extra_comment"
private const val EXTRA_LIKE_COUNT = "extra_like_count"
private const val EXTRA_REPLY_COUNT = "extra_reply_count"
private const val EXTRA_IS_LIKED = "extra_is_liked"
private const val EXTRA_IS_MINE = "extra_is_mine"
private const val EXTRA_IS_CREATOR_OWNER = "extra_is_creator_owner"
fun newIntent(context: Context, postId: Long, parentComment: CreatorChannelCommunityCommentUiModel): Intent {
return Intent(context, CreatorChannelCommunityReplyActivity::class.java)
.putExtra(EXTRA_POST_ID, postId)
.putExtra(EXTRA_COMMENT_ID, parentComment.commentId)
.putExtra(EXTRA_MEMBER_ID, parentComment.memberId)
.putExtra(EXTRA_MEMBER_NICKNAME, parentComment.memberNickname)
.putExtra(EXTRA_MEMBER_PROFILE_URL, parentComment.memberProfileUrl)
.putExtra(EXTRA_CREATED_AT_UTC, parentComment.createdAtUtc)
.putExtra(EXTRA_CREATED_AT_TEXT, parentComment.createdAtText)
.putExtra(EXTRA_COMMENT, parentComment.comment)
.putExtra(EXTRA_LIKE_COUNT, parentComment.likeCount)
.putExtra(EXTRA_REPLY_COUNT, parentComment.replyCount)
.putExtra(EXTRA_IS_LIKED, parentComment.isLiked)
.putExtra(EXTRA_IS_MINE, parentComment.isMine)
.putExtra(EXTRA_IS_CREATOR_OWNER, parentComment.isCreatorOwner)
}
}
}

View File

@@ -0,0 +1,85 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityReplyBinding
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
class CreatorChannelCommunityReplyAdapter(
private val onModifyClick: (CreatorChannelCommunityReplyUiModel) -> Unit,
private val onDeleteClick: (CreatorChannelCommunityReplyUiModel) -> Unit
) : RecyclerView.Adapter<CreatorChannelCommunityReplyAdapter.ViewHolder>() {
private var items: List<CreatorChannelCommunityReplyUiModel> = emptyList()
fun submitItems(items: List<CreatorChannelCommunityReplyUiModel>) {
this.items = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemCreatorChannelCommunityReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false),
onModifyClick,
onDeleteClick
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
class ViewHolder(
private val binding: ItemCreatorChannelCommunityReplyBinding,
private val onModifyClick: (CreatorChannelCommunityReplyUiModel) -> Unit,
private val onDeleteClick: (CreatorChannelCommunityReplyUiModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CreatorChannelCommunityReplyUiModel) = with(binding) {
ivCreatorChannelCommunityReplyItemProfile.loadUrl(item.memberProfileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
tvCreatorChannelCommunityReplyItemNickname.text = item.memberNickname
tvCreatorChannelCommunityReplyItemTime.text = item.createdAtText
tvCreatorChannelCommunityReplyItemBody.text = item.comment
ivCreatorChannelCommunityReplyItemMore.isVisible = item.canShowMore
ivCreatorChannelCommunityReplyItemMore.setOnClickListener { anchor ->
showOptionMenu(anchor, item, onModifyClick, onDeleteClick)
}
}
private fun showOptionMenu(
anchor: View,
item: CreatorChannelCommunityReplyUiModel,
onModifyClick: (CreatorChannelCommunityReplyUiModel) -> Unit,
onDeleteClick: (CreatorChannelCommunityReplyUiModel) -> Unit
) {
if (!item.canShowMore) return
val popup = PopupMenu(anchor.context, anchor)
val menuRes = if (item.isMine) R.menu.content_comment_option_menu else R.menu.content_comment_option_menu2
popup.menuInflater.inflate(menuRes, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.menu_review_modify -> onModifyClick(item)
R.id.menu_review_delete -> onDeleteClick(item)
}
true
}
popup.show()
}
private val CreatorChannelCommunityReplyUiModel.canShowMore: Boolean
get() = isMine || isCreatorOwner
}
}