feat(creator-channel): 커뮤니티 답글 상태 관리를 추가한다
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
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.v2.creator.channel.CreatorChannelEvent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityRepliesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
|
||||
class CreatorChannelCommunityReplyViewModel(
|
||||
private val repository: CreatorChannelRepository,
|
||||
private val legacyRepository: CreatorCommunityRepository,
|
||||
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>()
|
||||
val replyStateLiveData: LiveData<CreatorChannelCommunityReplyUiState>
|
||||
get() = _replyStateLiveData
|
||||
|
||||
private val _replyWrittenEventLiveData = MutableLiveData<Boolean>()
|
||||
val replyWrittenEventLiveData: LiveData<Boolean>
|
||||
get() = _replyWrittenEventLiveData
|
||||
|
||||
private val _replyChangedEventLiveData = MutableLiveData<Boolean>()
|
||||
val replyChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _replyChangedEventLiveData
|
||||
|
||||
private val _parentCommentChangedEventLiveData = MutableLiveData<Boolean>()
|
||||
val parentCommentChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _parentCommentChangedEventLiveData
|
||||
|
||||
private val _parentCommentDeletedEventLiveData = MutableLiveData<Boolean>()
|
||||
val parentCommentDeletedEventLiveData: LiveData<Boolean>
|
||||
get() = _parentCommentDeletedEventLiveData
|
||||
|
||||
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
||||
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var postId: Long = 0L
|
||||
private var parentComment: CreatorChannelCommunityCommentUiModel? = null
|
||||
private var replyRequestGeneration = 0
|
||||
private var isLoadingReplies = false
|
||||
private var isSendingReply = false
|
||||
private var isModifyingReply = false
|
||||
|
||||
fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) {
|
||||
val formattedParentComment = parentComment.copy(
|
||||
createdAtText = relativeTimeTextFormatter.format(parentComment.createdAtUtc)
|
||||
)
|
||||
this.postId = postId
|
||||
this.parentComment = formattedParentComment
|
||||
replyRequestGeneration++
|
||||
isLoadingReplies = false
|
||||
_replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content(
|
||||
parentComment = formattedParentComment,
|
||||
replies = emptyList(),
|
||||
replyPage = FIRST_PAGE,
|
||||
hasNextReply = false
|
||||
)
|
||||
if (formattedParentComment.commentId > 0) {
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreReplies() {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
if (!content.hasNextReply || isLoadingReplies) return
|
||||
|
||||
loadReplyPage(page = content.replyPage + 1, append = true)
|
||||
}
|
||||
|
||||
fun updateReplyInput(input: String) {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
_replyStateLiveData.value = content.copy(replyInput = input)
|
||||
}
|
||||
|
||||
fun submitReply() {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
val comment = content.replyInput.trim()
|
||||
val commentId = content.parentComment.commentId
|
||||
if (postId <= 0 || commentId <= 0 || comment.isBlank() || isSendingReply) return
|
||||
|
||||
isSendingReply = true
|
||||
_replyStateLiveData.value = content.copy(isSendingReply = true)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.registerComment(
|
||||
postId = postId,
|
||||
comment = comment,
|
||||
parentId = commentId,
|
||||
isSecret = false,
|
||||
token = authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleWriteResponse(response) },
|
||||
{
|
||||
isSendingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
showMutationFailureToast()
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun consumeReplyWrittenEvent() {
|
||||
_replyWrittenEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumeReplyChangedEvent() {
|
||||
_replyChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumeParentCommentChangedEvent() {
|
||||
_parentCommentChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumeParentCommentDeletedEvent() {
|
||||
_parentCommentDeletedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun modifyReply(replyId: Long, comment: String) {
|
||||
val trimmedComment = comment.trim()
|
||||
if (replyId <= 0 || isModifyingReply) return
|
||||
if (trimmedComment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
|
||||
modifyComment(ModifyCommentRequest(commentId = replyId, comment = trimmedComment))
|
||||
}
|
||||
|
||||
fun deleteReply(replyId: Long) {
|
||||
if (replyId <= 0 || isModifyingReply) return
|
||||
|
||||
modifyComment(ModifyCommentRequest(commentId = replyId, isActive = false))
|
||||
}
|
||||
|
||||
fun modifyParentComment(comment: String) {
|
||||
val parentComment = parentComment ?: return
|
||||
val trimmedComment = comment.trim()
|
||||
if (parentComment.commentId <= 0 || isModifyingReply) return
|
||||
if (trimmedComment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
|
||||
modifyParentComment(parentComment, trimmedComment)
|
||||
}
|
||||
|
||||
fun deleteParentComment() {
|
||||
val parentComment = parentComment ?: return
|
||||
if (parentComment.commentId <= 0 || isModifyingReply) return
|
||||
|
||||
deleteParentComment(parentComment.commentId)
|
||||
}
|
||||
|
||||
private fun modifyParentComment(parentComment: CreatorChannelCommunityCommentUiModel, comment: String) {
|
||||
isModifyingReply = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(
|
||||
ModifyCommentRequest(commentId = parentComment.commentId, comment = comment),
|
||||
authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
if (response.success) {
|
||||
val updatedParent = parentComment.copy(comment = comment)
|
||||
this.parentComment = updatedParent
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
_replyStateLiveData.value = current.copy(parentComment = updatedParent)
|
||||
_parentCommentChangedEventLiveData.value = true
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteParentComment(commentId: Long) {
|
||||
isModifyingReply = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false), authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
if (response.success) {
|
||||
_parentCommentDeletedEventLiveData.value = true
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadReplyPage(page: Int, append: Boolean) {
|
||||
val commentId = parentComment?.commentId ?: return
|
||||
if (commentId <= 0 || isLoadingReplies && append) return
|
||||
val generation = if (append) {
|
||||
replyRequestGeneration
|
||||
} else {
|
||||
++replyRequestGeneration
|
||||
}
|
||||
|
||||
isLoadingReplies = true
|
||||
val beforeRequest = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
if (beforeRequest != null) {
|
||||
_replyStateLiveData.value = beforeRequest.copy(isLoadingReplies = true)
|
||||
}
|
||||
compositeDisposable.add(
|
||||
repository.getCommunityCommentReplies(commentId, page, DEFAULT_PAGE_SIZE, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleRepliesResponse(response, append, generation) },
|
||||
{
|
||||
if (generation != replyRequestGeneration) return@subscribe
|
||||
|
||||
isLoadingReplies = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
_replyStateLiveData.value = current.copy(isLoadingReplies = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleRepliesResponse(
|
||||
response: ApiResponse<CreatorChannelCommunityRepliesResponse>,
|
||||
append: Boolean,
|
||||
generation: Int
|
||||
) {
|
||||
if (generation != replyRequestGeneration) return
|
||||
|
||||
isLoadingReplies = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
val data = response.data
|
||||
if (!response.success || data == null) {
|
||||
_replyStateLiveData.value = current.copy(isLoadingReplies = false)
|
||||
return
|
||||
}
|
||||
|
||||
val nextReplies = data.replies.map { it.toUiModel() }
|
||||
_replyStateLiveData.value = current.copy(
|
||||
replies = if (append) current.replies + nextReplies else nextReplies,
|
||||
replyPage = data.page,
|
||||
hasNextReply = data.hasNext,
|
||||
isLoadingReplies = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleWriteResponse(response: ApiResponse<Any>) {
|
||||
isSendingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
if (response.success) {
|
||||
_replyWrittenEventLiveData.value = true
|
||||
_replyChangedEventLiveData.value = true
|
||||
_replyStateLiveData.value = current.copy(replyInput = "", isSendingReply = false)
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun modifyComment(request: ModifyCommentRequest) {
|
||||
val commentId = parentComment?.commentId ?: return
|
||||
if (commentId <= 0) return
|
||||
|
||||
isModifyingReply = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(request, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
if (response.success) {
|
||||
_replyChangedEventLiveData.value = true
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMutationFailureToast(message: String? = null) {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
if (message.isNullOrBlank()) {
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
} else {
|
||||
ToastMessage(message = message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBlankCommentToast() {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
ToastMessage(resId = R.string.screen_creator_community_write_content_hint)
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityReplyResponse.toUiModel() = CreatorChannelCommunityReplyUiModel(
|
||||
replyId = commentId,
|
||||
commentId = parentComment?.commentId ?: 0L,
|
||||
memberId = writerId,
|
||||
memberNickname = writerNickname,
|
||||
memberProfileUrl = writerProfileImageUrl,
|
||||
createdAtUtc = createdAtUtc,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
comment = content,
|
||||
likeCount = 0,
|
||||
isLiked = false,
|
||||
isMine = writerId == SharedPreferenceManager.userId,
|
||||
isCreatorOwner = parentComment?.isCreatorOwner == true
|
||||
)
|
||||
|
||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PAGE_SIZE = 20
|
||||
private const val FIRST_PAGE = 0
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface CreatorChannelCommunityReplyUiState {
|
||||
data class Content(
|
||||
val parentComment: CreatorChannelCommunityCommentUiModel,
|
||||
val replies: List<CreatorChannelCommunityReplyUiModel>,
|
||||
val replyPage: Int,
|
||||
val hasNextReply: Boolean,
|
||||
val isLoadingReplies: Boolean = false,
|
||||
val replyInput: String = "",
|
||||
val isSendingReply: Boolean = false
|
||||
) : CreatorChannelCommunityReplyUiState
|
||||
}
|
||||
Reference in New Issue
Block a user