// // LiveRoomViewModel.swift // SodaLive // // Created by klaus on 2023/08/14. // import Foundation import Moya import Combine import AgoraRtcKit import AgoraRtmKit import FirebaseDynamicLinks final class LiveRoomViewModel: NSObject, ObservableObject { private var agora: Agora = Agora.shared private let repository = LiveRepository() private let userRepository = UserRepository() private let reportRepository = ReportRepository() private let rouletteRepository = RouletteRepository() private var subscription = Set() @Published var chatMessage = "" @Published var isSpeakerMute = false @Published var isMute = false @Published var role = LiveRoomMemberRole.LISTENER @Published var messageChangeFlag = false @Published var messages = [LiveRoomChat]() @Published var activeSpeakers = [UInt]() @Published var muteSpeakers = [UInt]() @Published var liveRoomInfo: GetRoomInfoResponse? @Published var userProfile: GetLiveRoomUserProfileResponse? @Published var coverImageUrl: String? @Published var isLoading = false @Published var errorMessage = "" @Published var reportMessage = "" @Published var isShowReportPopup = false @Published var isShowErrorPopup = false @Published var isShowUserProfilePopup = false @Published var popupContent = "" @Published var popupCancelTitle: String? = nil @Published var popupCancelAction: (() -> Void)? = nil @Published var popupConfirmTitle: String? = nil @Published var popupConfirmAction: (() -> Void)? = nil @Published var isShowPopup = false { didSet { if !isShowPopup { resetPopupContent() } } } @Published var isShowProfileList = false @Published var isShowProfilePopup = false { didSet { if !isShowProfilePopup { selectedProfile = nil } } } @Published var selectedProfile: LiveRoomMember? @Published var isShowNotice = true { didSet { if !isShowNotice { isExpandNotice = false } } } @Published var isExpandNotice = false @Published var isShowDonationPopup = false @Published var isShowDonationMessagePopup = false @Published var isShowDonationRankingPopup = false @Published var isSpeakerFold = false @Published var isShowQuitPopup = false @Published var isShowLiveEndPopup = false @Published var isShowEditRoomInfoDialog = false @Published var isShowShareView = false @Published var shareMessage = "" @Published var isShowKickOutPopup = false @Published var kickOutDesc = "" @Published var kickOutId = 0 { didSet { kickOutDesc = "\(getUserNicknameAndProfileUrl(accountId: kickOutId).nickname)님을 내보내시겠어요?" } } @Published var totalDonationCan = 0 @Published var donationMessageList = [LiveRoomDonationMessage]() @Published var donationMessageCount = 0 @Published var isBgOn = true @Published var donationStatus: GetLiveRoomDonationStatusResponse? @Published private(set) var offset: CGFloat = 0 @Published private(set) var originOffset: CGFloat = 0 private var isCheckedOriginOffset: Bool = false @Published var coverImage: UIImage? = nil @Published var isShowReportMenu = false @Published var isShowUesrBlockConfirm = false @Published var isShowUesrReportView = false @Published var isShowProfileReportConfirm = false @Published var isShowNoChattingConfirm = false @Published var reportUserId = 0 @Published var reportUserNickname = "" @Published var reportUserIsBlocked = false @Published var noChattingUserId = 0 @Published var noChattingUserNickname = "" @Published var noChattingUserProfileUrl = "" private let noChattingTime = 180 @Published var isNoChatting = false @Published var remainingNoChattingTime = 0 @Published var isActiveRoulette = false @Published var isShowRouletteSettings = false @Published var isShowRoulettePreview = false @Published var roulettePreview: RoulettePreview? = nil @Published var isShowRoulette = false @Published var rouletteItems = [String]() @Published var rouletteSelectedItem = "" var rouletteCan = 0 var timer: DispatchSourceTimer? func setOriginOffset(_ offset: CGFloat) { guard !isCheckedOriginOffset else { return } self.originOffset = offset self.offset = offset isCheckedOriginOffset = true } func setOffset(_ offset: CGFloat) { guard isCheckedOriginOffset else { return } self.offset = offset } func initAgoraEngine() { agora.rtcEngineDelegate = self agora.rtmDelegate = self agora.initialize() } private func deInitAgoraEngine() { agora.deInit() } func agoraConnectSuccess(isManager: Bool) { self.isLoading = false if isManager { role = .SPEAKER } else { role = .LISTENER } DEBUG_LOG("agoraConnectSuccess") if containNoChatRoom() { startNoChatting() } } func agoraConnectFail() { self.isLoading = false DEBUG_LOG("agoraConnectFail") AppState.shared.roomId = 0 AppState.shared.isShowPlayer = false } func quitRoom() { isLoading = true if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) { muteSpeakers.remove(at: index) } repository.quitRoom(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.deInitAgoraEngine() self.liveRoomInfo = nil AppState.shared.roomId = 0 } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowErrorPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowErrorPopup = true } self.isLoading = false } .store(in: &subscription) } func getRoomInfo(userId: Int = 0, onSuccess: @escaping (String) -> Void = { _ in }) { isLoading = true repository.getRoomInfo(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { self.liveRoomInfo = data if self.coverImageUrl != data.coverImageUrl { self.coverImageUrl = data.coverImageUrl } self.isActiveRoulette = data.isActiveRoulette self.isLoading = true self.agora.joinChannel( roomInfo: data, rtmChannelDelegate: self, onConnectSuccess: self.agoraConnectSuccess, onConnectFail: self.agoraConnectFail ) getTotalDonationCan() if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) { let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname onSuccess(nickname) } } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowErrorPopup = true } self.isLoading = false } catch { self.isLoading = false self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowErrorPopup = true } } .store(in: &subscription) } func toggleMute() { isMute.toggle() agora.mute(isMute) if isMute { muteSpeakers.append(UInt(UserDefaults.int(forKey: .userId))) } else { if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) { muteSpeakers.remove(at: index) } } } func toggleSpeakerMute() { isSpeakerMute.toggle() agora.speakerMute(isSpeakerMute) } func sendMessage() { DispatchQueue.main.async {[unowned self] in if isNoChatting { self.popupContent = "\(remainingNoChattingTime)초 동안 채팅하실 수 없습니다" self.isShowPopup = true } else if chatMessage.count > 0 { agora.sendMessageToGroup(textMessage: chatMessage, completion: { [unowned self] errorCode in if errorCode == .errorOk { let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId)) let rank = getUserRank(userId: UserDefaults.int(forKey: .userId)) self.messages.append(LiveRoomNormalChat(userId: UserDefaults.int(forKey: .userId), profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chatMessage)) self.messageChangeFlag.toggle() if self.messages.count > 100 { self.messages.remove(at: 0) } } self.chatMessage = "" }) } } } func donation(can: Int, message: String = "") { if can > 0 { isLoading = true repository.donation(roomId: AppState.shared.roomId, can: can, message: message) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) self.isLoading = false if decoded.success { let rawMessage = "\(can)캔을 후원하셨습니다." let donationRawMessage = LiveRoomChatRawMessage( type: .DONATION, message: rawMessage, can: can, donationMessage: message ) UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can) agora.sendRawMessageToGroup( rawMessage: donationRawMessage, completion: { [unowned self] errorCode in if errorCode == .errorOk { let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId)) self.messages.append( LiveRoomDonationChat( profileUrl: profileUrl, nickname: nickname, chat: rawMessage, can: can, donationMessage: message ) ) totalDonationCan += can self.messageChangeFlag.toggle() if self.messages.count > 100 { self.messages.remove(at: 0) } } else { refundDonation() } }, fail: { [unowned self] in refundDonation() } ) } else { if let message = decoded.message { self.popupContent = message } else { self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.isLoading = false self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } else { popupContent = "1캔 이상 후원하실 수 있습니다." isShowPopup = true } } private func refundDonation() { isLoading = true repository.refundDonation(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) self.isLoading = false if decoded.success { self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } else { if let message = decoded.message { self.errorMessage = message } else { self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." } self.isShowPopup = true } } catch { self.isLoading = false self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." self.isShowPopup = true } } .store(in: &subscription) } func inviteSpeaker(peerId: Int) { agora.sendMessageToPeer(peerId: String(peerId), rawMessage: LiveRoomRequestType.INVITE_SPEAKER.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in if errorCode == .ok { self.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요." self.isShowPopup = true } }) } func changeListener(peerId: Int, isFromManager: Bool = false) { agora.sendMessageToPeer(peerId: String(peerId), rawMessage: LiveRoomRequestType.CHANGE_LISTENER.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in if errorCode == .ok { if isFromManager { getRoomInfo() setManagerMessage() releaseManagerMessageToPeer(userId: peerId) self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: peerId).nickname)님을 스탭에서 해제했어요." } else { self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: peerId).nickname)님을 리스너로 변경했어요." } self.isShowPopup = true } }) } private func getUserNicknameAndProfileUrl(accountId: Int) -> (nickname: String, profileUrl: String) { for staff in liveRoomInfo!.managerList { if staff.id == accountId { return (staff.nickname, staff.profileImage) } } for speaker in liveRoomInfo!.speakerList { if speaker.id == accountId { return (speaker.nickname, speaker.profileImage) } } for listener in liveRoomInfo!.listenerList { if listener.id == accountId { return (listener.nickname, listener.profileImage) } } return ("", "") } func isEqualToStaffId(creatorId: Int) -> Bool { for staff in liveRoomInfo!.managerList { if staff.id == creatorId { return true } } return false } func setListener() { repository.setListener(roomId: AppState.shared.roomId, userId: UserDefaults.int(forKey: .userId)) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.role = .LISTENER self.agora.setRole(role: .audience) self.isMute = false self.agora.mute(isMute) if let index = self.muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) { self.muteSpeakers.remove(at: index) } self.getRoomInfo() } } catch { } } .store(in: &subscription) } private func setSpeaker() { repository.setSpeaker(roomId: AppState.shared.roomId, userId: UserDefaults.int(forKey: .userId)) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.role = .SPEAKER self.agora.setRole(role: .broadcaster) self.popupContent = "스피커가 되었어요!" self.isShowPopup = true self.isMute = false self.getRoomInfo() } } catch { } } .store(in: &subscription) } private func resetPopupContent() { errorMessage = "" popupContent = "" popupCancelTitle = nil popupCancelAction = nil popupConfirmTitle = nil popupConfirmAction = nil } private func requestSpeakerAllow(_ peerId: String) { agora.sendMessageToPeer(peerId: peerId, rawMessage: LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue.data(using: .utf8)!, completion: nil) } func editLiveRoomInfo(title: String, notice: String) { let request = EditLiveRoomInfoRequest( title: liveRoomInfo!.title != title ? title : nil, notice: liveRoomInfo!.notice != notice ? notice : nil, numberOfPeople: nil, beginDateTimeString: nil, timezone: nil ) if (request.title == nil && request.notice == nil && coverImage == nil) { self.errorMessage = "변경사항이 없습니다." self.isShowErrorPopup = true return } var multipartData = [MultipartFormData]() let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes if (request.title != nil || request.notice != nil) { let jsonData = try? encoder.encode(request) if let jsonData = jsonData { multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) } } if let coverImage = coverImage, let imageData = coverImage.jpegData(compressionQuality: 0.8) { multipartData.append( MultipartFormData( provider: .data(imageData), name: "coverImage", fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", mimeType: "image/*") ) } repository.editLiveRoomInfo(roomId: AppState.shared.roomId, parameters: multipartData) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.coverImage = nil self.getRoomInfo() let editRoomInfoMessage = LiveRoomChatRawMessage( type: .EDIT_ROOM_INFO, message: "", can: 0, donationMessage: "" ) self.agora.sendRawMessageToGroup(rawMessage: editRoomInfoMessage) } else { self.errorMessage = decoded.message ?? "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요." self.isShowPopup = true } } catch { self.errorMessage = "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요." self.isShowPopup = true } } .store(in: &subscription) } func shareRoom() { guard let link = URL(string: "https://sodalive.net/?room_id=\(AppState.shared.roomId)") else { return } let dynamicLinksDomainURIPrefix = "https://sodalive.page.link" guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else { self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요." self.isShowErrorPopup = true return } linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "kr.co.vividnext.sodalive") linkBuilder.iOSParameters?.appStoreID = "6461721697" linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "kr.co.vividnext.sodalive") guard let longDynamicLink = linkBuilder.url else { self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요." self.isShowErrorPopup = true return } DEBUG_LOG("The long URL is: \(longDynamicLink)") DynamicLinkComponents.shortenURL(longDynamicLink, options: nil) { [unowned self] url, warnings, error in let shortUrl = url?.absoluteString if let liveRoomInfo = self.liveRoomInfo { let urlString = shortUrl != nil ? shortUrl! : longDynamicLink.absoluteString if liveRoomInfo.isPrivateRoom { shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 비공개라이브에 초대하였습니다.\n" + "※ 라이브 참여: \(urlString)\n" + "(입장 비밀번호: \(liveRoomInfo.password!))" } else { shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 공개라이브에 초대하였습니다.\n" + "※ 라이브 참여: \(urlString)" } isShowShareView = true } else { self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요." self.isShowErrorPopup = true return } } } func kickOut() { repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { _ in } .store(in: &subscription) let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId { agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in if errorCode == .ok { self.popupContent = "\(nickname)님을 내보냈습니다." self.isShowPopup = true } }) } if let index = muteSpeakers.firstIndex(of: UInt(kickOutId)) { muteSpeakers.remove(at: index) } isShowKickOutPopup = false kickOutDesc = "" kickOutId = 0 } func getDonationStatus() { isLoading = true repository.donationStatus(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { self.donationStatus = data } else { self.errorMessage = "후원현황을 가져오지 못했습니다\n다시 시도해 주세요." self.isShowPopup = true } } catch { self.isLoading = false self.errorMessage = "후원현황을 가져오지 못했습니다\n다시 시도해 주세요." self.isShowPopup = true } } .store(in: &subscription) } func creatorFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) { var userId = 0 if let creatorId = creatorId { userId = creatorId } else if let liveRoomInfo = liveRoomInfo { userId = liveRoomInfo.creatorId } if userId > 0 { isLoading = true userRepository.creatorFollow(creatorId: userId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.getRoomInfo() if isGetUserProfile { getUserProfile(userId: userId) } } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } } func creatorUnFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) { var userId = 0 if let creatorId = creatorId { userId = creatorId } else if let liveRoomInfo = liveRoomInfo { userId = liveRoomInfo.creatorId } if userId > 0 { isLoading = true userRepository.creatorUnFollow(creatorId: userId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.getRoomInfo() if isGetUserProfile { getUserProfile(userId: userId) } } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } } func getUserRank(userId: Int) -> Int { // 방장 -> -2 // 스탭 -> -3 // 나머지 -> 체크 if userId == liveRoomInfo!.creatorId { return -2 } else if isEqualToStaffId(creatorId: userId) { return -3 } else { return liveRoomInfo!.donationRankingTop3UserIds.firstIndex(of: userId) ?? -1 } } func getTotalDonationCan() { repository.getTotalDoantionCan(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { self.totalDonationCan = data.totalDonationCan } } catch { } } .store(in: &subscription) } func getMemberCan() { userRepository.getMemberCan() .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { UserDefaults.set(data.can, forKey: .can) } } catch { } } .store(in: &subscription) } func getDonationMessageList() { isLoading = true repository.getDonationMessageList(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse<[LiveRoomDonationMessage]>.self, from: responseData) if let data = decoded.data, decoded.success { self.donationMessageList.removeAll() self.donationMessageList.append(contentsOf: data) self.donationMessageCount = data.count } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } func deleteDonationMessage(uuid: String) { isLoading = true repository.deleteDonationMessage(roomId: AppState.shared.roomId, messageUUID: uuid) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.donationMessageCount -= 1 let filteredDonationMessageList = self.donationMessageList.filter { $0.uuid != uuid } self.donationMessageList.removeAll() self.donationMessageList.append(contentsOf: filteredDonationMessageList) } else { self.errorMessage = "메시지를 삭제하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } catch { self.errorMessage = "메시지를 삭제하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } func getUserProfile(userId: Int) { isLoading = true repository.getUserProfile(roomId: AppState.shared.roomId, userId: userId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { userProfile = data isShowUserProfilePopup = true } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } func setManager(userId: Int) { isLoading = true repository.setManager(roomId: AppState.shared.roomId, userId: userId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { getRoomInfo() setManagerMessage() self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: userId).nickname)님을 스탭으로 지정했습니다." self.isShowPopup = true } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } func releaseManagerMessageToPeer(userId: Int) { agora.sendMessageToPeer( peerId: String(userId), rawMessage: LiveRoomRequestType.RELEASE_MANAGER.rawValue.data(using: .utf8)!, completion: nil ) } func setManagerMessageToPeer(userId: Int) { agora.sendMessageToPeer( peerId: String(userId), rawMessage: LiveRoomRequestType.SET_MANAGER.rawValue.data(using: .utf8)!, completion: nil ) } func setNoChatting() { agora.sendMessageToPeer( peerId: String(noChattingUserId), rawMessage: LiveRoomRequestType.NO_CHATTING.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in if errorCode == .ok { self.popupContent = "\(noChattingUserNickname)님을 3분간 채팅금지를 하였습니다." self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.noChattingUserId = 0 self.noChattingUserNickname = "" self.noChattingUserProfileUrl = "" } } } ) } private func setManagerMessage() { let setManagerMessage = LiveRoomChatRawMessage( type: .SET_MANAGER, message: "", can: 0, donationMessage: "" ) self.agora.sendRawMessageToGroup(rawMessage: setManagerMessage) } func userBlock(onSuccess: @escaping (Int) -> Void) { isLoading = true userRepository.memberBlock(userId: reportUserId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.reportMessage = "차단하였습니다." self.getUserProfile(userId: reportUserId) onSuccess(reportUserId) self.reportUserId = 0 self.reportUserNickname = "" self.reportUserIsBlocked = false } else { if let message = decoded.message { self.reportMessage = message } else { self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } } self.isShowReportPopup = true } catch { self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowReportPopup = true } } .store(in: &subscription) } func userUnBlock() { isLoading = true userRepository.memberUnBlock(userId: reportUserId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.reportMessage = "차단이 해제 되었습니다." self.getUserProfile(userId: reportUserId) self.reportUserId = 0 self.reportUserNickname = "" self.reportUserIsBlocked = false } else { if let message = decoded.message { self.reportMessage = message } else { self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } } self.isShowReportPopup = true } catch { self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowReportPopup = true } } .store(in: &subscription) } func report(type: ReportType, reason: String = "프로필 신고") { isLoading = true let request = ReportRequest(type: type, reason: reason, reportedMemberId: reportUserId, cheersId: nil, audioContentId: nil) reportRepository.report(request: request) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data self.reportUserId = 0 self.reportUserNickname = "" self.reportUserIsBlocked = false do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if let message = decoded.message { self.reportMessage = message } else { self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowReportPopup = true } catch { self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowReportPopup = true } } .store(in: &subscription) } private func containNoChatRoom() -> Bool { let noChatRoomList = getNoChatRoomListFromUserDefaults() if let _ = noChatRoomList.firstIndex(of: liveRoomInfo!.roomId) { return true } return false } private func startNoChatting() { isNoChatting = true remainingNoChattingTime = noChattingTime popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: liveRoomInfo!.creatorId).nickname)님이 3분간 채팅을 금지하였습니다." isShowPopup = true startCountDown() } private func startCountDown() { // 1초마다 타이머를 실행 let queue = DispatchQueue.global(qos: .background) timer = DispatchSource.makeTimerSource(queue: queue) timer?.schedule(deadline: .now(), repeating: 1.0) timer?.setEventHandler { [unowned self] in DispatchQueue.main.async { self.remainingNoChattingTime -= 1 if self.remainingNoChattingTime <= 0 { self.isNoChatting = false self.timer?.cancel() self.removeNoChatRoom() self.popupContent = "채팅금지가 해제되었습니다." self.isShowPopup = true } } } timer?.resume() } private func addNoChatRoom() { var noChatRoomList = getNoChatRoomListFromUserDefaults() noChatRoomList.append(liveRoomInfo!.roomId) saveNoChatRoomListToUserDefaults(noChatRoomList: noChatRoomList) } private func removeNoChatRoom() { var noChatRoomList = getNoChatRoomListFromUserDefaults() if let index = noChatRoomList.firstIndex(of: liveRoomInfo!.roomId) { noChatRoomList.remove(at: index) } saveNoChatRoomListToUserDefaults(noChatRoomList: noChatRoomList) } private func getNoChatRoomListFromUserDefaults() -> [Int] { if let noChatRoomListData = UserDefaults.data(forKey: .noChatRoomList) { let jsonDecoder = JSONDecoder() if let noChatRoomList = try? jsonDecoder.decode([Int].self, from: noChatRoomListData) { return noChatRoomList } } return [] } private func saveNoChatRoomListToUserDefaults(noChatRoomList: [Int]) { let jsonEncoder = JSONEncoder() if let jsonData = try? jsonEncoder.encode(noChatRoomList) { UserDefaults.set(jsonData, forKey: .noChatRoomList) } } func setActiveRoulette(isActiveRoulette: Bool) { self.popupContent = isActiveRoulette ? "룰렛을 활성화 했습니다." : "룰렛을 비활성화 했습니다." self.isShowPopup = true self.agora.sendRawMessageToGroup( rawMessage: LiveRoomChatRawMessage( type: .TOGGLE_ROULETTE, message: "", can: 0, donationMessage: "", isActiveRoulette: isActiveRoulette ) ) } func showRoulette() { if let liveRoomInfo = liveRoomInfo, !isLoading { isLoading = true rouletteRepository.getRoulette(creatorId: liveRoomInfo.creatorId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success, !data.items.isEmpty { self.roulettePreview = RoulettePreview(can: data.can, items: calculatePercentages(options: data.items)) self.isShowRoulettePreview = true } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." } self.isShowErrorPopup = true } } catch { self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." self.isShowErrorPopup = true } } .store(in: &subscription) } } func spinRoulette() { if !isLoading { isLoading = true rouletteRepository.spinRoulette(request: SpinRouletteRequest(roomId: AppState.shared.roomId)) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success, !data.items.isEmpty { UserDefaults.set(UserDefaults.int(forKey: .can) - data.can, forKey: .can) randomSelectRouletteItem(can: data.can, items: data.items) } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." } self.isShowErrorPopup = true } } catch { self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." self.isShowErrorPopup = true } } .store(in: &subscription) } } func sendRouletteDonation() { let rouletteRawMessage = LiveRoomChatRawMessage( type: .ROULETTE_DONATION, message: rouletteSelectedItem, can: rouletteCan, donationMessage: "" ) self.agora.sendRawMessageToGroup( rawMessage: rouletteRawMessage, completion: { [unowned self] errorCode in if errorCode == .errorOk { let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId)) self.messages.append( LiveRoomRouletteDonationChat( profileUrl: profileUrl, nickname: nickname, rouletteResult: rouletteSelectedItem ) ) totalDonationCan += rouletteCan self.rouletteItems.removeAll() self.rouletteSelectedItem = "" self.rouletteCan = 0 self.messageChangeFlag.toggle() if self.messages.count > 100 { self.messages.remove(at: 0) } } else { self.refundRouletteDonation() } }, fail: { [unowned self] in self.refundRouletteDonation() } ) } private func calculatePercentages(options: [RouletteItem]) -> [RoulettePreviewItem] { let totalWeight = options.reduce(0) { $0 + $1.weight } let updatedOptions = options.map { option in let percent = floor(Double(option.weight) / Double(totalWeight) * 10000) / 100 return RoulettePreviewItem(title: option.title, percent: "\(String(format: "%.2f", percent))%") } return updatedOptions } private func randomSelectRouletteItem(can: Int, items: [RouletteItem]) { isLoading = true var rouletteItems = [String]() items.forEach { var i = 1 while (i < $0.weight * 10) { rouletteItems.append($0.title) i += 1 } } isLoading = false self.rouletteItems.removeAll() self.rouletteItems.append(contentsOf: items.map { $0.title }) self.rouletteSelectedItem = rouletteItems.randomElement()! self.rouletteCan = can self.isShowRoulette = true } private func refundRouletteDonation() { isLoading = true rouletteRepository.refundRouletteDonation(roomId: AppState.shared.roomId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) self.isLoading = false if decoded.success { self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } else { if let message = decoded.message { self.errorMessage = message } else { self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." } self.isShowPopup = true } } catch { self.isLoading = false self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." self.isShowPopup = true } } .store(in: &subscription) } } extension LiveRoomViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { let activeSpeakerIds = speakers .filter { $0.volume > 0 } .map { $0.uid } DEBUG_LOG("activeSpeakerIds::: \(activeSpeakerIds)") activeSpeakers.removeAll() activeSpeakers.append(contentsOf: activeSpeakerIds) } func rtcEngine(_ engine: AgoraRtcEngineKit, didAudioMuted muted: Bool, byUid uid: UInt) { if muted && !muteSpeakers.contains(uid){ muteSpeakers.append(uid) } else { if let index = muteSpeakers.firstIndex(of: uid) { muteSpeakers.remove(at: index) } } } func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { getRoomInfo() } func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { DispatchQueue.main.async {[unowned self] in if uid == UInt(self.liveRoomInfo!.creatorId) { // 라이브 종료 self.liveRoomInfo = nil self.errorMessage = "라이브가 종료되었습니다." self.isShowErrorPopup = true } else { // get room info self.getRoomInfo() } } } } extension LiveRoomViewModel: AgoraRtmDelegate { func rtmKit(_ kit: AgoraRtmKit, messageReceived message: AgoraRtmMessage, fromPeer peerId: String) { if message.type == .raw, let rawMessage = message as? AgoraRtmRawMessage { let rawMessageString = String(data: rawMessage.rawData, encoding: .utf8) DispatchQueue.main.async { [unowned self] in if rawMessageString == LiveRoomRequestType.CHANGE_LISTENER.rawValue { self.setListener() return } if rawMessageString == LiveRoomRequestType.REQUEST_SPEAKER.rawValue { self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: Int(peerId)!).nickname)님이 스피커 요청을 했어요!\n스퍼커로 초대할까요?" self.popupCancelTitle = "건너뛰기" self.popupCancelAction = { self.isShowPopup = false } self.popupConfirmTitle = "스피커로 초대" self.popupConfirmAction = { self.isShowPopup = false if self.liveRoomInfo!.speakerList.count <= 4 { self.requestSpeakerAllow(peerId) } else { self.errorMessage = "스피커 정원이 초과되었습니다." self.isShowErrorPopup = true } } self.isShowPopup = true return } if rawMessageString == LiveRoomRequestType.INVITE_SPEAKER.rawValue && self.role == .LISTENER { self.popupContent = "스피커로 초대되었어요" self.popupCancelTitle = "다음에요" self.popupCancelAction = { self.isShowPopup = false } self.popupConfirmTitle = "스피커로 참여하기" self.popupConfirmAction = { self.isShowPopup = false self.setSpeaker() } self.isShowPopup = true return } if rawMessageString == LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue && self.role == .LISTENER { self.setSpeaker() return } if rawMessageString == LiveRoomRequestType.KICK_OUT.rawValue { if let roomInfo = self.liveRoomInfo { self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 라이브에서 내보냈습니다." } else { self.popupContent = "방장님이 라이브에서 내보냈습니다." } self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in self.quitRoom() } return } if rawMessageString == LiveRoomRequestType.SET_MANAGER.rawValue { if self.role == .SPEAKER { self.role = .LISTENER self.isMute = false self.agora.mute(isMute) self.agora.setRole(role: .audience) } if let roomInfo = self.liveRoomInfo { self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 스탭으로 지정했습니다." } else { self.popupContent = "방장님이 스탭으로 지정했습니다" } self.isShowPopup = true } if rawMessageString == LiveRoomRequestType.RELEASE_MANAGER.rawValue { if let roomInfo = self.liveRoomInfo { self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 스탭에서 해제했습니다." } else { self.popupContent = "방장님이 스탭에서 해제했습니다." } self.isShowPopup = true } if rawMessageString == LiveRoomRequestType.NO_CHATTING.rawValue { DispatchQueue.main.async { self.addNoChatRoom() self.startNoChatting() } } } } } } extension LiveRoomViewModel: AgoraRtmChannelDelegate { func channel(_ channel: AgoraRtmChannel, messageReceived message: AgoraRtmMessage, from member: AgoraRtmMember) { let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(member.userId)!) if message.type == .raw, let rawMessage = message as? AgoraRtmRawMessage { do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: rawMessage.rawData) if decoded.type == .DONATION { self.messages.append( LiveRoomDonationChat( profileUrl: profileUrl, nickname: nickname, chat: decoded.message, can: decoded.can, donationMessage: decoded.donationMessage ?? "" ) ) self.totalDonationCan += decoded.can } else if decoded.type == .ROULETTE_DONATION { self.messages.append( LiveRoomRouletteDonationChat( profileUrl: profileUrl, nickname: nickname, rouletteResult: decoded.message ) ) self.totalDonationCan += decoded.can } else if decoded.type == .TOGGLE_ROULETTE && decoded.isActiveRoulette != nil { self.isActiveRoulette = decoded.isActiveRoulette! } else if decoded.type == .EDIT_ROOM_INFO || decoded.type == .SET_MANAGER { self.getRoomInfo() } } catch { } } else { let chat = message.text let rank = getUserRank(userId: Int(member.userId) ?? 0) if !chat.trimmingCharacters(in: .whitespaces).isEmpty { messages.append(LiveRoomNormalChat(userId: Int(member.userId)!, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chat)) } } DispatchQueue.main.async { [unowned self] in self.messageChangeFlag.toggle() if self.messages.count > 100 { self.messages.remove(at: 0) } } } func channel(_ channel: AgoraRtmChannel, memberJoined member: AgoraRtmMember) { getRoomInfo(userId: Int(member.userId)!) { [unowned self] nickname in if !nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { DispatchQueue.main.async { [unowned self] in self.messages.append(LiveRoomJoinChat(nickname: nickname)) self.messageChangeFlag.toggle() if self.messages.count > 100 { self.messages.remove(at: 0) } } } } } func channel(_ channel: AgoraRtmChannel, memberLeft member: AgoraRtmMember) { if let liveRoomInfo = liveRoomInfo, liveRoomInfo.creatorId != Int(member.userId)! { getRoomInfo() } } }