feat(creator): 커뮤니티 구매와 메뉴 동작을 정리한다
This commit is contained in:
@@ -6,12 +6,4 @@ sealed interface CommunityChange {
|
||||
data object Created : CommunityChange
|
||||
|
||||
data object Updated : CommunityChange
|
||||
|
||||
data class Deleted(
|
||||
val postId: Long
|
||||
) : CommunityChange
|
||||
|
||||
data class PinChanged(
|
||||
val postId: Long
|
||||
) : CommunityChange
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
||||
@@ -60,8 +61,9 @@ import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||
@@ -96,6 +98,7 @@ class CreatorChannelActivity :
|
||||
|
||||
private val liveViewModel: LiveViewModel by inject()
|
||||
private val creatorCommunityRepository: CreatorCommunityRepository by inject()
|
||||
private val creatorChannelRepository: CreatorChannelRepository by inject()
|
||||
private var creatorId: Long = 0L
|
||||
private var currentHeader: CreatorChannelHeaderUiModel? = null
|
||||
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
|
||||
@@ -107,6 +110,7 @@ class CreatorChannelActivity :
|
||||
private var isOwnerFabAnimating: Boolean = false
|
||||
private var isDonationFloatingButtonVisible: Boolean = false
|
||||
private var isFanTalkFixedPlusVisible: Boolean = false
|
||||
private var isCommunityPostMutating: Boolean = false
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
|
||||
private val liveActionCoordinator: LiveActionCoordinator by lazy {
|
||||
@@ -126,13 +130,6 @@ class CreatorChannelActivity :
|
||||
resolveCommunityActivityResult(CommunityActivityResultSource.Write, result.resultCode)
|
||||
)
|
||||
}
|
||||
private val communityPostModifyLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
handleCommunityChange(
|
||||
resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode)
|
||||
)
|
||||
}
|
||||
private val communityDetailLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
@@ -140,6 +137,13 @@ class CreatorChannelActivity :
|
||||
resolveCommunityActivityResult(CommunityActivityResultSource.Detail, result.resultCode)
|
||||
)
|
||||
}
|
||||
private val communityModifyLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
handleCommunityChange(
|
||||
resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode)
|
||||
)
|
||||
}
|
||||
private val fanTalkWriteLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
@@ -627,6 +631,142 @@ class CreatorChannelActivity :
|
||||
handleCommunityAction(CommunityActionCommand.PostDetail(postId), communityDetailLauncher::launch)
|
||||
}
|
||||
|
||||
override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem) {
|
||||
if (!item.showMore) return
|
||||
|
||||
CreatorCommunityPostMenuBottomSheetDialog(
|
||||
isFixed = item.isPinned,
|
||||
isCreator = item.isOwner,
|
||||
onClickPin = { updateCreatorChannelCommunityPostFixed(item) },
|
||||
onClickModify = {
|
||||
if (item.isOwner) {
|
||||
communityModifyLauncher.launch(
|
||||
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onClickDelete = { showDeleteCreatorChannelCommunityPostDialog(item) },
|
||||
onClickReport = {
|
||||
CreatorCommunityReportDialog(
|
||||
this,
|
||||
layoutInflater
|
||||
) { reason -> reportCreatorChannelCommunityPost(item, reason) }.show(screenWidth)
|
||||
}
|
||||
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem) {
|
||||
if (!item.isOwner || isCommunityPostMutating) return
|
||||
|
||||
isCommunityPostMutating = true
|
||||
compositeDisposable.add(
|
||||
creatorCommunityRepository.updateCommunityPostFixed(item.postId, !item.isPinned, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isCommunityPostMutating = false
|
||||
if (response.success) {
|
||||
handleCommunityChange(CommunityChange.Updated)
|
||||
} else {
|
||||
showCommunityMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isCommunityPostMutating = false
|
||||
showCommunityMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDeleteCreatorChannelCommunityPostDialog(item: CreatorChannelCommunityMenuItem) {
|
||||
if (!item.isOwner) return
|
||||
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_creator_community_delete_title),
|
||||
desc = getString(R.string.screen_creator_community_delete_desc),
|
||||
confirmButtonTitle = getString(R.string.confirm_delete_title),
|
||||
confirmButtonClick = { deleteCreatorChannelCommunityPost(item) },
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem) {
|
||||
if (!item.isOwner || isCommunityPostMutating) return
|
||||
|
||||
val request = Gson().toJson(
|
||||
ModifyCommunityPostRequest(
|
||||
creatorCommunityId = item.postId,
|
||||
isActive = false
|
||||
)
|
||||
).toRequestBody("text/plain".toMediaType())
|
||||
isCommunityPostMutating = true
|
||||
compositeDisposable.add(
|
||||
creatorCommunityRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isCommunityPostMutating = false
|
||||
if (response.success) {
|
||||
handleCommunityChange(CommunityChange.Updated)
|
||||
} else {
|
||||
showCommunityMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isCommunityPostMutating = false
|
||||
showCommunityMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun reportCreatorChannelCommunityPost(
|
||||
item: CreatorChannelCommunityMenuItem,
|
||||
reason: String
|
||||
) {
|
||||
if (item.isOwner || !item.showMore || reason.isBlank() || isCommunityPostMutating) return
|
||||
|
||||
isCommunityPostMutating = true
|
||||
compositeDisposable.add(
|
||||
creatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isCommunityPostMutating = false
|
||||
if (response.success) {
|
||||
showToast(
|
||||
if (response.message.isNullOrBlank()) {
|
||||
getString(R.string.character_comment_report_submitted)
|
||||
} else {
|
||||
response.message
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showCommunityMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isCommunityPostMutating = false
|
||||
showCommunityMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCommunityMutationFailureToast(message: String? = null) {
|
||||
showToast(message.takeUnless { it.isNullOrBlank() } ?: getString(R.string.common_error_unknown))
|
||||
}
|
||||
|
||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||
|
||||
override fun onCreatorChannelFanTalkContentChanged() {
|
||||
updateViewPagerHeight {
|
||||
postCheckCreatorChannelCurrentTabNeedsMore()
|
||||
@@ -697,93 +837,6 @@ class CreatorChannelActivity :
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
override fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) {
|
||||
CreatorCommunityPostMenuBottomSheetDialog(
|
||||
isFixed = item.isPinned,
|
||||
isCreator = true,
|
||||
onClickPin = {
|
||||
updateCreatorChannelCommunityPostFixed(item)
|
||||
},
|
||||
onClickModify = {
|
||||
communityPostModifyLauncher.launch(
|
||||
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickDelete = {
|
||||
showCreatorChannelCommunityDeleteDialog(item)
|
||||
},
|
||||
onClickReport = {}
|
||||
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun showCreatorChannelCommunityDeleteDialog(item: CreatorChannelCommunityPostUiModel) {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_creator_community_delete_title),
|
||||
desc = getString(R.string.screen_creator_community_delete_desc),
|
||||
confirmButtonTitle = getString(R.string.confirm_delete_title),
|
||||
confirmButtonClick = {
|
||||
deleteCreatorChannelCommunityPost(item)
|
||||
},
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityPostUiModel) {
|
||||
compositeDisposable.add(
|
||||
creatorCommunityRepository.updateCommunityPostFixed(
|
||||
postId = item.postId,
|
||||
isFixed = !item.isPinned,
|
||||
token = authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
if (response.success) {
|
||||
handleCommunityChange(CommunityChange.PinChanged(item.postId))
|
||||
} else {
|
||||
response.message?.let(::showToast)
|
||||
}
|
||||
},
|
||||
{ error -> error.message?.let(::showToast) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityPostUiModel) {
|
||||
val request = ModifyCommunityPostRequest(
|
||||
creatorCommunityId = item.postId,
|
||||
isActive = false
|
||||
)
|
||||
val requestJson = Gson().toJson(request)
|
||||
compositeDisposable.add(
|
||||
creatorCommunityRepository.modifyCommunityPost(
|
||||
postImage = null,
|
||||
request = requestJson.toRequestBody("text/plain".toMediaType()),
|
||||
token = authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
if (response.success) {
|
||||
handleCommunityChange(CommunityChange.Deleted(item.postId))
|
||||
} else {
|
||||
response.message?.let(::showToast)
|
||||
}
|
||||
},
|
||||
{ error -> error.message?.let(::showToast) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||
|
||||
private fun refreshCreatorChannelCommunity() {
|
||||
findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()
|
||||
}
|
||||
@@ -795,9 +848,10 @@ class CreatorChannelActivity :
|
||||
homeActionDelegate?.refreshHome()
|
||||
refreshCreatorChannelCommunity()
|
||||
}
|
||||
CommunityChange.Updated,
|
||||
is CommunityChange.Deleted,
|
||||
is CommunityChange.PinChanged -> refreshCreatorChannelCommunity()
|
||||
CommunityChange.Updated -> {
|
||||
homeActionDelegate?.refreshHome()
|
||||
refreshCreatorChannelCommunity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioConte
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
||||
@@ -28,7 +29,8 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
||||
onSeriesClick = ::onSeriesClicked,
|
||||
onDonationClick = ::onDonationClicked,
|
||||
onSectionChevronClick = ::onSectionChevronClicked,
|
||||
onCommunityClick = ::onCommunityClicked
|
||||
onCommunityClick = ::onCommunityClicked,
|
||||
onCommunityMoreClick = ::onCommunityMoreClicked
|
||||
)
|
||||
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
||||
private val host: Host
|
||||
@@ -134,6 +136,10 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
||||
host.onCreatorChannelCommunityPostClicked(postId)
|
||||
}
|
||||
|
||||
private fun onCommunityMoreClicked(item: CreatorChannelCommunityMenuItem) {
|
||||
host.onCreatorChannelCommunityMoreClicked(item)
|
||||
}
|
||||
|
||||
private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) {
|
||||
host.onCreatorChannelCurrentLiveClicked(live)
|
||||
}
|
||||
@@ -150,6 +156,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
||||
fun onCreatorChannelDonationClicked()
|
||||
fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab)
|
||||
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
||||
fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
|
||||
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding
|
||||
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.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
|
||||
@@ -29,14 +27,11 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
||||
private val viewModel: CreatorChannelCommunityViewModel by viewModel()
|
||||
private val listAdapter = CreatorChannelCommunityListAdapter(
|
||||
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) },
|
||||
onPlayClick = { item -> toggleCommunityAudio(item) },
|
||||
onOwnerMoreClick = { item -> host.onCreatorChannelCommunityOwnerMoreClicked(item) },
|
||||
isPlayingContent = { postId -> mediaPlayerManager?.isPlayingContent(postId) == true }
|
||||
onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) }
|
||||
)
|
||||
private val gridAdapter = CreatorChannelCommunityGridAdapter(
|
||||
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }
|
||||
)
|
||||
private var mediaPlayerManager: CreatorCommunityMediaPlayerManager? = null
|
||||
private var currentContentState: CreatorChannelCommunityUiState.Content? = null
|
||||
private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null
|
||||
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
||||
@@ -45,7 +40,6 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mediaPlayerManager = CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }
|
||||
bindLoading()
|
||||
setupCommunityList()
|
||||
setupClickListeners()
|
||||
@@ -53,19 +47,12 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
mediaPlayerManager?.stopContent()
|
||||
mediaPlayerManager = null
|
||||
currentContentState = null
|
||||
lastContentLayoutKey = null
|
||||
binding.rvCreatorChannelCommunity.adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mediaPlayerManager?.pauseContent()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun setupCommunityList() = with(binding.rvCreatorChannelCommunity) {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = listAdapter
|
||||
@@ -220,16 +207,11 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
||||
layoutCreatorChannelCommunityEmpty.updatePadding(bottom = bottomPadding)
|
||||
}
|
||||
|
||||
private fun toggleCommunityAudio(item: CreatorChannelCommunityPostUiModel) {
|
||||
val audioUrl = item.audioUrl ?: return
|
||||
mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))
|
||||
}
|
||||
|
||||
interface Host {
|
||||
fun isCreatorChannelOwner(): Boolean
|
||||
fun onCreatorChannelCommunityContentChanged()
|
||||
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
||||
fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel)
|
||||
fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -23,10 +23,14 @@ import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PurchaseCommunityPostDialog
|
||||
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.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
@@ -40,6 +44,12 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
||||
) {
|
||||
|
||||
private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel()
|
||||
private val modifyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
viewModel.loadDetail(postId)
|
||||
}
|
||||
}
|
||||
private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
@@ -65,6 +75,11 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
||||
}
|
||||
|
||||
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
||||
binding.btnCreatorChannelCommunityDetailMore.setOnClickListener {
|
||||
val content = viewModel.detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@setOnClickListener
|
||||
showPostMenu(content.post)
|
||||
}
|
||||
binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true
|
||||
binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider()
|
||||
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
||||
@@ -103,6 +118,13 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
||||
viewModel.consumePostChangedEvent()
|
||||
}
|
||||
}
|
||||
viewModel.postDeletedEventLiveData.observe(this) { deleted ->
|
||||
if (deleted == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
viewModel.consumePostDeletedEvent()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
viewModel.toastLiveData.observe(this) { event ->
|
||||
event.consume()?.let { toast ->
|
||||
val message = toast.message ?: toast.resId?.let(::getString)
|
||||
@@ -150,6 +172,7 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
||||
}
|
||||
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
|
||||
tvCreatorChannelCommunityDetailTime.text = post.createdAtText
|
||||
btnCreatorChannelCommunityDetailMore.isVisible = post.showMore
|
||||
tvCreatorChannelCommunityDetailBody.text = post.content
|
||||
layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable
|
||||
tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat()
|
||||
@@ -163,9 +186,6 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
||||
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
|
||||
layoutCreatorChannelCommunityDetailImageContainer.layoutParams =
|
||||
layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply {
|
||||
@@ -296,6 +316,37 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun showPostMenu(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||
CreatorCommunityPostMenuBottomSheetDialog(
|
||||
isFixed = post.isPinned,
|
||||
isCreator = post.isOwner,
|
||||
onClickPin = viewModel::updateCommunityPostFixed,
|
||||
onClickModify = {
|
||||
modifyLauncher.launch(
|
||||
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, post.postId)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickDelete = ::showDeletePostDialog,
|
||||
onClickReport = {
|
||||
CreatorCommunityReportDialog(this, layoutInflater, viewModel::reportCommunityPost).show(screenWidth)
|
||||
}
|
||||
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun showDeletePostDialog() {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_creator_community_delete_title),
|
||||
desc = getString(R.string.screen_creator_community_delete_desc),
|
||||
confirmButtonTitle = getString(R.string.confirm_delete_title),
|
||||
confirmButtonClick = viewModel::deleteCommunityPost,
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun showDeleteCommentDialog(commentId: Long) {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.gson.Gson
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
@@ -12,12 +13,15 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class CreatorChannelCommunityDetailViewModel(
|
||||
private val repository: CreatorChannelRepository,
|
||||
@@ -37,6 +41,10 @@ class CreatorChannelCommunityDetailViewModel(
|
||||
val postChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _postChangedEventLiveData
|
||||
|
||||
private val _postDeletedEventLiveData = MutableLiveData<Boolean>()
|
||||
val postDeletedEventLiveData: LiveData<Boolean>
|
||||
get() = _postDeletedEventLiveData
|
||||
|
||||
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
||||
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
||||
get() = _toastLiveData
|
||||
@@ -49,6 +57,7 @@ class CreatorChannelCommunityDetailViewModel(
|
||||
private var isSendingComment = false
|
||||
private var isModifyingComment = false
|
||||
private var isPurchasingPost = false
|
||||
private var isMutatingPost = false
|
||||
|
||||
fun loadDetail(postId: Long) {
|
||||
if (postId <= 0) return
|
||||
@@ -181,6 +190,10 @@ class CreatorChannelCommunityDetailViewModel(
|
||||
_postChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumePostDeletedEvent() {
|
||||
_postDeletedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun startCommentEdit(commentId: Long, comment: String) {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (commentId <= 0) return
|
||||
@@ -245,6 +258,97 @@ class CreatorChannelCommunityDetailViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun updateCommunityPostFixed() {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (!content.post.isOwner || isMutatingPost) return
|
||||
|
||||
isMutatingPost = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.updateCommunityPostFixed(postId, !content.post.isPinned, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isMutatingPost = false
|
||||
if (response.success) {
|
||||
_postChangedEventLiveData.value = true
|
||||
loadDetail(postId)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isMutatingPost = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteCommunityPost() {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (!content.post.isOwner || isMutatingPost) return
|
||||
|
||||
val request = Gson().toJson(
|
||||
ModifyCommunityPostRequest(
|
||||
creatorCommunityId = postId,
|
||||
isActive = false
|
||||
)
|
||||
).toRequestBody("text/plain".toMediaType())
|
||||
isMutatingPost = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isMutatingPost = false
|
||||
if (response.success) {
|
||||
_postDeletedEventLiveData.value = true
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isMutatingPost = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun reportCommunityPost(reason: String) {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (reason.isBlank() || content.post.isOwner || !content.post.showMore || isMutatingPost) return
|
||||
|
||||
isMutatingPost = true
|
||||
compositeDisposable.add(
|
||||
repository.reportCommunityPost(postId, reason, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isMutatingPost = false
|
||||
if (response.success) {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
if (response.message.isNullOrBlank()) {
|
||||
ToastMessage(resId = R.string.character_comment_report_submitted)
|
||||
} else {
|
||||
ToastMessage(message = response.message)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isMutatingPost = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun submitCommentEdit(
|
||||
content: CreatorChannelCommunityDetailUiState.Content,
|
||||
commentId: Long,
|
||||
@@ -425,25 +529,30 @@ class CreatorChannelCommunityDetailViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel(
|
||||
postId = postId,
|
||||
creatorId = creatorId,
|
||||
creatorNickname = creatorNickname,
|
||||
creatorProfileUrl = creatorProfileUrl,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
content = content,
|
||||
imageUrl = imageUrl.takeUnless { isLocked() },
|
||||
audioUrl = audioUrl.takeUnless { isLocked() },
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount,
|
||||
isLiked = isLiked,
|
||||
isPinned = isPinned,
|
||||
isLocked = isLocked(),
|
||||
showPaywall = isLocked(),
|
||||
showReaction = !isLocked()
|
||||
)
|
||||
private fun CreatorChannelCommunityPostDetailResponse.toUiModel(): CreatorChannelCommunityPostDetailUiModel {
|
||||
val isOwner = creatorId == SharedPreferenceManager.userId
|
||||
return CreatorChannelCommunityPostDetailUiModel(
|
||||
postId = postId,
|
||||
creatorId = creatorId,
|
||||
creatorNickname = creatorNickname,
|
||||
creatorProfileUrl = creatorProfileUrl,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
content = content,
|
||||
imageUrl = imageUrl.takeUnless { isLocked() },
|
||||
audioUrl = audioUrl.takeUnless { isLocked() },
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount,
|
||||
isLiked = isLiked,
|
||||
isPinned = isPinned,
|
||||
isOwner = isOwner,
|
||||
showMore = isOwner || price == 0 || existOrdered,
|
||||
isLocked = isLocked(),
|
||||
showPaywall = isLocked(),
|
||||
showReaction = !isLocked()
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean {
|
||||
return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId
|
||||
@@ -526,6 +635,8 @@ data class CreatorChannelCommunityPostDetailUiModel(
|
||||
val commentCount: Int,
|
||||
val isLiked: Boolean,
|
||||
val isPinned: Boolean,
|
||||
val isOwner: Boolean,
|
||||
val showMore: Boolean,
|
||||
val isLocked: Boolean,
|
||||
val showPaywall: Boolean,
|
||||
val showReaction: Boolean
|
||||
|
||||
@@ -21,7 +21,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
|
||||
val isLocked = price > 0 && !existOrdered && !isOwner
|
||||
val showOwnerActions = isOwner && creatorId == currentUserId
|
||||
val visibleImageUrl = imageUrl.takeUnless { isLocked }
|
||||
val showPlayButton = !isLocked && !audioUrl.isNullOrBlank() && !visibleImageUrl.isNullOrBlank()
|
||||
return CreatorChannelCommunityPostUiModel(
|
||||
postId = postId,
|
||||
creatorId = creatorId,
|
||||
@@ -30,7 +29,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
content = content,
|
||||
imageUrl = visibleImageUrl,
|
||||
audioUrl = audioUrl,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
likeCount = likeCount,
|
||||
@@ -40,11 +38,17 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
|
||||
showNotice = isPinned,
|
||||
isPinned = isPinned,
|
||||
isLocked = isLocked,
|
||||
showOwnerMore = showOwnerActions,
|
||||
showOwnerTopPrice = showOwnerActions && price > 0,
|
||||
showPlayButton = showPlayButton,
|
||||
gridPreviewText = content.toGridPreviewText(),
|
||||
imageMode = toImageMode(isLocked, visibleImageUrl)
|
||||
imageMode = toImageMode(isLocked, visibleImageUrl),
|
||||
menuItem = CreatorChannelCommunityMenuItem(
|
||||
postId = postId,
|
||||
creatorId = creatorId,
|
||||
currentUserId = currentUserId,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
isPinned = isPinned
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.model
|
||||
|
||||
data class CreatorChannelCommunityMenuItem(
|
||||
val postId: Long,
|
||||
val creatorId: Long,
|
||||
val currentUserId: Long,
|
||||
val price: Int,
|
||||
val existOrdered: Boolean,
|
||||
val isPinned: Boolean
|
||||
) {
|
||||
val isOwner: Boolean
|
||||
get() = creatorId == currentUserId
|
||||
|
||||
val showMore: Boolean
|
||||
get() = isOwner || price == 0 || existOrdered
|
||||
}
|
||||
@@ -32,7 +32,6 @@ data class CreatorChannelCommunityPostUiModel(
|
||||
val createdAtText: String,
|
||||
val content: String,
|
||||
val imageUrl: String?,
|
||||
val audioUrl: String?,
|
||||
val price: Int,
|
||||
val existOrdered: Boolean,
|
||||
val likeCount: Int,
|
||||
@@ -42,9 +41,8 @@ data class CreatorChannelCommunityPostUiModel(
|
||||
val showNotice: Boolean,
|
||||
val isPinned: Boolean,
|
||||
val isLocked: Boolean,
|
||||
val showOwnerMore: Boolean,
|
||||
val showOwnerTopPrice: Boolean,
|
||||
val showPlayButton: Boolean,
|
||||
val gridPreviewText: String,
|
||||
val imageMode: CreatorChannelCommunityImageMode
|
||||
val imageMode: CreatorChannelCommunityImageMode,
|
||||
val menuItem: CreatorChannelCommunityMenuItem
|
||||
)
|
||||
|
||||
@@ -19,14 +19,13 @@ import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBindi
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
|
||||
|
||||
class CreatorChannelCommunityListAdapter(
|
||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||
private val isPlayingContent: (Long) -> Boolean = { false }
|
||||
private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}
|
||||
) : RecyclerView.Adapter<CreatorChannelCommunityListAdapter.ViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelCommunityPostUiModel> = emptyList()
|
||||
@@ -40,9 +39,7 @@ class CreatorChannelCommunityListAdapter(
|
||||
return ViewHolder(
|
||||
ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
onPostClick,
|
||||
onPlayClick,
|
||||
onOwnerMoreClick,
|
||||
isPlayingContent
|
||||
onMoreClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,9 +52,7 @@ class CreatorChannelCommunityListAdapter(
|
||||
class ViewHolder(
|
||||
private val binding: ItemCreatorChannelCommunityListBinding,
|
||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||
private val isPlayingContent: (Long) -> Boolean
|
||||
private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
init {
|
||||
@@ -86,7 +81,7 @@ class CreatorChannelCommunityListAdapter(
|
||||
|
||||
val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked }
|
||||
layoutCreatorChannelCommunityListImageContainer.isVisible =
|
||||
visibleImageUrl != null || item.isLocked || item.showPlayButton
|
||||
visibleImageUrl != null || item.isLocked
|
||||
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
|
||||
resetCommunityImageHeight()
|
||||
if (visibleImageUrl != null) {
|
||||
@@ -119,19 +114,14 @@ class CreatorChannelCommunityListAdapter(
|
||||
}
|
||||
layoutCreatorChannelCommunityListLockedOverlay.isVisible = item.isLocked
|
||||
ivCreatorChannelCommunityListLock.isVisible = item.isLocked
|
||||
tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked
|
||||
layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked
|
||||
tvCreatorChannelCommunityListLockedPrice.text = item.price.moneyFormat()
|
||||
ivCreatorChannelCommunityListPlay.isVisible = item.showPlayButton
|
||||
ivCreatorChannelCommunityListPlay.setImageResource(
|
||||
if (isPlayingContent(item.postId)) R.drawable.ic_player_pause else R.drawable.ic_new_player_play
|
||||
)
|
||||
ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }
|
||||
|
||||
layoutCreatorChannelCommunityListTopActions.isVisible = item.showOwnerMore || item.showOwnerTopPrice
|
||||
layoutCreatorChannelCommunityListTopActions.isVisible = item.showOwnerTopPrice || item.menuItem.showMore
|
||||
layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice
|
||||
tvCreatorChannelCommunityListTopPrice.text = item.price.moneyFormat()
|
||||
ivCreatorChannelCommunityListOwnerMore.isVisible = item.showOwnerMore
|
||||
ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }
|
||||
btnCreatorChannelCommunityListMore.isVisible = item.menuItem.showMore
|
||||
btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) }
|
||||
}
|
||||
|
||||
private fun resetCommunityImageHeight() {
|
||||
|
||||
@@ -103,7 +103,9 @@ data class CreatorChannelCommunityPostResponse(
|
||||
@SerializedName("dateUtc") val dateUtc: String,
|
||||
@SerializedName("existOrdered") val existOrdered: Boolean,
|
||||
@SerializedName("likeCount") val likeCount: Int,
|
||||
@SerializedName("commentCount") val commentCount: Int
|
||||
@SerializedName("commentCount") val commentCount: Int,
|
||||
@SerializedName("isPinned") val isPinned: Boolean,
|
||||
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
|
||||
@@ -185,6 +185,11 @@ class CreatorChannelRepository(
|
||||
token = token
|
||||
)
|
||||
|
||||
fun reportCommunityPost(postId: Long, reason: String, token: String) = reportRepository.report(
|
||||
request = ReportRequest(ReportType.COMMUNITY_POST, reason, communityPostId = postId),
|
||||
token = token
|
||||
)
|
||||
|
||||
fun deleteFanTalk(fanTalkId: Long, token: String) = explorerRepository.modifyCheers(
|
||||
request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false),
|
||||
token = token
|
||||
|
||||
@@ -18,7 +18,8 @@ fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long): CreatorChanne
|
||||
?.let { add(CreatorChannelHomeSection.Schedules(it)) }
|
||||
audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) }
|
||||
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
||||
communities.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Communities(it)) }
|
||||
communities.takeIf { it.isNotEmpty() }
|
||||
?.let { add(CreatorChannelHomeSection.Communities(it, currentMemberId)) }
|
||||
add(CreatorChannelHomeSection.FanTalk(fanTalk))
|
||||
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
||||
add(CreatorChannelHomeSection.Activity(activity))
|
||||
|
||||
@@ -57,7 +57,10 @@ sealed interface CreatorChannelHomeSection {
|
||||
data class Schedules(val schedules: List<CreatorChannelScheduleResponse>) : CreatorChannelHomeSection
|
||||
data class AudioContents(val audioContents: List<CreatorChannelAudioContentResponse>) : CreatorChannelHomeSection
|
||||
data class Series(val series: List<CreatorChannelSeriesResponse>) : CreatorChannelHomeSection
|
||||
data class Communities(val communities: List<CreatorChannelCommunityPostResponse>) : CreatorChannelHomeSection
|
||||
data class Communities(
|
||||
val communities: List<CreatorChannelCommunityPostResponse>,
|
||||
val currentMemberId: Long
|
||||
) : CreatorChannelHomeSection
|
||||
data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection
|
||||
data class Introduce(val introduce: String) : CreatorChannelHomeSection
|
||||
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection
|
||||
|
||||
@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityP
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
||||
@@ -43,7 +44,8 @@ class CreatorChannelHomeSectionAdapter(
|
||||
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {},
|
||||
private val onDonationClick: () -> Unit = {},
|
||||
private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {},
|
||||
private val onCommunityClick: (Long) -> Unit = {}
|
||||
private val onCommunityClick: (Long) -> Unit = {},
|
||||
private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}
|
||||
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelHomeSection> = emptyList()
|
||||
@@ -65,7 +67,8 @@ class CreatorChannelHomeSectionAdapter(
|
||||
onSeriesClick,
|
||||
onDonationClick,
|
||||
onSectionChevronClick,
|
||||
onCommunityClick
|
||||
onCommunityClick,
|
||||
onCommunityMoreClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +86,8 @@ class CreatorChannelHomeSectionAdapter(
|
||||
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit,
|
||||
private val onDonationClick: () -> Unit,
|
||||
private val onSectionChevronClick: (CreatorChannelTab) -> Unit,
|
||||
private val onCommunityClick: (Long) -> Unit
|
||||
private val onCommunityClick: (Long) -> Unit,
|
||||
private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
private val title: TextView? = view.findViewById(R.id.tv_section_title)
|
||||
private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron)
|
||||
@@ -366,6 +370,14 @@ class CreatorChannelHomeSectionAdapter(
|
||||
communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) }
|
||||
val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)
|
||||
visibleCommunities.forEachIndexed { index, community ->
|
||||
val menuItem = CreatorChannelCommunityMenuItem(
|
||||
postId = community.postId,
|
||||
creatorId = community.creatorId,
|
||||
currentUserId = item.currentMemberId,
|
||||
price = community.price,
|
||||
existOrdered = community.existOrdered,
|
||||
isPinned = community.isPinned
|
||||
)
|
||||
val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(
|
||||
itemView.resources.configuration.screenWidthDp
|
||||
)
|
||||
@@ -381,7 +393,8 @@ class CreatorChannelHomeSectionAdapter(
|
||||
)
|
||||
)
|
||||
setHideEmptyTextRows(true)
|
||||
bind(community.toFeedCommunityItem())
|
||||
bind(community.toFeedCommunityItem(showMore = menuItem.showMore))
|
||||
setOnMoreClick { onCommunityMoreClick(menuItem) }
|
||||
}
|
||||
bindCommunityImages(row, community)
|
||||
row.layoutParams = LinearLayout.LayoutParams(
|
||||
@@ -443,7 +456,9 @@ class CreatorChannelHomeSectionAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(): FeedItem.Community = FeedItem.Community(
|
||||
private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(
|
||||
showMore: Boolean
|
||||
): FeedItem.Community = FeedItem.Community(
|
||||
feedId = postId.toString(),
|
||||
creatorId = creatorId.toString(),
|
||||
creatorName = creatorNickname,
|
||||
@@ -458,7 +473,11 @@ class CreatorChannelHomeSectionAdapter(
|
||||
audioUrl = audioUrl,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
showKeyword = false
|
||||
isPinned = isPinned,
|
||||
isCommentAvailable = isCommentAvailable,
|
||||
hideReactionWhenLocked = true,
|
||||
showKeyword = false,
|
||||
showMore = showMore
|
||||
)
|
||||
|
||||
private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) {
|
||||
|
||||
@@ -20,6 +20,8 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var profileImage: ImageView? = null
|
||||
private var noticeRow: View? = null
|
||||
private var moreButton: View? = null
|
||||
private var creatorText: TextView? = null
|
||||
private var createdAtText: TextView? = null
|
||||
private var bodyText: TextView? = null
|
||||
@@ -27,15 +29,21 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
private var communityImage: ImageView? = null
|
||||
private var paidOverlay: View? = null
|
||||
private var priceText: TextView? = null
|
||||
private var reactionRow: View? = null
|
||||
private var commentIcon: ImageView? = null
|
||||
private var commentCountText: TextView? = null
|
||||
private var likeIcon: ImageView? = null
|
||||
private var likeCountText: TextView? = null
|
||||
private var currentItem: FeedItem.Community? = null
|
||||
private var clickListener: ((FeedItem) -> Unit)? = null
|
||||
private var moreClickListener: ((FeedItem.Community) -> Unit)? = null
|
||||
private var hideEmptyTextRows: Boolean = false
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
noticeRow = findViewById(R.id.ll_feed_community_notice)
|
||||
profileImage = findViewById(R.id.iv_feed_community_profile)
|
||||
moreButton = findViewById(R.id.btn_feed_community_more)
|
||||
creatorText = findViewById(R.id.tv_feed_community_creator)
|
||||
createdAtText = findViewById(R.id.tv_feed_community_created_at)
|
||||
bodyText = findViewById(R.id.tv_feed_community_body)
|
||||
@@ -43,7 +51,10 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
communityImage = findViewById(R.id.iv_feed_community_image)
|
||||
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
|
||||
priceText = findViewById(R.id.tv_feed_community_price)
|
||||
reactionRow = findViewById(R.id.ll_feed_community_reaction)
|
||||
commentIcon = findViewById(R.id.iv_feed_community_comment)
|
||||
commentCountText = findViewById(R.id.tv_feed_community_comment_count)
|
||||
likeIcon = findViewById(R.id.iv_feed_community_like)
|
||||
likeCountText = findViewById(R.id.tv_feed_community_like_count)
|
||||
clipToOutline = true
|
||||
outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP)
|
||||
@@ -59,7 +70,8 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
requireNotNull(createdAtText).text = item.createdAtText
|
||||
requireNotNull(bodyText).text = item.bodyText
|
||||
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
|
||||
val isLocked = item.price > 0 && !item.existOrdered
|
||||
requireNotNull(noticeRow).isVisible = item.isPinned
|
||||
val isLocked = item.isLocked
|
||||
val hasImage = !item.imageUrl.isNullOrBlank()
|
||||
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
|
||||
resetCommunityImageHeight()
|
||||
@@ -69,9 +81,15 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
}
|
||||
requireNotNull(paidOverlay).isVisible = isLocked
|
||||
requireNotNull(priceText).text = item.price.toString()
|
||||
requireNotNull(reactionRow).isVisible = item.showReaction
|
||||
requireNotNull(commentIcon).isVisible = item.showComment
|
||||
requireNotNull(commentCountText).text = item.commentCount.toString()
|
||||
requireNotNull(commentCountText).isVisible = item.showComment
|
||||
requireNotNull(likeIcon).isVisible = item.showReaction
|
||||
requireNotNull(likeCountText).text = item.likeCount.toString()
|
||||
requireNotNull(likeCountText).isVisible = item.showReaction
|
||||
applyClickState(item)
|
||||
applyMoreClickState(item)
|
||||
}
|
||||
|
||||
fun profileImageView(): ImageView = requireNotNull(profileImage)
|
||||
@@ -116,6 +134,11 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
currentItem?.let(::applyClickState)
|
||||
}
|
||||
|
||||
fun setOnMoreClick(listener: ((FeedItem.Community) -> Unit)?) {
|
||||
moreClickListener = listener
|
||||
currentItem?.let(::applyMoreClickState)
|
||||
}
|
||||
|
||||
fun setHideEmptyTextRows(hide: Boolean) {
|
||||
hideEmptyTextRows = hide
|
||||
currentItem?.let(::bind)
|
||||
@@ -130,6 +153,14 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
isClickable = listener != null
|
||||
}
|
||||
|
||||
private fun applyMoreClickState(item: FeedItem.Community) {
|
||||
val listener = moreClickListener
|
||||
requireNotNull(moreButton).isVisible = item.showMore
|
||||
requireNotNull(moreButton).setOnClickListener(
|
||||
if (listener == null) null else View.OnClickListener { listener(item) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateRootWidth(width: Int) {
|
||||
val currentLayoutParams = layoutParams
|
||||
layoutParams = if (currentLayoutParams == null) {
|
||||
|
||||
@@ -47,6 +47,19 @@ sealed class FeedItem(open val feedId: String, val variant: FeedVariant) {
|
||||
val audioUrl: String? = null,
|
||||
val price: Int = 0,
|
||||
val existOrdered: Boolean = false,
|
||||
val showKeyword: Boolean = true
|
||||
) : FeedItem(feedId, FeedVariant.Community)
|
||||
val isPinned: Boolean = false,
|
||||
val isCommentAvailable: Boolean = true,
|
||||
val hideReactionWhenLocked: Boolean = false,
|
||||
val showKeyword: Boolean = true,
|
||||
val showMore: Boolean = false
|
||||
) : FeedItem(feedId, FeedVariant.Community) {
|
||||
val isLocked: Boolean
|
||||
get() = price > 0 && !existOrdered
|
||||
|
||||
val showReaction: Boolean
|
||||
get() = !hideReactionWhenLocked || !isLocked
|
||||
|
||||
val showComment: Boolean
|
||||
get() = showReaction && isCommentAvailable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toStartOf="@id/layout_creator_channel_community_detail_price"
|
||||
app:layout_constraintEnd_toStartOf="@id/btn_creator_channel_community_detail_more"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
||||
tools:text="크리에이터" />
|
||||
@@ -91,36 +91,17 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname"
|
||||
tools:text="2분 전" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_price"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_creator_channel_community_price"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_4"
|
||||
android:paddingVertical="2dp"
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_detail_more"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/read_more"
|
||||
android:src="@drawable/ic_seemore_vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_bar_cash" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_price"
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
tools:text="300" />
|
||||
</LinearLayout>
|
||||
tools:visibility="visible" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
@@ -222,22 +203,6 @@
|
||||
tools:text="Audio"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_purchased"
|
||||
style="@style/Typography.Caption2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_creator_channel_community_price"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingHorizontal="@dimen/spacing_8"
|
||||
android:paddingVertical="@dimen/spacing_4"
|
||||
android:text="@string/creator_channel_community_purchased_badge"
|
||||
android:textColor="@color/black"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_reaction"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -122,13 +122,13 @@
|
||||
tools:text="300" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_list_owner_more"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_more"
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_list_more"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/read_more"
|
||||
android:src="@drawable/ic_seemore_vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
@@ -187,32 +187,34 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_list_locked_price"
|
||||
style="@style/Typography.Body3"
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_list_locked_price"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:background="@drawable/bg_creator_channel_community_price"
|
||||
android:drawableStart="@drawable/ic_bar_cash"
|
||||
android:drawablePadding="@dimen/spacing_6"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:text="30"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_list_play"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_player_play"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_bar_cash" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_list_locked_price"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_6"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
tools:text="30" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -8,7 +8,33 @@
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_feed_community_notice"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_14"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_pin" />
|
||||
|
||||
<TextView
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/creator_channel_community_notice"
|
||||
android:textColor="@color/green_400" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="42dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
@@ -22,9 +48,10 @@
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
@@ -50,6 +77,16 @@
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="2분 전" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_feed_community_more"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/read_more"
|
||||
android:src="@drawable/ic_seemore_vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@@ -125,6 +162,7 @@
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_feed_community_reaction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="@dimen/spacing_16"
|
||||
@@ -132,6 +170,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_feed_community_comment"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
@@ -148,6 +187,7 @@
|
||||
tools:text="5" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_feed_community_like"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="15dp"
|
||||
|
||||
@@ -44,13 +44,4 @@ class CommunityChangeTest {
|
||||
|
||||
assertEquals(List(CommunityActivityResultSource.entries.size) { CommunityChange.Ignored }, changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `삭제와 고정 변경은 post ID를 가진 명시적 변경으로 표현한다`() {
|
||||
val deleted = CommunityChange.Deleted(postId = 31L)
|
||||
val pinChanged = CommunityChange.PinChanged(postId = 32L)
|
||||
|
||||
assertEquals(31L, deleted.postId)
|
||||
assertEquals(32L, pinChanged.postId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,164 @@ class CreatorChannelActivitySourceTest {
|
||||
assertTrue(source.contains("private fun handleCommunityChange(change: CommunityChange)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `홈과 커뮤니티 list 더보기는 동일 menu item을 Activity Host까지 전달한다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||
).readText()
|
||||
val homeFragment = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
|
||||
).readText()
|
||||
val communityFragment = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
|
||||
).readText()
|
||||
val activity = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(
|
||||
adapter.contains("private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}")
|
||||
)
|
||||
assertTrue(adapter.contains("currentUserId = item.currentMemberId"))
|
||||
assertTrue(adapter.contains("showMore = menuItem.showMore"))
|
||||
assertTrue(adapter.contains("setOnMoreClick { onCommunityMoreClick(menuItem) }"))
|
||||
assertTrue(adapter.contains("row.setOnClickListener { onCommunityClick(community.postId) }"))
|
||||
assertTrue(homeFragment.contains("onCommunityMoreClick = ::onCommunityMoreClicked"))
|
||||
assertTrue(homeFragment.contains("host.onCreatorChannelCommunityMoreClicked(item)"))
|
||||
assertTrue(
|
||||
homeFragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)")
|
||||
)
|
||||
assertTrue(communityFragment.contains("host.onCreatorChannelCommunityMoreClicked(item)"))
|
||||
assertTrue(
|
||||
communityFragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)")
|
||||
)
|
||||
assertTrue(
|
||||
activity.contains(
|
||||
"override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 더보기는 하나의 Activity callback에서 권한별 기존 메뉴와 수정 결과를 연결한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val handlerSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)",
|
||||
endMarker = "override fun onCreatorChannelFanTalkContentChanged"
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
Regex(
|
||||
Regex.escape(
|
||||
"override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"
|
||||
)
|
||||
)
|
||||
.findAll(source)
|
||||
.count()
|
||||
)
|
||||
assertTrue(handlerSource.contains("if (!item.showMore) return"))
|
||||
assertTrue(handlerSource.contains("CreatorCommunityPostMenuBottomSheetDialog("))
|
||||
assertTrue(handlerSource.contains("isFixed = item.isPinned"))
|
||||
assertTrue(handlerSource.contains("isCreator = item.isOwner"))
|
||||
assertTrue(handlerSource.contains("onClickPin = { updateCreatorChannelCommunityPostFixed(item) }"))
|
||||
assertTrue(handlerSource.contains("onClickDelete = { showDeleteCreatorChannelCommunityPostDialog(item) }"))
|
||||
assertTrue(handlerSource.contains("CreatorCommunityReportDialog("))
|
||||
assertTrue(handlerSource.contains("if (item.isOwner)"))
|
||||
assertFalse(handlerSource.contains("isCreatorChannelOwner()"))
|
||||
assertTrue(source.contains("private val communityModifyLauncher = registerForActivityResult"))
|
||||
assertTrue(source.contains("CommunityActivityResultSource.Modify"))
|
||||
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java"))
|
||||
assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 고정과 삭제 mutation은 역할과 중복을 재확인하고 성공만 목록을 갱신한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val pinSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem)",
|
||||
endMarker = "private fun showDeleteCreatorChannelCommunityPostDialog"
|
||||
)
|
||||
val deleteSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem)",
|
||||
endMarker = "private fun reportCreatorChannelCommunityPost"
|
||||
)
|
||||
val deleteDialogSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "private fun showDeleteCreatorChannelCommunityPostDialog",
|
||||
endMarker = "private fun deleteCreatorChannelCommunityPost"
|
||||
)
|
||||
|
||||
listOf(pinSource, deleteSource).forEach { mutationSource ->
|
||||
assertTrue(
|
||||
mutationSource.contains(
|
||||
"if (!item.isOwner || isCommunityPostMutating) return"
|
||||
)
|
||||
)
|
||||
assertFalse(mutationSource.contains("isCreatorChannelOwner()"))
|
||||
assertTrue(mutationSource.contains("isCommunityPostMutating = true"))
|
||||
assertEquals(2, "isCommunityPostMutating = false".toRegex().findAll(mutationSource).count())
|
||||
assertTrue(mutationSource.contains("if (response.success)"))
|
||||
assertTrue(mutationSource.contains("handleCommunityChange(CommunityChange.Updated)"))
|
||||
assertTrue(mutationSource.contains("showCommunityMutationFailureToast"))
|
||||
}
|
||||
assertTrue(
|
||||
pinSource.contains(
|
||||
"creatorCommunityRepository.updateCommunityPostFixed(item.postId, !item.isPinned, authToken())"
|
||||
)
|
||||
)
|
||||
assertTrue(deleteDialogSource.contains("if (!item.isOwner) return"))
|
||||
assertFalse(deleteDialogSource.contains("isCreatorChannelOwner()"))
|
||||
assertTrue(deleteSource.contains("ModifyCommunityPostRequest("))
|
||||
assertTrue(deleteSource.contains("creatorCommunityId = item.postId"))
|
||||
assertTrue(deleteSource.contains("isActive = false"))
|
||||
assertTrue(deleteSource.contains("Gson().toJson"))
|
||||
assertTrue(deleteSource.contains("toRequestBody(\"text/plain\".toMediaType())"))
|
||||
assertTrue(
|
||||
deleteSource.contains(
|
||||
"creatorCommunityRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 신고 mutation은 방문자 역할과 중복을 재확인하고 성공 토스트만 표시한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val reportSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "private fun reportCreatorChannelCommunityPost(",
|
||||
endMarker = "private fun showCommunityMutationFailureToast"
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
reportSource.contains(
|
||||
"if (item.isOwner || !item.showMore || reason.isBlank() || " +
|
||||
"isCommunityPostMutating) return"
|
||||
)
|
||||
)
|
||||
assertFalse(reportSource.contains("isCreatorChannelOwner()"))
|
||||
assertTrue(reportSource.contains("isCommunityPostMutating = true"))
|
||||
assertEquals(2, "isCommunityPostMutating = false".toRegex().findAll(reportSource).count())
|
||||
assertTrue(
|
||||
reportSource.contains(
|
||||
"creatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())"
|
||||
)
|
||||
)
|
||||
assertTrue(reportSource.contains("response.message.isNullOrBlank()"))
|
||||
assertTrue(reportSource.contains("R.string.character_comment_report_submitted"))
|
||||
assertTrue(reportSource.contains("showCommunityMutationFailureToast"))
|
||||
assertFalse(reportSource.contains("handleCommunityChange("))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() {
|
||||
val source = projectFile(
|
||||
@@ -653,80 +811,12 @@ class CreatorChannelActivitySourceTest {
|
||||
assertTrue(source.contains("iconResId = R.drawable.ic_new_upload_community_post"))
|
||||
assertTrue(source.contains("textResId = R.string.creator_channel_owner_fab_community"))
|
||||
assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()"))
|
||||
assertTrue(source.contains("private val communityPostModifyLauncher"))
|
||||
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java"))
|
||||
assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)"))
|
||||
assertTrue(source.contains("creatorCommunityRepository.updateCommunityPostFixed("))
|
||||
assertTrue(source.contains("isFixed = !item.isPinned"))
|
||||
assertTrue(source.contains("creatorCommunityRepository.modifyCommunityPost("))
|
||||
assertTrue(source.contains("isActive = false"))
|
||||
assertFalse(source.contains("communityPostModifyLauncher"))
|
||||
assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked"))
|
||||
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()"))
|
||||
assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked(postId: Long)"))
|
||||
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked(item.postId)"))
|
||||
assertFalse(source.contains("onClickPin = {},"))
|
||||
assertFalse(source.contains("onClickModify = {},"))
|
||||
assertFalse(source.contains("onClickDelete = {},"))
|
||||
assertTrue(fragment.contains("fun onCreatorChannelCommunityRefreshRequested()"))
|
||||
assertTrue(
|
||||
|
||||
fragment.contains(
|
||||
"CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }"
|
||||
)
|
||||
)
|
||||
assertTrue(fragment.contains("mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))"))
|
||||
assertTrue(fragment.contains("mediaPlayerManager?.stopContent()"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 고정 성공은 실제 post id로 단일 변경 handler를 호출한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val fixedSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "private fun updateCreatorChannelCommunityPostFixed",
|
||||
endMarker = "private fun deleteCreatorChannelCommunityPost"
|
||||
)
|
||||
val successMarker = "if (response.success)"
|
||||
val beforeSuccessSource = fixedSource.substring(0, fixedSource.indexOf(successMarker))
|
||||
val successSource = fixedSource.substring(
|
||||
fixedSource.indexOf(successMarker),
|
||||
fixedSource.indexOf("} else", fixedSource.indexOf(successMarker))
|
||||
)
|
||||
|
||||
assertTrue(fixedSource.contains("postId = item.postId"))
|
||||
assertTrue(fixedSource.contains("isFixed = !item.isPinned"))
|
||||
assertTrue(successSource.contains("handleCommunityChange(CommunityChange.PinChanged(item.postId))"))
|
||||
assertFalse(beforeSuccessSource.contains("CommunityChange.PinChanged"))
|
||||
assertEquals(1, fixedSource.split("CommunityChange.PinChanged").size - 1)
|
||||
assertFalse(fixedSource.contains("CommunityChange.PinChanged(creatorId)"))
|
||||
assertFalse(fixedSource.contains("refreshCreatorChannelCommunity()"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 삭제 성공은 실제 post id로 단일 변경 handler를 호출한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||
).readText()
|
||||
val deleteSource = sourceSection(
|
||||
source = source,
|
||||
startMarker = "private fun deleteCreatorChannelCommunityPost",
|
||||
endMarker = "private fun authToken()"
|
||||
)
|
||||
val successMarker = "if (response.success)"
|
||||
val beforeSuccessSource = deleteSource.substring(0, deleteSource.indexOf(successMarker))
|
||||
val successSource = deleteSource.substring(
|
||||
deleteSource.indexOf(successMarker),
|
||||
deleteSource.indexOf("} else", deleteSource.indexOf(successMarker))
|
||||
)
|
||||
|
||||
assertTrue(deleteSource.contains("creatorCommunityId = item.postId"))
|
||||
assertTrue(deleteSource.contains("isActive = false"))
|
||||
assertTrue(successSource.contains("handleCommunityChange(CommunityChange.Deleted(item.postId))"))
|
||||
assertFalse(beforeSuccessSource.contains("CommunityChange.Deleted"))
|
||||
assertEquals(1, deleteSource.split("CommunityChange.Deleted").size - 1)
|
||||
assertFalse(deleteSource.contains("CommunityChange.Deleted(creatorId)"))
|
||||
assertFalse(deleteSource.contains("refreshCreatorChannelCommunity()"))
|
||||
assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager"))
|
||||
assertFalse(fragment.contains("toggleCommunityAudio"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -743,18 +833,14 @@ class CreatorChannelActivitySourceTest {
|
||||
handlerSource.indexOf("CommunityChange.Created"),
|
||||
handlerSource.indexOf("CommunityChange.Updated")
|
||||
)
|
||||
val mutationSource = handlerSource.substring(handlerSource.indexOf("CommunityChange.Updated"))
|
||||
val updatedSource = handlerSource.substring(handlerSource.indexOf("CommunityChange.Updated"))
|
||||
|
||||
assertTrue(createdSource.contains("homeActionDelegate?.refreshHome()"))
|
||||
assertTrue(createdSource.contains("refreshCreatorChannelCommunity()"))
|
||||
assertTrue(
|
||||
mutationSource.contains(
|
||||
"CommunityChange.Updated,\n" +
|
||||
" is CommunityChange.Deleted,\n" +
|
||||
" is CommunityChange.PinChanged -> refreshCreatorChannelCommunity()"
|
||||
)
|
||||
)
|
||||
assertFalse(mutationSource.contains("homeActionDelegate?.refreshHome()"))
|
||||
assertTrue(updatedSource.contains("homeActionDelegate?.refreshHome()"))
|
||||
assertTrue(updatedSource.contains("refreshCreatorChannelCommunity()"))
|
||||
assertFalse(updatedSource.contains("CommunityChange.Deleted"))
|
||||
assertFalse(updatedSource.contains("CommunityChange.PinChanged"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2003,7 +2089,7 @@ class CreatorChannelActivitySourceTest {
|
||||
assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp("))
|
||||
assertTrue(adapter.contains("rootWidthDp = communityWidthDp"))
|
||||
assertTrue(adapter.contains("setHideEmptyTextRows(true)"))
|
||||
assertTrue(adapter.contains("bind(community.toFeedCommunityItem())"))
|
||||
assertTrue(adapter.contains("bind(community.toFeedCommunityItem(showMore = menuItem.showMore))"))
|
||||
assertTrue(adapter.contains("bindCommunityImages(row, community)"))
|
||||
assertTrue(adapter.contains("BlurTransformation(itemView.context, 25f, 2.5f)"))
|
||||
assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams("))
|
||||
|
||||
@@ -61,6 +61,16 @@ class CreatorChannelHomeMapperTest {
|
||||
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `현재 회원 id를 커뮤니티 section에 보존한다`() {
|
||||
val communities = response().toUiContent(currentMemberId = 77L)
|
||||
.sections
|
||||
.filterIsInstance<CreatorChannelHomeSection.Communities>()
|
||||
.single()
|
||||
|
||||
assertEquals(77L, communities.currentMemberId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 후원 empty와 팬Talk empty section을 생성한다`() {
|
||||
val content = response(
|
||||
@@ -267,7 +277,9 @@ class CreatorChannelHomeMapperTest {
|
||||
dateUtc = "2026-06-11T12:00:00Z",
|
||||
existOrdered = false,
|
||||
likeCount = 1,
|
||||
commentCount = 2
|
||||
commentCount = 2,
|
||||
isPinned = false,
|
||||
isCommentAvailable = true
|
||||
)
|
||||
|
||||
private fun fanTalk() = CreatorChannelFanTalkSummaryResponse(
|
||||
|
||||
@@ -4,7 +4,9 @@ import com.google.gson.Gson
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class CreatorChannelHomeModelsTest {
|
||||
@@ -36,6 +38,15 @@ class CreatorChannelHomeModelsTest {
|
||||
assertEquals("https://x.example", response.sns.xUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home response preserves community pinned and comment availability`() {
|
||||
val response = gson.fromJson(homeJson(scheduleType = "AUDIO"), CreatorChannelHomeResponse::class.java)
|
||||
|
||||
val community = response.communities.single()
|
||||
assertTrue(community.isPinned)
|
||||
assertFalse(community.isCommentAvailable)
|
||||
}
|
||||
|
||||
private fun homeJson(scheduleType: String): String = """
|
||||
{
|
||||
"creator": {
|
||||
@@ -63,7 +74,24 @@ class CreatorChannelHomeModelsTest {
|
||||
],
|
||||
"audioContents": [],
|
||||
"series": [],
|
||||
"communities": [],
|
||||
"communities": [
|
||||
{
|
||||
"postId": 21,
|
||||
"creatorId": 1,
|
||||
"creatorNickname": "creator",
|
||||
"creatorProfileUrl": "https://example.com/profile.png",
|
||||
"imageUrl": null,
|
||||
"audioUrl": null,
|
||||
"content": "community",
|
||||
"price": 0,
|
||||
"dateUtc": "2026-06-12T00:00:00Z",
|
||||
"existOrdered": false,
|
||||
"likeCount": 6,
|
||||
"commentCount": 5,
|
||||
"isPinned": true,
|
||||
"isCommentAvailable": false
|
||||
}
|
||||
],
|
||||
"fanTalk": {
|
||||
"totalCount": 0,
|
||||
"latestFanTalk": null
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
@@ -29,6 +30,7 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
||||
val reply = inflateView(R.layout.activity_creator_channel_community_reply)
|
||||
|
||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back))
|
||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_more))
|
||||
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header))
|
||||
assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments))
|
||||
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall))
|
||||
@@ -39,6 +41,9 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
|
||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_edit_cancel))
|
||||
assertRemovedId("tv_creator_channel_community_detail_title")
|
||||
assertRemovedId("layout_creator_channel_community_detail_price")
|
||||
assertRemovedId("tv_creator_channel_community_detail_price")
|
||||
assertRemovedId("tv_creator_channel_community_detail_purchased")
|
||||
assertRemovedId("tv_creator_channel_community_detail_comment_unavailable")
|
||||
|
||||
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back))
|
||||
@@ -156,6 +161,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
||||
assertTrue(detailActivity.contains("btn_audio_content_play"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `상세 게시글 더보기는 기존 관리 신고 UI와 수정 launcher를 재사용한다`() {
|
||||
val detailActivity = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/" +
|
||||
"CreatorChannelCommunityDetailActivity.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(detailActivity.contains("CreatorCommunityPostMenuBottomSheetDialog"))
|
||||
assertTrue(detailActivity.contains("CreatorCommunityReportDialog"))
|
||||
assertTrue(detailActivity.contains("CreatorCommunityModifyActivity"))
|
||||
assertTrue(detailActivity.contains("modifyLauncher"))
|
||||
assertTrue(detailActivity.contains("Constants.EXTRA_COMMUNITY_POST_ID"))
|
||||
assertTrue(detailActivity.contains("btnCreatorChannelCommunityDetailMore"))
|
||||
assertFalse(detailActivity.contains("layoutCreatorChannelCommunityDetailPrice"))
|
||||
assertFalse(detailActivity.contains("tvCreatorChannelCommunityDetailPurchased"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
|
||||
val commentAdapter = projectFile(
|
||||
|
||||
@@ -31,6 +31,8 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argThat
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.isNull
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
@@ -469,6 +471,99 @@ class CreatorChannelCommunityDetailViewModelTest {
|
||||
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `게시글 더보기는 작성자 또는 무료 또는 구매 완료일 때만 표시한다`() {
|
||||
SharedPreferenceManager.userId = 10L
|
||||
val cases = listOf(
|
||||
detailResponse(isCommentAvailable = false, creatorId = 10L, price = 300, existOrdered = false),
|
||||
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 0, existOrdered = false),
|
||||
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 300, existOrdered = true),
|
||||
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 300, existOrdered = false)
|
||||
)
|
||||
|
||||
val results = cases.map { response ->
|
||||
stubDetail(response)
|
||||
viewModel.loadDetail(POST_ID)
|
||||
(viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content).post
|
||||
}
|
||||
|
||||
assertTrue(results[0].isOwner)
|
||||
assertTrue(results[0].showMore)
|
||||
assertFalse(results[1].isOwner)
|
||||
assertTrue(results[1].showMore)
|
||||
assertTrue(results[2].showMore)
|
||||
assertFalse(results[3].showMore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `게시글 고정 성공은 반전 값을 요청하고 상세 재조회와 변경 이벤트를 노출한다`() {
|
||||
whenever(repository.getCommunityPostDetail(POST_ID, AUTH_TOKEN)).thenReturn(
|
||||
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = false, creatorId = 10L, isPinned = false), null)),
|
||||
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = false, creatorId = 10L, isPinned = true), null))
|
||||
)
|
||||
whenever(legacyRepository.updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN))
|
||||
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
||||
viewModel.loadDetail(POST_ID)
|
||||
|
||||
viewModel.updateCommunityPostFixed()
|
||||
|
||||
val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
|
||||
assertTrue(state.post.isPinned)
|
||||
assertTrue(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||
verify(legacyRepository).updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN)
|
||||
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `게시글 삭제 성공은 삭제 이벤트를 노출한다`() {
|
||||
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 10L))
|
||||
whenever(legacyRepository.modifyCommunityPost(isNull(), any(), eq(AUTH_TOKEN)))
|
||||
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
||||
viewModel.loadDetail(POST_ID)
|
||||
|
||||
viewModel.deleteCommunityPost()
|
||||
|
||||
assertTrue(viewModel.postDeletedEventLiveData.requireValue() == true)
|
||||
verify(legacyRepository).modifyCommunityPost(
|
||||
isNull(),
|
||||
argThat { requestBody ->
|
||||
okio.Buffer().also(requestBody::writeTo).readUtf8().contains("\"isActive\":false")
|
||||
},
|
||||
eq(AUTH_TOKEN)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `게시글 신고 성공은 repository 호출과 toast만 노출한다`() {
|
||||
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 999L))
|
||||
whenever(repository.reportCommunityPost(POST_ID, "스팸", AUTH_TOKEN))
|
||||
.thenReturn(Single.just(ApiResponse(true, Any(), "접수됨")))
|
||||
viewModel.loadDetail(POST_ID)
|
||||
|
||||
viewModel.reportCommunityPost("스팸")
|
||||
|
||||
verify(repository).reportCommunityPost(POST_ID, "스팸", AUTH_TOKEN)
|
||||
assertEquals("접수됨", viewModel.toastLiveData.requireValue()?.consume()?.message)
|
||||
assertFalse(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `게시글 mutation 진행 중에는 중복 요청을 막고 실패 시 성공 이벤트를 노출하지 않는다`() {
|
||||
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 10L))
|
||||
val subject = PublishSubject.create<ApiResponse<Any>>()
|
||||
whenever(legacyRepository.updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN))
|
||||
.thenReturn(subject.firstOrError())
|
||||
viewModel.loadDetail(POST_ID)
|
||||
|
||||
viewModel.updateCommunityPostFixed()
|
||||
viewModel.updateCommunityPostFixed()
|
||||
subject.onNext(ApiResponse(false, null, "고정 실패"))
|
||||
|
||||
verify(legacyRepository, times(1)).updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN)
|
||||
assertEquals("고정 실패", viewModel.toastLiveData.requireValue()?.consume()?.message)
|
||||
assertFalse(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() {
|
||||
stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L)))
|
||||
@@ -571,7 +666,8 @@ class CreatorChannelCommunityDetailViewModelTest {
|
||||
existOrdered: Boolean = true,
|
||||
creatorId: Long = 100L,
|
||||
commentIds: List<Long> = emptyList(),
|
||||
commentsHasNext: Boolean = false
|
||||
commentsHasNext: Boolean = false,
|
||||
isPinned: Boolean = false
|
||||
) = CreatorChannelCommunityPostDetailResponse(
|
||||
postId = POST_ID,
|
||||
creatorId = creatorId,
|
||||
@@ -587,7 +683,7 @@ class CreatorChannelCommunityDetailViewModelTest {
|
||||
likeCount = likeCount,
|
||||
commentCount = 2,
|
||||
isLiked = isLiked,
|
||||
isPinned = false,
|
||||
isPinned = isPinned,
|
||||
comments = commentsResponse(ids = commentIds, hasNext = commentsHasNext)
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -62,7 +63,7 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 재생 반응 owner 영역을 제공한다`() {
|
||||
fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 반응과 작성자 금액을 제공한다`() {
|
||||
val item = inflateView(R.layout.item_creator_channel_community_list)
|
||||
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_list.xml").readText()
|
||||
val imageContainer = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_image_container))
|
||||
@@ -77,24 +78,29 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
||||
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_image))
|
||||
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_locked_overlay))
|
||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price))
|
||||
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_play))
|
||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count))
|
||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count))
|
||||
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions))
|
||||
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price))
|
||||
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_owner_more))
|
||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price))
|
||||
val moreButton = requireNotNull(item.findViewById<View>(R.id.btn_creator_channel_community_list_more))
|
||||
assertSame(imageContainer, (lockIcon.parent as View).parent)
|
||||
assertSame(imageContainer, (lockedPrice.parent as View).parent)
|
||||
assertSame(imageContainer, ((lockedPrice.parent as View).parent as View).parent)
|
||||
assertSame(
|
||||
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
|
||||
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price).parent
|
||||
)
|
||||
assertSame(
|
||||
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
|
||||
moreButton.parent
|
||||
)
|
||||
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\""))
|
||||
assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\""))
|
||||
assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\""))
|
||||
assertTrue(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||
assertTrue(itemLayout.contains("android:id=\"@+id/iv_creator_channel_community_list_play\""))
|
||||
assertFalse(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||
assertTrue(itemLayout.contains("android:src=\"@drawable/ic_bar_cash\""))
|
||||
assertFalse(itemLayout.contains("iv_creator_channel_community_list_play"))
|
||||
assertFalse(itemLayout.contains("iv_creator_channel_community_list_owner_more"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -138,7 +144,7 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `커뮤니티 fragment source는 pagination view mode owner padding stopContent 계약을 사용한다`() {
|
||||
fun `커뮤니티 fragment source는 pagination view mode owner padding과 상세 이동 계약을 사용한다`() {
|
||||
val fragment = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
|
||||
).readText()
|
||||
@@ -161,8 +167,10 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
||||
assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize("))
|
||||
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
|
||||
assertTrue(fragment.contains("applyOwnerCtaPadding"))
|
||||
assertTrue(fragment.contains("pauseContent"))
|
||||
assertTrue(fragment.contains("stopContent"))
|
||||
assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager"))
|
||||
assertFalse(fragment.contains("toggleCommunityAudio"))
|
||||
assertFalse(fragment.contains("onPlayClick"))
|
||||
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -181,19 +189,25 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
||||
assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment"))
|
||||
assertTrue(listAdapter.contains("isLocked"))
|
||||
assertTrue(gridAdapter.contains("isLocked"))
|
||||
assertTrue(listAdapter.contains("showPlayButton"))
|
||||
assertTrue(listAdapter.contains("showOwnerMore"))
|
||||
assertTrue(listAdapter.contains("isPlayingContent"))
|
||||
assertTrue(listAdapter.contains("R.drawable.ic_player_pause"))
|
||||
assertTrue(listAdapter.contains("R.drawable.ic_new_player_play"))
|
||||
assertTrue(listAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
|
||||
assertTrue(listAdapter.contains("private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}"))
|
||||
assertTrue(listAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
|
||||
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }"))
|
||||
assertTrue(listAdapter.contains("onOwnerMoreClick(item)"))
|
||||
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }"))
|
||||
assertTrue(
|
||||
listAdapter.contains(
|
||||
"btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) }"
|
||||
)
|
||||
)
|
||||
assertTrue(listAdapter.contains("item.showOwnerTopPrice || item.menuItem.showMore"))
|
||||
assertFalse(listAdapter.contains("onPlayClick"))
|
||||
assertFalse(listAdapter.contains("onOwnerMoreClick"))
|
||||
assertFalse(listAdapter.contains("isPlayingContent"))
|
||||
assertFalse(listAdapter.contains("showPlayButton"))
|
||||
assertFalse(listAdapter.contains("showOwnerMore"))
|
||||
assertTrue(gridAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
|
||||
assertTrue(gridAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
|
||||
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
|
||||
assertFalse(gridAdapter.contains("onMoreClick"))
|
||||
assertFalse(gridAdapter.contains("CreatorChannelCommunityMenuItem"))
|
||||
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
|
||||
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
|
||||
assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice"))
|
||||
assertTrue(gridAdapter.contains(".asBitmap()"))
|
||||
@@ -211,8 +225,17 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
||||
).readText()
|
||||
|
||||
assertTrue(fragment.contains("onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }"))
|
||||
assertTrue(fragment.contains("onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) }"))
|
||||
assertTrue(fragment.contains("private val gridAdapter = CreatorChannelCommunityGridAdapter("))
|
||||
assertTrue(fragment.contains("fun onCreatorChannelCommunityPostClicked(postId: Long)"))
|
||||
assertTrue(fragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"))
|
||||
assertFalse(fragment.contains("onPlayClick"))
|
||||
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked"))
|
||||
assertFalse(
|
||||
fragment.substringAfter("private val gridAdapter = CreatorChannelCommunityGridAdapter(")
|
||||
.substringBefore(")\n private var currentContentState")
|
||||
.contains("onMoreClick")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -66,7 +66,7 @@ class CreatorChannelCommunityMapperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `유료 미구매 타인 게시글은 잠금 상태이고 play button을 숨긴다`() {
|
||||
fun `유료 미구매 타인 게시글은 잠금 상태이고 이미지를 숨긴다`() {
|
||||
val item = listOf(
|
||||
communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3")
|
||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single()
|
||||
@@ -74,7 +74,6 @@ class CreatorChannelCommunityMapperTest {
|
||||
assertTrue(item.isLocked)
|
||||
assertNull(item.imageUrl)
|
||||
assertEquals(CreatorChannelCommunityImageMode.LockedGray, item.imageMode)
|
||||
assertFalse(item.showPlayButton)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,7 +92,7 @@ class CreatorChannelCommunityMapperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `고정 게시글 여부는 owner more 정책에서 사용할 수 있도록 UI 모델에 보존한다`() {
|
||||
fun `고정 게시글 여부는 UI 모델에 보존한다`() {
|
||||
val pinnedItem = listOf(communityPost(isPinned = true))
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
|
||||
.single()
|
||||
@@ -108,33 +107,32 @@ class CreatorChannelCommunityMapperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `본인 또는 구매한 사용자는 이미지와 오디오가 있으면 play button을 표시한다`() {
|
||||
val ownerItem = listOf(
|
||||
communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3")
|
||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L).single()
|
||||
val orderedItem = listOf(
|
||||
communityPost(price = 100, existOrdered = true, imageUrl = "image.png", audioUrl = "audio.mp3")
|
||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single()
|
||||
fun `메뉴 권한은 작성자 무료 구매 여부에 따라 계산하고 고정 상태를 보존한다`() {
|
||||
val authorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false, isPinned = true))
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
|
||||
.single()
|
||||
val visitorFree = listOf(communityPost(price = 0, existOrdered = false))
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||
.single()
|
||||
val visitorPaidOrdered = listOf(communityPost(price = 100, existOrdered = true))
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||
.single()
|
||||
val visitorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false))
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||
.single()
|
||||
|
||||
assertFalse(ownerItem.isLocked)
|
||||
assertTrue(ownerItem.showPlayButton)
|
||||
assertFalse(orderedItem.isLocked)
|
||||
assertTrue(orderedItem.showPlayButton)
|
||||
assertTrue(authorPaidNotOrdered.menuItem.isOwner)
|
||||
assertTrue(authorPaidNotOrdered.menuItem.showMore)
|
||||
assertTrue(authorPaidNotOrdered.menuItem.isPinned)
|
||||
assertTrue(authorPaidNotOrdered.isPinned)
|
||||
assertTrue(visitorFree.menuItem.showMore)
|
||||
assertTrue(visitorPaidOrdered.menuItem.showMore)
|
||||
assertFalse(visitorPaidNotOrdered.menuItem.isOwner)
|
||||
assertFalse(visitorPaidNotOrdered.menuItem.showMore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `오디오 또는 이미지가 없으면 play button을 숨긴다`() {
|
||||
val items = listOf(
|
||||
communityPost(postId = 1L, imageUrl = null, audioUrl = "audio.mp3"),
|
||||
communityPost(postId = 2L, imageUrl = "image.png", audioUrl = null),
|
||||
communityPost(postId = 3L, imageUrl = "image.png", audioUrl = " ")
|
||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||
|
||||
assertEquals(listOf(false, false, false), items.map { it.showPlayButton })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `본인 채널에 본인이 쓴 게시글에서만 owner more와 유료 top price를 표시한다`() {
|
||||
fun `본인 채널에 본인이 쓴 유료 게시글에서만 top price를 표시한다`() {
|
||||
val ownerPaid = listOf(communityPost(price = 100, creatorId = 10L))
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
|
||||
.single()
|
||||
@@ -148,13 +146,9 @@ class CreatorChannelCommunityMapperTest {
|
||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
|
||||
.single()
|
||||
|
||||
assertTrue(ownerPaid.showOwnerMore)
|
||||
assertTrue(ownerPaid.showOwnerTopPrice)
|
||||
assertTrue(ownerFree.showOwnerMore)
|
||||
assertFalse(ownerFree.showOwnerTopPrice)
|
||||
assertFalse(otherCreator.showOwnerMore)
|
||||
assertFalse(otherCreator.showOwnerTopPrice)
|
||||
assertFalse(otherChannel.showOwnerMore)
|
||||
assertFalse(otherChannel.showOwnerTopPrice)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.home
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.text.style.ClickableSpan
|
||||
@@ -659,6 +658,19 @@ class HomeMainFragmentLayoutTest {
|
||||
assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `popular community mapper preserves locked item reactions`() {
|
||||
val item = popularCommunityData(
|
||||
audioUrl = null,
|
||||
price = 100,
|
||||
existOrdered = false
|
||||
).toUiModel().item
|
||||
|
||||
assertTrue(item.isLocked)
|
||||
assertTrue(item.showReaction)
|
||||
assertTrue(item.showComment)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home relative time formatter converts active creator utc timestamp to relative time text`() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
@@ -1509,7 +1521,9 @@ class HomeMainFragmentLayoutTest {
|
||||
|
||||
private fun popularCommunityData(
|
||||
audioUrl: String?,
|
||||
createdAt: String = "2분 전"
|
||||
createdAt: String = "2분 전",
|
||||
price: Int = 0,
|
||||
existOrdered: Boolean = false
|
||||
): HomePopularCommunityPostItem {
|
||||
return HomePopularCommunityPostItem(
|
||||
postId = 1L,
|
||||
@@ -1519,11 +1533,11 @@ class HomeMainFragmentLayoutTest {
|
||||
imageUrl = null,
|
||||
audioUrl = audioUrl,
|
||||
content = "본문",
|
||||
price = 0,
|
||||
price = price,
|
||||
createdAt = createdAt,
|
||||
likeCount = 6L,
|
||||
commentCount = 5L,
|
||||
existOrdered = false
|
||||
existOrdered = existOrdered
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import org.junit.Test
|
||||
|
||||
@@ -102,4 +104,71 @@ class FeedItemTest {
|
||||
assertEquals(5, item.commentCount)
|
||||
assertEquals(6, item.likeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `locked paid community item hides reactions and comments`() {
|
||||
val item = communityItem(
|
||||
price = 100,
|
||||
existOrdered = false,
|
||||
hideReactionWhenLocked = true
|
||||
)
|
||||
|
||||
assertTrue(item.isLocked)
|
||||
assertFalse(item.showReaction)
|
||||
assertFalse(item.showComment)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `locked paid community item preserves reactions by default`() {
|
||||
val item = communityItem(price = 100, existOrdered = false)
|
||||
|
||||
assertTrue(item.isLocked)
|
||||
assertTrue(item.showReaction)
|
||||
assertTrue(item.showComment)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `free community item with comments disabled keeps likes only`() {
|
||||
val item = communityItem(price = 0, isCommentAvailable = false)
|
||||
|
||||
assertFalse(item.isLocked)
|
||||
assertTrue(item.showReaction)
|
||||
assertFalse(item.showComment)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community item preserves pinned state`() {
|
||||
val item = communityItem(isPinned = true)
|
||||
|
||||
assertTrue(item.isPinned)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community item hides more action by default`() {
|
||||
assertFalse(communityItem().showMore)
|
||||
}
|
||||
|
||||
private fun communityItem(
|
||||
price: Int = 0,
|
||||
existOrdered: Boolean = false,
|
||||
isPinned: Boolean = false,
|
||||
isCommentAvailable: Boolean = true,
|
||||
hideReactionWhenLocked: Boolean = false
|
||||
) = FeedItem.Community(
|
||||
feedId = "feed-community-policy",
|
||||
creatorId = "creator-1",
|
||||
creatorName = "크리에이터 이름",
|
||||
creatorImageUrl = "https://example.com/profile.png",
|
||||
postId = "post-policy",
|
||||
bodyText = "커뮤니티 본문",
|
||||
keywordText = "",
|
||||
createdAtText = "2분 전",
|
||||
commentCount = 5,
|
||||
likeCount = 6,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
isPinned = isPinned,
|
||||
isCommentAvailable = isCommentAvailable,
|
||||
hideReactionWhenLocked = hideReactionWhenLocked
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,28 @@ class FeedViewTest {
|
||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community more click은 현재 bind된 item만 전달한다`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
val first = sampleCommunityItem(bodyText = "첫 번째", keywordText = "", showMore = true)
|
||||
val second = sampleCommunityItem(bodyText = "두 번째", keywordText = "", showMore = true)
|
||||
var clickedItem: FeedItem.Community? = null
|
||||
var rootClickedItem: FeedItem? = null
|
||||
|
||||
view.setOnMoreClick { clickedItem = it }
|
||||
view.setOnFeedClick { rootClickedItem = it }
|
||||
view.bind(first)
|
||||
view.bind(second)
|
||||
view.findViewById<View>(R.id.btn_feed_community_more).performClick()
|
||||
|
||||
assertSame(second, clickedItem)
|
||||
assertEquals(null, rootClickedItem)
|
||||
|
||||
view.performClick()
|
||||
|
||||
assertSame(second, rootClickedItem)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community layout uses runtime image height instead of fixed crop height`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
@@ -201,7 +223,8 @@ class FeedViewTest {
|
||||
audioUrl: String? = null,
|
||||
price: Int = 0,
|
||||
existOrdered: Boolean = false,
|
||||
showKeyword: Boolean = true
|
||||
showKeyword: Boolean = true,
|
||||
showMore: Boolean = false
|
||||
) = FeedItem.Community(
|
||||
feedId = "feed-community-1",
|
||||
creatorId = "creator-1",
|
||||
@@ -217,7 +240,8 @@ class FeedViewTest {
|
||||
audioUrl = audioUrl,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
showKeyword = showKeyword
|
||||
showKeyword = showKeyword,
|
||||
showMore = showMore
|
||||
)
|
||||
|
||||
private fun Int.dpToPx(): Int {
|
||||
|
||||
Reference in New Issue
Block a user