Files
sodalive-ios/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift

512 lines
20 KiB
Swift

import Foundation
import Combine
import Moya
final class CreatorChannelReplyDetailViewModel: ObservableObject {
private let communityRepository = CreatorCommunityRepository()
private let v2CommunityRepository = CreatorChannelCommunityRepository()
private let explorerRepository = ExplorerRepository()
private let fanTalkRepository = CreatorChannelFanTalkRepository()
private var subscription = Set<AnyCancellable>()
private var context: CreatorChannelReplyDetailContext?
private var onFanTalkRefresh: (() -> Void)?
private var onCommunityRefresh: (() -> Void)?
private var communityReplyPage = 0
private let communityReplyPageSize = 20
private var communityReplyTotalCount = 0
private var isCommunityReplyLast = false
private let fanTalkReplyPageSize = 20
@Published var replyText = ""
@Published var parentItem: CreatorChannelReplyDetailDisplayItem?
@Published var replies = [CreatorChannelReplyDetailDisplayItem]()
@Published var isLoading = false
@Published var isLoadingNextPage = false
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var selectedActionItem: CreatorChannelReplyDetailDisplayItem?
@Published var editingItem: CreatorChannelReplyDetailDisplayItem?
@Published var deletingItem: CreatorChannelReplyDetailDisplayItem?
@Published var isShowDeleteDialog = false
var isSendEnabled: Bool {
replyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
var isEditing: Bool {
editingItem != nil
}
func configure(context: CreatorChannelReplyDetailContext, onFanTalkRefresh: (() -> Void)?, onCommunityRefresh: (() -> Void)? = nil) {
guard self.context == nil else { return }
self.context = context
self.onFanTalkRefresh = onFanTalkRefresh
self.onCommunityRefresh = onCommunityRefresh
switch context {
case .community(_, _, let parentComment):
parentItem = CreatorChannelReplyDetailDisplayItem(communityComment: parentComment)
fetchCommunityReplies(reset: true)
case .communityPostDetail(let creatorId, _, let parentComment):
parentItem = CreatorChannelReplyDetailDisplayItem(communityPostDetail: parentComment, creatorId: creatorId)
fetchCommunityReplies(reset: true)
case .fanTalk(let creatorId, let parentFanTalk):
parentItem = CreatorChannelReplyDetailDisplayItem(fanTalk: parentFanTalk)
replies = parentFanTalk.creatorReplies.map {
CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId)
}
}
}
func canEdit(_ item: CreatorChannelReplyDetailDisplayItem) -> Bool {
switch item.source {
case .community:
return item.isMine
case .fanTalk:
guard item.id > 0 else { return false }
return item.isMine || item.isCreatorReply
}
}
func canDelete(_ item: CreatorChannelReplyDetailDisplayItem) -> Bool {
switch item.source {
case .community:
if item.isMine { return true }
if case .community(let creatorId, _, _) = context {
return creatorId == UserDefaults.int(forKey: .userId)
}
if case .communityPostDetail(let creatorId, _, _) = context {
return creatorId == UserDefaults.int(forKey: .userId)
}
return false
case .fanTalk:
guard item.id > 0 else { return false }
if item.isMine || item.isCreatorReply { return true }
if case .fanTalk(let creatorId, _) = context {
return creatorId == UserDefaults.int(forKey: .userId)
}
return false
}
}
func selectActionItem(_ item: CreatorChannelReplyDetailDisplayItem) {
selectedActionItem = selectedActionItem?.id == item.id ? nil : item
}
func beginEditing(item: CreatorChannelReplyDetailDisplayItem) {
guard canEdit(item) else { return }
selectedActionItem = nil
editingItem = item
replyText = item.content
}
func cancelEditing() {
editingItem = nil
replyText = ""
}
func presentDeleteDialog(item: CreatorChannelReplyDetailDisplayItem) {
guard canDelete(item) else { return }
selectedActionItem = nil
deletingItem = item
isShowDeleteDialog = true
}
func send() {
guard isSendEnabled, isLoading == false else { return }
if let editingItem {
modify(item: editingItem)
} else {
createReply()
}
}
func confirmDelete() {
guard let deletingItem, isLoading == false else { return }
isShowDeleteDialog = false
switch deletingItem.source {
case .community:
deleteCommunity(item: deletingItem)
case .fanTalk:
deleteFanTalk(item: deletingItem)
}
}
func fetchCommunityReplies(reset: Bool = false) {
guard isCommunityContext else { return }
guard isLoading == false, isLoadingNextPage == false else { return }
if reset {
communityReplyPage = isCommunityPostDetailContext ? 0 : 1
communityReplyTotalCount = 0
isCommunityReplyLast = false
} else if isCommunityReplyLast {
return
}
if reset {
isLoading = true
} else {
isLoadingNextPage = true
}
let requestPage = communityReplyPage
let publisher = communityRepliesPublisher(page: requestPage)
publisher
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
guard let self else { return }
self.isLoading = false
self.isLoadingNextPage = false
self.handleCommunityRepliesResponse(response, requestPage: requestPage, reset: reset)
}
.store(in: &subscription)
}
func fetchNextCommunityRepliesIfNeeded(currentItem: CreatorChannelReplyDetailDisplayItem) {
guard currentItem == replies.last else { return }
fetchCommunityReplies()
}
private func createReply() {
let content = replyText.trimmingCharacters(in: .whitespacesAndNewlines)
switch context {
case .community(_, let postId, let parentComment):
isLoading = true
communityRepository.createCommunityPostComment(
comment: content,
postId: postId,
parentId: parentComment.id,
isSecret: parentComment.isSecret
)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleCommunityMutationResponse(response)
}
.store(in: &subscription)
case .communityPostDetail(_, let postId, let parentComment):
isLoading = true
communityRepository.createCommunityPostComment(
comment: content,
postId: postId,
parentId: parentComment.commentId,
isSecret: false
)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleCommunityMutationResponse(response)
}
.store(in: &subscription)
case .fanTalk(let creatorId, let parentFanTalk):
isLoading = true
explorerRepository.writeCheers(parentCheersId: parentFanTalk.fanTalkId, creatorId: creatorId, content: content)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleFanTalkMutationResponse(response)
}
.store(in: &subscription)
case .none:
return
}
}
private func modify(item: CreatorChannelReplyDetailDisplayItem) {
let content = replyText.trimmingCharacters(in: .whitespacesAndNewlines)
guard content.isEmpty == false, content != item.content else {
errorMessage = I18n.Explorer.noChanges
isShowPopup = true
return
}
switch item.source {
case .community:
isLoading = true
var request = ModifyCommunityPostCommentRequest(commentId: item.id)
request.comment = content
communityRepository.modifyComment(request: request)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleCommunityMutationResponse(response)
}
.store(in: &subscription)
case .fanTalk:
guard item.id > 0 else { return }
isLoading = true
explorerRepository.modifyCheers(cheersId: item.id, content: content, isActive: nil)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleFanTalkMutationResponse(response)
}
.store(in: &subscription)
}
}
private func deleteCommunity(item: CreatorChannelReplyDetailDisplayItem) {
isLoading = true
var request = ModifyCommunityPostCommentRequest(commentId: item.id)
request.isActive = false
communityRepository.modifyComment(request: request)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleCommunityMutationResponse(response)
}
.store(in: &subscription)
}
private func deleteFanTalk(item: CreatorChannelReplyDetailDisplayItem) {
guard item.id > 0 else { return }
isLoading = true
explorerRepository.modifyCheers(cheersId: item.id, content: nil, isActive: false)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
self?.handleFanTalkMutationResponse(response)
}
.store(in: &subscription)
}
private func fetchFanTalkReplies(page: Int = 0) {
guard case .fanTalk(let creatorId, let parentFanTalk) = context else { return }
fanTalkRepository.getFanTalks(creatorId: creatorId, page: page, size: fanTalkReplyPageSize)
.sink { [weak self] result in
guard let self else { return }
if case .failure(let error) = result {
ERROR_LOG(error.localizedDescription)
self.applyFailureState()
}
} receiveValue: { [weak self] response in
guard let self else { return }
do {
let decoded = try JSONDecoder().decode(ApiResponse<CreatorChannelFanTalkTabResponse>.self, from: response.data)
if let data = decoded.data, decoded.success {
if let fanTalk = data.fanTalks.first(where: { $0.fanTalkId == parentFanTalk.fanTalkId }) {
self.isLoading = false
self.replies = fanTalk.creatorReplies.map {
CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId)
}
} else if data.hasNext {
self.fetchFanTalkReplies(page: page + 1)
} else {
self.isLoading = false
}
} else {
self.isLoading = false
self.errorMessage = decoded.message ?? I18n.Common.commonError
self.isShowPopup = true
}
} catch {
ERROR_LOG(error.localizedDescription)
self.isLoading = false
self.errorMessage = I18n.Common.commonError
self.isShowPopup = true
}
}
.store(in: &subscription)
}
private func handleCommunityMutationResponse(_ response: Moya.Response) {
isLoading = false
do {
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
if decoded.success {
applyLocalSuccessState()
onCommunityRefresh?()
fetchCommunityReplies(reset: true)
} else {
errorMessage = decoded.message ?? I18n.Common.commonError
isShowPopup = true
}
} catch {
ERROR_LOG(error.localizedDescription)
errorMessage = I18n.Common.commonError
isShowPopup = true
}
}
private func handleFanTalkMutationResponse(_ response: Moya.Response) {
isLoading = false
do {
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
if decoded.success {
applyLocalSuccessState()
onFanTalkRefresh?()
fetchFanTalkReplies()
} else {
errorMessage = decoded.message ?? I18n.Common.commonError
isShowPopup = true
}
} catch {
ERROR_LOG(error.localizedDescription)
errorMessage = I18n.Common.commonError
isShowPopup = true
}
}
private func applyLocalSuccessState() {
let trimmedReplyText = replyText.trimmingCharacters(in: .whitespacesAndNewlines)
if let editingItem {
let updatedItem = editingItem.updatingContent(trimmedReplyText)
if parentItem?.id == editingItem.id && parentItem?.source == editingItem.source {
parentItem = updatedItem
} else if let index = replies.firstIndex(where: { $0.id == editingItem.id && $0.source == editingItem.source }) {
replies[index] = updatedItem
}
}
if let deletingItem {
if parentItem?.id == deletingItem.id && parentItem?.source == deletingItem.source {
AppState.shared.back()
} else {
replies.removeAll { $0.id == deletingItem.id && $0.source == deletingItem.source }
}
}
replyText = ""
editingItem = nil
deletingItem = nil
}
private func applyFailureState() {
isLoading = false
isLoadingNextPage = false
errorMessage = I18n.Common.commonError
isShowPopup = true
}
private var isCommunityContext: Bool {
if case .community = context { return true }
if case .communityPostDetail = context { return true }
return false
}
private var isCommunityPostDetailContext: Bool {
if case .communityPostDetail = context { return true }
return false
}
private func communityRepliesPublisher(page: Int) -> AnyPublisher<Response, MoyaError> {
switch context {
case .community(_, _, let parentComment):
return communityRepository.getCommentReplyList(commentId: parentComment.id, page: page, size: communityReplyPageSize)
case .communityPostDetail(_, _, let parentComment):
return v2CommunityRepository.getCommunityCommentReplies(commentId: parentComment.commentId, page: page, size: communityReplyPageSize)
default:
return Fail(error: MoyaError.underlying(NSError(domain: "CreatorChannelReplyDetail", code: -1), nil)).eraseToAnyPublisher()
}
}
private func handleCommunityRepliesResponse(_ response: Moya.Response, requestPage: Int, reset: Bool) {
if isCommunityPostDetailContext {
handleCommunityPostDetailRepliesResponse(response, requestPage: requestPage, reset: reset)
} else {
handleLegacyCommunityRepliesResponse(response, requestPage: requestPage, reset: reset)
}
}
private func handleLegacyCommunityRepliesResponse(_ response: Moya.Response, requestPage: Int, reset: Bool) {
do {
let decoded = try JSONDecoder().decode(ApiResponse<GetCommunityPostCommentListResponse>.self, from: response.data)
if let data = decoded.data, decoded.success {
let newReplies = data.items.map { CreatorChannelReplyDetailDisplayItem(communityComment: $0) }
applyCommunityReplies(newReplies, requestPage: requestPage, reset: reset, totalCount: data.totalCount, hasNext: nil)
} else {
errorMessage = decoded.message ?? I18n.Common.commonError
isShowPopup = true
}
} catch {
ERROR_LOG(error.localizedDescription)
errorMessage = I18n.Common.commonError
isShowPopup = true
}
}
private func handleCommunityPostDetailRepliesResponse(_ response: Moya.Response, requestPage: Int, reset: Bool) {
do {
let decoded = try JSONDecoder().decode(ApiResponse<CreatorChannelCommunityRepliesResponse>.self, from: response.data)
if let data = decoded.data, decoded.success {
let creatorId = contextCreatorId
let newReplies = data.replies.map { CreatorChannelReplyDetailDisplayItem(communityPostDetailReply: $0, creatorId: creatorId) }
applyCommunityReplies(newReplies, requestPage: requestPage, reset: reset, totalCount: data.replyCount, hasNext: data.hasNext)
} else {
errorMessage = decoded.message ?? I18n.Common.commonError
isShowPopup = true
}
} catch {
ERROR_LOG(error.localizedDescription)
errorMessage = I18n.Common.commonError
isShowPopup = true
}
}
private var contextCreatorId: Int {
switch context {
case .community(let creatorId, _, _), .communityPostDetail(let creatorId, _, _), .fanTalk(let creatorId, _):
return creatorId
case .none:
return 0
}
}
private func applyCommunityReplies(_ newReplies: [CreatorChannelReplyDetailDisplayItem], requestPage: Int, reset: Bool, totalCount: Int, hasNext: Bool?) {
if reset || requestPage == (isCommunityPostDetailContext ? 0 : 1) {
replies = newReplies
} else {
replies.append(contentsOf: newReplies)
}
communityReplyTotalCount = totalCount
communityReplyPage = requestPage + 1
isCommunityReplyLast = hasNext.map { !$0 } ?? (replies.count >= totalCount || newReplies.isEmpty)
}
}