feat(creator-channel): 커뮤니티 상세 화면을 구현한다
This commit is contained in:
@@ -0,0 +1,233 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
|
||||||
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChannelCommunityDetailBinding>(
|
||||||
|
ActivityCreatorChannelCommunityDetailBinding::inflate
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel()
|
||||||
|
private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
viewModel.loadDetail(postId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val commentAdapter = CreatorChannelCommunityCommentAdapter(
|
||||||
|
onReplyClick = { comment -> replyLauncher.launch(CreatorChannelCommunityReplyActivity.newIntent(this, postId, comment)) },
|
||||||
|
onModifyClick = { comment -> showModifyCommentDialog(comment.commentId, comment.comment) },
|
||||||
|
onDeleteClick = { comment -> showDeleteCommentDialog(comment.commentId) }
|
||||||
|
)
|
||||||
|
private lateinit var mediaPlayerManager: CreatorCommunityMediaPlayerManager
|
||||||
|
private var postId: Long = 0L
|
||||||
|
|
||||||
|
override fun setupView() {
|
||||||
|
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) { }
|
||||||
|
postId = intent.getLongExtra(EXTRA_POST_ID, 0L)
|
||||||
|
if (postId <= 0L) {
|
||||||
|
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
||||||
|
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.rvCreatorChannelCommunityDetailComments.adapter = commentAdapter
|
||||||
|
binding.scrollCreatorChannelCommunityDetail.setOnScrollChangeListener { scrollView, _, scrollY, _, oldScrollY ->
|
||||||
|
val contentHeight = binding.scrollCreatorChannelCommunityDetail.getChildAt(0)?.measuredHeight ?: 0
|
||||||
|
val isScrollingDown = scrollY > oldScrollY
|
||||||
|
val isAtBottom = scrollY >= contentHeight - scrollView.measuredHeight
|
||||||
|
if (isScrollingDown && isAtBottom) {
|
||||||
|
viewModel.loadMoreComments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.etCreatorChannelCommunityDetailComment.movementMethod = ScrollingMovementMethod()
|
||||||
|
binding.etCreatorChannelCommunityDetailComment.doAfterTextChanged { editable ->
|
||||||
|
viewModel.updateCommentInput(editable?.toString().orEmpty())
|
||||||
|
}
|
||||||
|
binding.btnCreatorChannelCommunityDetailSend.setOnClickListener { viewModel.submitComment() }
|
||||||
|
binding.ivCreatorChannelCommunityDetailHeart.setOnClickListener { viewModel.toggleLike() }
|
||||||
|
|
||||||
|
viewModel.detailStateLiveData.observe(this) { state -> render(state) }
|
||||||
|
viewModel.commentWrittenEventLiveData.observe(this) { written ->
|
||||||
|
if (written == true) {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
viewModel.consumeCommentWrittenEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.postChangedEventLiveData.observe(this) { changed ->
|
||||||
|
if (changed == true) {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
viewModel.consumePostChangedEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.toastLiveData.observe(this) { event ->
|
||||||
|
event.consume()?.let { toast ->
|
||||||
|
val message = toast.message ?: toast.resId?.let(::getString)
|
||||||
|
message?.let(::showToast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.loadDetail(postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
mediaPlayerManager.pauseContent()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mediaPlayerManager.stopContent()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun render(state: CreatorChannelCommunityDetailUiState) {
|
||||||
|
when (state) {
|
||||||
|
CreatorChannelCommunityDetailUiState.Loading -> Unit
|
||||||
|
is CreatorChannelCommunityDetailUiState.Error -> {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
state.message ?: getString(R.string.creator_channel_community_error_message),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is CreatorChannelCommunityDetailUiState.Content -> renderContent(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderContent(content: CreatorChannelCommunityDetailUiState.Content) = with(binding) {
|
||||||
|
val post = content.post
|
||||||
|
ivCreatorChannelCommunityDetailProfile.loadUrl(post.creatorProfileUrl) {
|
||||||
|
crossfade(true)
|
||||||
|
placeholder(R.drawable.bg_placeholder)
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
}
|
||||||
|
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
|
||||||
|
tvCreatorChannelCommunityDetailTime.text = post.createdAtText
|
||||||
|
tvCreatorChannelCommunityDetailBody.text = post.content
|
||||||
|
tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat()
|
||||||
|
tvCreatorChannelCommunityDetailCommentCount.text = post.commentCount.moneyFormat()
|
||||||
|
tvCreatorChannelCommunityDetailCommentHeader.text = getString(
|
||||||
|
R.string.creator_channel_community_comment_header,
|
||||||
|
post.commentCount.moneyFormat()
|
||||||
|
)
|
||||||
|
ivCreatorChannelCommunityDetailHeart.setImageResource(
|
||||||
|
if (post.isLiked) R.drawable.ic_feed_community_heart_fill else R.drawable.ic_feed_community_heart
|
||||||
|
)
|
||||||
|
layoutCreatorChannelCommunityDetailPrice.isVisible = post.isLocked
|
||||||
|
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
|
||||||
|
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
|
||||||
|
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
||||||
|
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
|
||||||
|
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
|
||||||
|
if (post.imageUrl != null) {
|
||||||
|
Glide.with(ivCreatorChannelCommunityDetailImage)
|
||||||
|
.load(post.imageUrl)
|
||||||
|
.placeholder(R.drawable.ic_place_holder)
|
||||||
|
.into(ivCreatorChannelCommunityDetailImage)
|
||||||
|
} else {
|
||||||
|
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
|
||||||
|
}
|
||||||
|
tvCreatorChannelCommunityDetailAudio.isVisible = post.audioUrl != null
|
||||||
|
tvCreatorChannelCommunityDetailAudio.text = getString(R.string.creator_channel_tab_audio)
|
||||||
|
tvCreatorChannelCommunityDetailAudio.setOnClickListener {
|
||||||
|
toggleAudio(post)
|
||||||
|
}
|
||||||
|
rvCreatorChannelCommunityDetailComments.isVisible = content.isCommentAvailable
|
||||||
|
tvCreatorChannelCommunityDetailCommentHeader.isVisible = content.isCommentAvailable
|
||||||
|
layoutCreatorChannelCommunityDetailInputBar.isVisible = content.isCommentInputVisible
|
||||||
|
ivCreatorChannelCommunityDetailComment.isVisible = content.isCommentAvailable
|
||||||
|
tvCreatorChannelCommunityDetailCommentCount.isVisible = content.isCommentAvailable
|
||||||
|
commentAdapter.submitItems(content.comments)
|
||||||
|
if (etCreatorChannelCommunityDetailComment.text.toString() != content.commentInput) {
|
||||||
|
etCreatorChannelCommunityDetailComment.setText(content.commentInput)
|
||||||
|
etCreatorChannelCommunityDetailComment.setSelection(content.commentInput.length)
|
||||||
|
}
|
||||||
|
updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityDetailSend) {
|
||||||
|
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 toggleAudio(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||||
|
val audioUrl = post.audioUrl ?: return
|
||||||
|
mediaPlayerManager.toggleContent(CreatorCommunityContentItem(post.postId, audioUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showModifyCommentDialog(commentId: 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.modifyComment(commentId, modifiedComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteCommentDialog(commentId: 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.deleteComment(commentId) }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_POST_ID = "extra_post_id"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, postId: Long): Intent {
|
||||||
|
return Intent(context, CreatorChannelCommunityDetailActivity::class.java)
|
||||||
|
.putExtra(EXTRA_POST_ID, postId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.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.ItemCreatorChannelCommunityCommentBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
|
||||||
|
|
||||||
|
class CreatorChannelCommunityCommentAdapter(
|
||||||
|
private val onReplyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||||
|
private val onModifyClick: (CreatorChannelCommunityCommentUiModel) -> Unit = {},
|
||||||
|
private val onDeleteClick: (CreatorChannelCommunityCommentUiModel) -> Unit = {}
|
||||||
|
) : RecyclerView.Adapter<CreatorChannelCommunityCommentAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var items: List<CreatorChannelCommunityCommentUiModel> = emptyList()
|
||||||
|
|
||||||
|
fun submitItems(items: List<CreatorChannelCommunityCommentUiModel>) {
|
||||||
|
this.items = items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return ViewHolder(
|
||||||
|
ItemCreatorChannelCommunityCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
|
onReplyClick,
|
||||||
|
onModifyClick,
|
||||||
|
onDeleteClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemCreatorChannelCommunityCommentBinding,
|
||||||
|
private val onReplyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||||
|
private val onModifyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||||
|
private val onDeleteClick: (CreatorChannelCommunityCommentUiModel) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(item: CreatorChannelCommunityCommentUiModel) {
|
||||||
|
bindComment(binding, item, onReplyClick)
|
||||||
|
binding.ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore
|
||||||
|
binding.ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor ->
|
||||||
|
showOptionMenu(anchor, item, onModifyClick, onDeleteClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun bindComment(
|
||||||
|
binding: ItemCreatorChannelCommunityCommentBinding,
|
||||||
|
item: CreatorChannelCommunityCommentUiModel,
|
||||||
|
onReplyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||||
|
onModifyClick: ((CreatorChannelCommunityCommentUiModel) -> Unit)? = null,
|
||||||
|
onDeleteClick: ((CreatorChannelCommunityCommentUiModel) -> Unit)? = null
|
||||||
|
) = with(binding) {
|
||||||
|
ivCreatorChannelCommunityCommentProfile.loadUrl(item.memberProfileUrl) {
|
||||||
|
crossfade(true)
|
||||||
|
placeholder(R.drawable.bg_placeholder)
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
}
|
||||||
|
tvCreatorChannelCommunityCommentNickname.text = item.memberNickname
|
||||||
|
tvCreatorChannelCommunityCommentTime.text = item.createdAtText
|
||||||
|
tvCreatorChannelCommunityCommentBody.text = item.comment
|
||||||
|
ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore && onDeleteClick != null
|
||||||
|
ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor ->
|
||||||
|
showOptionMenu(
|
||||||
|
anchor,
|
||||||
|
item,
|
||||||
|
onModifyClick ?: {},
|
||||||
|
onDeleteClick ?: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
layoutCreatorChannelCommunityCommentLatestReply.isVisible = item.latestReply != null
|
||||||
|
item.latestReply?.let { latestReply ->
|
||||||
|
tvCreatorChannelCommunityCommentLatestReplyNickname.text = latestReply.memberNickname
|
||||||
|
tvCreatorChannelCommunityCommentLatestReplyBody.text = latestReply.comment
|
||||||
|
}
|
||||||
|
root.setOnClickListener { onReplyClick(item) }
|
||||||
|
layoutCreatorChannelCommunityCommentLatestReply.setOnClickListener { onReplyClick(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showOptionMenu(
|
||||||
|
anchor: View,
|
||||||
|
item: CreatorChannelCommunityCommentUiModel,
|
||||||
|
onModifyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||||
|
onDeleteClick: (CreatorChannelCommunityCommentUiModel) -> 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 CreatorChannelCommunityCommentUiModel.canShowMore: Boolean
|
||||||
|
get() = isMine || isCreatorOwner
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user