// // 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 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 = false { didSet { if isShowNotice { isShowMenuPan = false } } } @Published var isShowMenuPan = false { didSet { if isShowMenuPan { isShowNotice = 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 isShowingNewChat = false @Published var isShowPhotoPicker = false @Published var noticeViewWidth: CGFloat = UIFont.systemFontSize @Published var noticeViewHeight: CGFloat = UIFont.systemFontSize @Published var isBgOn = true @Published var isSignatureOn = 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 roulettePreviewList = [RoulettePreview]() @Published var isShowRoulette = false @Published var rouletteItems = [String]() @Published var rouletteSelectedItem = "" var rouletteCan = 0 @Published var signatureImageUrl = "" { didSet { showSignatureImage() } } @Published var signature: LiveRoomDonationResponse? = nil { didSet { showSignatureImage() } } private var menuId = 0 @Published var menu = "" @Published var menuList = [GetMenuPresetResponse]() @Published var isActivateMenu = false { didSet { if !isActivateMenu { menu = "" } } } @Published var selectedMenu: SelectedMenu? = nil var signatureImageUrls = [String]() var signatureList = [LiveRoomDonationResponse]() var isShowSignatureImage = false 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(chatMessage: String, onSuccess: @escaping () -> Void) { 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) } } onSuccess() }) } } } 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(ApiResponse.self, from: responseData) self.isLoading = false if decoded.success { let rawMessage = "\(can)캔을 후원하셨습니다." let donationRawMessage = LiveRoomChatRawMessage( type: .DONATION, message: rawMessage, can: can, signature: decoded.data, signatureImageUrl: decoded.data?.imageUrl, 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 addSignature(signature: decoded.data) 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, menuPanId: isActivateMenu ? menuId : 0, menuPan: isActivateMenu ? menu : "", isActiveMenuPan: isActivateMenu ) if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan) { self.errorMessage = "변경사항이 없습니다." self.isShowErrorPopup = true return } var multipartData = [MultipartFormData]() let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes if (request.title != nil || request.notice != nil || menu != liveRoomInfo?.menuPan) { 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.menuList.removeAll() self.isActivateMenu = false self.selectedMenu = nil self.menu = "" self.isShowMenuPan = false self.getRoomInfo() let editRoomInfoMessage = LiveRoomChatRawMessage( type: .EDIT_ROOM_INFO, message: "", can: 0, donationMessage: "" ) self.agora.sendRawMessageToGroup(rawMessage: editRoomInfoMessage) self.errorMessage = "라이브 정보가 수정되었습니다." self.isShowErrorPopup = true } else { self.errorMessage = decoded.message ?? "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요." self.isShowErrorPopup = true } } catch { self.errorMessage = "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요." self.isShowErrorPopup = true } } .store(in: &subscription) } func selectMenuPreset(selectedMenuPreset: SelectedMenu) { if menuList.isEmpty && (selectedMenuPreset == .MENU_2 || selectedMenuPreset == .MENU_3) { errorMessage = "메뉴 1을 먼저 설정하세요" isShowPopup = true return } if menuList.count == 1 && selectedMenuPreset == .MENU_3 { errorMessage = "메뉴 1과 메뉴 2를 먼저 설정하세요" isShowPopup = true return } if self.selectedMenu != selectedMenuPreset { self.selectedMenu = selectedMenuPreset if menuList.count > selectedMenuPreset.rawValue { let menu = menuList[selectedMenuPreset.rawValue] self.menu = menu.menu self.menuId = menu.id } else { self.menu = "" self.menuId = 0 } } } func getAllMenuPreset(onFailure: @escaping () -> Void) { isLoading = true repository.getAllMenuPreset(creatorId: 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 self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse<[GetMenuPresetResponse]>.self, from: responseData) if let data = decoded.data, decoded.success { self.menuList.removeAll() self.menuList.append(contentsOf: data) self.isActivateMenu = false self.selectedMenu = nil for (index, menuPreset) in self.menuList.enumerated() { if menuPreset.isActive { switch index { case 1: self.selectMenuPreset(selectedMenuPreset: .MENU_2) case 2: self.selectMenuPreset(selectedMenuPreset: .MENU_3) default: self.selectMenuPreset(selectedMenuPreset: .MENU_1) } self.isActivateMenu = true } } } else { onFailure() if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { onFailure() self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } .store(in: &subscription) } 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, message: String) { self.popupContent = message self.isShowPopup = true self.agora.sendRawMessageToGroup( rawMessage: LiveRoomChatRawMessage( type: .TOGGLE_ROULETTE, message: "", can: 0, donationMessage: "", isActiveRoulette: isActiveRoulette ) ) } func showRoulette() { if let liveRoomInfo = liveRoomInfo, !isLoading { self.roulettePreviewList.removeAll() 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<[GetRouletteResponse]>.self, from: responseData) if let data = decoded.data, decoded.success, !data.isEmpty { let roulettePreviewList = data .filter { $0.isActive } .filter { !$0.items.isEmpty} .map { RoulettePreview(id: $0.id, can: $0.can, items: calculatePercentages(options: $0.items)) } self.roulettePreviewList.append(contentsOf: roulettePreviewList) 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(rouletteId: Int) { if !isLoading { isLoading = true rouletteRepository.spinRoulette(request: SpinRouletteRequest(roomId: AppState.shared.roomId, rouletteId: rouletteId)) .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) self.rouletteItems.removeAll() self.rouletteItems.append(contentsOf: data.items.map { $0.title }) self.rouletteSelectedItem = data.result self.rouletteCan = data.can self.isShowRoulette = 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 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 updatedOptions = options.map { option in return RoulettePreviewItem(title: option.title, percent: "\(String(format: "%.2f", option.percentage))%") } return updatedOptions } 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) } private func addSignatureImage(imageUrl: String) { if imageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { if !isShowSignatureImage { isShowSignatureImage = true signatureImageUrl = imageUrl } else { signatureImageUrls.append(imageUrl) } } } private func addSignature(signature: LiveRoomDonationResponse?) { if let signature = signature { if !isShowSignatureImage { self.signature = signature isShowSignatureImage = true } else { self.signatureList.append(signature) } } } private func showSignatureImage() { if let signature = signature { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(signature.time)) { if let nextSignature = self.signatureList.first { self.signature = nextSignature self.signatureList.removeFirst() } else { self.signature = nil self.isShowSignatureImage = false } } } else if signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { DispatchQueue.main.asyncAfter(deadline: .now() + 7) { if let imageUrl = self.signatureImageUrls.first { self.signatureImageUrl = imageUrl self.signatureImageUrls.removeFirst() } else { self.signatureImageUrl = "" self.isShowSignatureImage = false } } } } } 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 if let signature = decoded.signature { self.addSignature(signature: signature) } else if let imageUrl = decoded.signatureImageUrl { self.addSignatureImage(imageUrl: imageUrl) } } 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() } } }