diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyActivity.kt new file mode 100644 index 00000000..1d4ac89d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyActivity.kt @@ -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::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) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/ui/CreatorChannelCommunityReplyAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/ui/CreatorChannelCommunityReplyAdapter.kt new file mode 100644 index 00000000..b4dd5b04 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/ui/CreatorChannelCommunityReplyAdapter.kt @@ -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() { + + private var items: List = emptyList() + + fun submitItems(items: List) { + 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 + } +}