2572 lines
106 KiB
Swift
2572 lines
106 KiB
Swift
//
|
|
// LiveRoomViewModel.swift
|
|
// SodaLive
|
|
//
|
|
// Created by klaus on 2023/08/14.
|
|
//
|
|
|
|
import Foundation
|
|
import Moya
|
|
import Combine
|
|
import UIKit
|
|
|
|
import AgoraRtcKit
|
|
import AgoraRtmKit
|
|
|
|
struct BigHeartParticle: Identifiable {
|
|
let id: UUID
|
|
var x: CGFloat
|
|
var y: CGFloat
|
|
var vx: CGFloat
|
|
var vy: CGFloat
|
|
var opacity: Double
|
|
var scale: CGFloat
|
|
var rotation: Double
|
|
var life: Double // 남은 수명 (초)
|
|
var size: CGFloat // 파편 기본 크기 (pt)
|
|
var isRain: Bool // 낙하 파편 여부
|
|
var gravityScale: CGFloat // 중력 계수(파티클별 낙하 속도 분산)
|
|
}
|
|
|
|
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 let userActionRepository = UserActionRepository()
|
|
private var subscription = Set<AnyCancellable>()
|
|
|
|
@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 isLoadingLikeHeart = false
|
|
@Published var isLoading = false
|
|
@Published var errorMessage = ""
|
|
@Published var reportMessage = ""
|
|
@Published var isShowReportPopup = false
|
|
@Published var isShowErrorPopup = false
|
|
@Published var isShowUserProfilePopup = false
|
|
@Published var changeIsAdult = false {
|
|
didSet {
|
|
if changeIsAdult && !UserDefaults.bool(forKey: .auth) {
|
|
agora.speakerMute(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
@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 isShowHeartRankingPopup = 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 totalHeartCount = 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 isEntryMessageEnabled = true
|
|
@Published var donationStatus: GetLiveRoomDonationStatusResponse?
|
|
@Published var heartStatus: GetLiveRoomHeartListResponse?
|
|
|
|
@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 isShowNoticeLikeHeart = false {
|
|
didSet {
|
|
if !isShowNoticeLikeHeart {
|
|
isAvailableLikeHeart = true
|
|
}
|
|
}
|
|
}
|
|
|
|
@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()
|
|
}
|
|
}
|
|
|
|
@Published var heartNickname: String? = nil {
|
|
didSet {
|
|
if heartNickname != nil {
|
|
showNextHeartMessage()
|
|
}
|
|
}
|
|
}
|
|
@Published var heartNicknameList = [String]()
|
|
|
|
private var menuId = 0
|
|
@Published var menu = ""
|
|
@Published var menuList = [GetMenuPresetResponse]()
|
|
|
|
@Published var isActivateMenu = false {
|
|
didSet {
|
|
if !isActivateMenu {
|
|
menu = ""
|
|
}
|
|
}
|
|
}
|
|
@Published var selectedMenu: SelectedMenu? = nil
|
|
|
|
@Published var hearts: [Heart] = []
|
|
|
|
// Remote BIG-HEART (수신측) 물 채우기 + 폭발 파편 상태
|
|
@Published var isShowRemoteBigHeart: Bool = false
|
|
@Published var remoteWaterProgress: CGFloat = 0
|
|
@Published var remoteWavePhase: CGFloat = 0
|
|
@Published var bigHeartParticles: [BigHeartParticle] = []
|
|
|
|
// 최근 폭발 파편의 개수/크기 기록(낙하 효과에 사용)
|
|
private var lastExplosionCount: Int = 0
|
|
private var lastExplosionSizes: [CGFloat] = []
|
|
// 폭발 파편이 모두 사라진 직후 비(낙하)를 스폰해야 하는지 여부
|
|
private var shouldSpawnRainAfterExplosionEnds: Bool = false
|
|
|
|
var signatureImageUrls = [String]()
|
|
var signatureList = [LiveRoomDonationResponse]()
|
|
var isShowSignatureImage = false
|
|
|
|
var timer: DispatchSourceTimer?
|
|
var heartTimer: DispatchSourceTimer?
|
|
var periodicPlaybackTimer: DispatchSourceTimer?
|
|
|
|
// BIG HEART 관련 타이머
|
|
var remoteWaterTimer: DispatchSourceTimer?
|
|
var bigHeartParticleTimer: DispatchSourceTimer?
|
|
|
|
var isAvailableLikeHeart = false
|
|
|
|
private var blockedMemberIdList = Set<Int>()
|
|
|
|
private var hasInvokedJoinChannel = false
|
|
|
|
// 로컬 BIG_HEART 발신자: 원격 물 채움 연출 억제 플래그
|
|
private var suppressNextRemoteWaterFill = false
|
|
|
|
func getBlockedMemberIdList() {
|
|
userRepository.getBlockedMemberIdList()
|
|
.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<[Int]>.self, from: responseData)
|
|
|
|
if let data = decoded.data, decoded.success {
|
|
self.blockedMemberIdList.removeAll()
|
|
self.blockedMemberIdList.formUnion(data)
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
.store(in: &subscription)
|
|
}
|
|
|
|
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.rtmClientDelegate = 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()
|
|
}
|
|
|
|
startPeriodicPlaybackValidation()
|
|
}
|
|
|
|
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<GetRoomInfoResponse>.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
|
|
|
|
let rtcState = self.agora.getRtcConnectionState()
|
|
let rtcConnected = rtcState == AgoraConnectionState.connected
|
|
let rtmLoggedIn = self.agora.isRtmLoggedIn()
|
|
|
|
if (!hasInvokedJoinChannel && !(rtcConnected && rtmLoggedIn)) {
|
|
hasInvokedJoinChannel = true
|
|
self.agora.joinRtcChannel(rtcToken: data.rtcToken, channelName: data.channelName)
|
|
self.agora.rtmLogin(
|
|
creatorId: data.creatorId,
|
|
rtmToken: data.rtmToken,
|
|
channelName: data.channelName,
|
|
onConnectSuccess: self.agoraConnectSuccess,
|
|
onConnectFail: self.agoraConnectFail
|
|
)
|
|
} else {
|
|
DEBUG_LOG("joinChannel - skip (rtcConnected=\(rtcConnected), rtmLoggedIn=\(rtmLoggedIn), hasInvokedJoinChannel=\(hasInvokedJoinChannel))")
|
|
}
|
|
|
|
getTotalDonationCan()
|
|
getTotalHeartCount()
|
|
|
|
if data.isAdult && !UserDefaults.bool(forKey: .auth) {
|
|
changeIsAdult = true
|
|
}
|
|
|
|
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) { _, error in
|
|
if error == nil {
|
|
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
|
|
let rank = self.getUserRank(userId: UserDefaults.int(forKey: .userId))
|
|
self.messages.append(LiveRoomNormalChat(userId: UserDefaults.int(forKey: .userId), profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chatMessage))
|
|
|
|
self.invalidateChat()
|
|
}
|
|
|
|
onSuccess()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func donation(can: Int, message: String = "", isSecret: Bool = false) {
|
|
if isSecret && can < 10 {
|
|
popupContent = "비밀 미션은 최소 10캔 이상부터 이용이 가능합니다."
|
|
isShowPopup = true
|
|
} else if can < 1 {
|
|
popupContent = "1캔 이상 후원하실 수 있습니다."
|
|
isShowPopup = true
|
|
} else {
|
|
isLoading = true
|
|
|
|
repository.donation(roomId: AppState.shared.roomId, can: can, message: message, isSecret: isSecret)
|
|
.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<LiveRoomDonationResponse>.self, from: responseData)
|
|
|
|
self.isLoading = false
|
|
|
|
if decoded.success {
|
|
var rawMessage = ""
|
|
|
|
if isSecret {
|
|
rawMessage = "\(can)캔으로 비밀미션을 보냈습니다.🤫"
|
|
} else {
|
|
rawMessage = "\(can)캔을 후원하셨습니다.💰🪙"
|
|
}
|
|
|
|
let donationRawMessage = LiveRoomChatRawMessage(
|
|
type: isSecret ? .SECRET_DONATION : .DONATION,
|
|
message: rawMessage,
|
|
can: can,
|
|
signature: decoded.data,
|
|
signatureImageUrl: decoded.data?.imageUrl,
|
|
donationMessage: message
|
|
)
|
|
|
|
UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can)
|
|
|
|
if isSecret {
|
|
agora.sendRawMessageToPeer(peerId: String(liveRoomInfo!.creatorId), rawMessage: donationRawMessage) { [unowned self] _, error in
|
|
if error == nil {
|
|
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
|
|
self.messages.append(
|
|
LiveRoomDonationChat(
|
|
memberId: UserDefaults.int(forKey: .userId),
|
|
profileUrl: profileUrl,
|
|
nickname: nickname,
|
|
chat: rawMessage,
|
|
can: can,
|
|
donationMessage: message
|
|
)
|
|
)
|
|
|
|
addSignature(signature: decoded.data)
|
|
|
|
self.invalidateChat()
|
|
} else {
|
|
refundDonation()
|
|
}
|
|
} fail: { [unowned self] in
|
|
refundDonation()
|
|
}
|
|
} else {
|
|
agora.sendRawMessageToGroup(rawMessage: donationRawMessage) { [unowned self] _, error in
|
|
if error == nil {
|
|
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
|
|
self.messages.append(
|
|
LiveRoomDonationChat(
|
|
memberId: UserDefaults.int(forKey: .userId),
|
|
profileUrl: profileUrl,
|
|
nickname: nickname,
|
|
chat: rawMessage,
|
|
can: can,
|
|
donationMessage: message
|
|
)
|
|
)
|
|
|
|
totalDonationCan += can
|
|
addSignature(signature: decoded.data)
|
|
|
|
self.invalidateChat()
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
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)!) { [unowned self] _, error in
|
|
if error == nil {
|
|
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)!) { [unowned self] _, error in
|
|
if error == nil {
|
|
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, isAdult: Bool) {
|
|
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,
|
|
isAdult: liveRoomInfo!.isAdult != isAdult ? isAdult : nil
|
|
)
|
|
|
|
if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan && request.isAdult == nil) {
|
|
return
|
|
}
|
|
|
|
var multipartData = [MultipartFormData]()
|
|
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .withoutEscapingSlashes
|
|
|
|
if (request.title != nil || request.notice != nil || request.isAdult != 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)
|
|
} 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 shareRoom() {
|
|
if let liveRoomInfo = self.liveRoomInfo {
|
|
let params = [
|
|
"af_dp": "voiceon://",
|
|
"deep_link_value": "live",
|
|
"deep_link_sub5": "\(AppState.shared.roomId)"
|
|
]
|
|
|
|
if let shareUrl = createOneLinkUrlWithURLComponents(params: params) {
|
|
if liveRoomInfo.isPrivateRoom {
|
|
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 보이스온 비공개라이브에 초대하였습니다.\n" +
|
|
"※ 라이브 참여: \(shareUrl)\n" +
|
|
"(입장 비밀번호: \(liveRoomInfo.password!))"
|
|
} else {
|
|
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 보이스온 공개라이브에 초대하였습니다.\n" +
|
|
"※ 라이브 참여: \(shareUrl)"
|
|
}
|
|
|
|
isShowShareView = true
|
|
} else {
|
|
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
|
self.isShowErrorPopup = true
|
|
return
|
|
}
|
|
} else {
|
|
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
|
self.isShowErrorPopup = true
|
|
return
|
|
}
|
|
}
|
|
|
|
func kickOut() {
|
|
if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId {
|
|
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
|
|
agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!) { [unowned self] _, error in
|
|
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<GetLiveRoomDonationStatusResponse>.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 getHeartStatus() {
|
|
isLoading = true
|
|
repository.heartStatus(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<GetLiveRoomHeartListResponse>.self, from: responseData)
|
|
|
|
if let data = decoded.data, decoded.success {
|
|
self.heartStatus = 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
|
|
}
|
|
}
|
|
|
|
private func getTotalHeartCount() {
|
|
repository.getTotalHeartCount(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<GetLiveRoomHeartTotalResponse>.self, from: responseData)
|
|
|
|
if let data = decoded.data, decoded.success {
|
|
self.totalHeartCount = data.totalHeartCount
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
.store(in: &subscription)
|
|
}
|
|
|
|
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<GetLiveRoomDonationTotalResponse>.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<GetMemberCanResponse>.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<GetLiveRoomUserProfileResponse>.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)!) { [unowned self] _, error in
|
|
if error == nil {
|
|
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) {
|
|
blockedMemberIdList.insert(reportUserId)
|
|
|
|
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() {
|
|
blockedMemberIdList.remove(reportUserId)
|
|
|
|
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()
|
|
guard let roomId = liveRoomInfo?.roomId else { return }
|
|
if let index = noChatRoomList.firstIndex(of: 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<SpinRouletteResponse>.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: ""
|
|
)
|
|
|
|
agora.sendRawMessageToGroup(rawMessage: rouletteRawMessage) { [unowned self] _, error in
|
|
if error == nil {
|
|
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.invalidateChat()
|
|
} else {
|
|
self.refundRouletteDonation()
|
|
}
|
|
} fail: { [unowned self] in
|
|
self.refundRouletteDonation()
|
|
}
|
|
}
|
|
|
|
func stopPeriodicPlaybackValidation() {
|
|
periodicPlaybackTimer?.cancel()
|
|
periodicPlaybackTimer = nil
|
|
}
|
|
|
|
private func trackEventLiveContinuousListen30() {
|
|
userActionRepository.trackEvent(actionType: .LIVE_CONTINUOUS_LISTEN_30)
|
|
.sink { result in
|
|
switch result {
|
|
case .finished:
|
|
DEBUG_LOG("finish")
|
|
case .failure(let error):
|
|
ERROR_LOG(error.localizedDescription)
|
|
}
|
|
} receiveValue: { response in
|
|
DEBUG_LOG("트래킹 성공: \(response)")
|
|
}
|
|
.store(in: &subscription)
|
|
}
|
|
|
|
private func startPeriodicPlaybackValidation() {
|
|
let queue = DispatchQueue.global(qos: .background)
|
|
let period = DispatchTimeInterval.seconds(1800)
|
|
periodicPlaybackTimer = DispatchSource.makeTimerSource(queue: queue)
|
|
periodicPlaybackTimer?.schedule(deadline: .now() + period, repeating: period)
|
|
periodicPlaybackTimer?.setEventHandler { [weak self] in
|
|
self?.trackEventLiveContinuousListen30()
|
|
}
|
|
periodicPlaybackTimer?.resume()
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func likeHeart(
|
|
messageType: LiveRoomChatRawMessage.LiveRoomChatRawMessageType = .HEART_DONATION,
|
|
heartCount: Int = 1
|
|
) {
|
|
if isAvailableLikeHeart {
|
|
if !isLoadingLikeHeart {
|
|
isLoadingLikeHeart = true
|
|
|
|
repository.likeHeart(roomId: AppState.shared.roomId, heartCount: heartCount)
|
|
.sink { result in
|
|
switch result {
|
|
case .finished:
|
|
DEBUG_LOG("finish")
|
|
case .failure(let error):
|
|
ERROR_LOG(error.localizedDescription)
|
|
}
|
|
} receiveValue: { [unowned self] response in
|
|
self.isLoadingLikeHeart = false
|
|
let responseData = response.data
|
|
|
|
do {
|
|
let jsonDecoder = JSONDecoder()
|
|
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
|
|
|
|
if decoded.success {
|
|
UserDefaults.set(UserDefaults.int(forKey: .can) - heartCount, forKey: .can)
|
|
|
|
let donationRawMessage = LiveRoomChatRawMessage(
|
|
type: messageType,
|
|
message: "",
|
|
can: heartCount,
|
|
donationMessage: nil
|
|
)
|
|
|
|
agora.sendRawMessageToGroup(rawMessage: donationRawMessage) { [unowned self] _, error in
|
|
if error == nil {
|
|
let (nickname, _) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
|
|
self.addHeartMessage(nickname: nickname)
|
|
|
|
totalHeartCount += heartCount
|
|
|
|
if messageType == .BIG_HEART_DONATION {
|
|
// 로컬 발신: 수신 알림 직후 즉시 폭발 연출만 보이고,
|
|
// 물 채움(1초)은 발신 측에서는 생략
|
|
self.suppressNextRemoteWaterFill = true
|
|
addBigHeartAnimation()
|
|
} else {
|
|
addHeart()
|
|
}
|
|
|
|
self.invalidateChat()
|
|
} else {
|
|
self.refundDonation()
|
|
}
|
|
} fail: { [unowned self] in
|
|
self.refundDonation()
|
|
}
|
|
}
|
|
} catch {
|
|
refundDonation()
|
|
}
|
|
}
|
|
.store(in: &subscription)
|
|
}
|
|
} else {
|
|
isShowNoticeLikeHeart = true
|
|
}
|
|
}
|
|
|
|
private func addHeart() {
|
|
let heart = Heart(
|
|
id: UUID(),
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
opacity: 1,
|
|
speed: CGFloat.random(in: 1...3),
|
|
scale: 0.5,
|
|
direction: Bool.random() ? "left" : "right"
|
|
)
|
|
hearts.append(heart)
|
|
|
|
if hearts.count == 1 {
|
|
startHeartTimer()
|
|
}
|
|
}
|
|
|
|
private func updateHearts() {
|
|
for i in (0..<hearts.count).reversed() {
|
|
hearts[i].offsetY -= hearts[i].speed * 2 // Y축으로 이동
|
|
hearts[i].opacity -= hearts[i].speed * 0.004444444444 // 투명도 감소
|
|
hearts[i].scale += 0.0067
|
|
|
|
if hearts[i].direction == "left" {
|
|
hearts[i].offsetX -= 0.8
|
|
|
|
if hearts[i].offsetX <= -22 {
|
|
hearts[i].direction = "right"
|
|
}
|
|
} else {
|
|
hearts[i].offsetX += 0.8
|
|
|
|
if hearts[i].offsetX >= 22 {
|
|
hearts[i].direction = "left"
|
|
}
|
|
}
|
|
|
|
// 화면을 벗어나거나 완전히 사라진 하트는 삭제
|
|
if hearts[i].scale >= 1 || hearts[i].opacity <= 0 || hearts[i].offsetY < -450 {
|
|
hearts.remove(at: i)
|
|
|
|
if hearts.isEmpty {
|
|
stopHeartTimer()
|
|
}
|
|
}
|
|
}
|
|
|
|
// 최대 하트 개수 제한
|
|
if hearts.count > 100 {
|
|
hearts.removeFirst()
|
|
}
|
|
}
|
|
|
|
private func addHeartMessage(nickname: String) {
|
|
if heartNickname != nil {
|
|
self.heartNicknameList.append(nickname)
|
|
} else {
|
|
self.heartNickname = nickname
|
|
}
|
|
}
|
|
|
|
private func showNextHeartMessage() {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
if let nextHeartNickname = self.heartNicknameList.first {
|
|
self.heartNickname = nextHeartNickname
|
|
self.heartNicknameList.removeFirst()
|
|
} else {
|
|
self.heartNickname = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func startHeartTimer() {
|
|
if heartTimer == nil {
|
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
|
timer.schedule(deadline: .now(), repeating: 0.033)
|
|
timer.setEventHandler { [unowned self] in
|
|
DispatchQueue.main.async {
|
|
self.updateHearts()
|
|
}
|
|
}
|
|
timer.resume()
|
|
self.heartTimer = timer
|
|
}
|
|
}
|
|
|
|
func stopHeartTimer() {
|
|
heartTimer?.cancel()
|
|
heartTimer = nil
|
|
}
|
|
|
|
private func addBigHeartAnimation() {
|
|
// 로컬 발신 직후 1회는 원격 물 채움 연출을 생략하고 바로 폭발만 실행
|
|
if suppressNextRemoteWaterFill {
|
|
suppressNextRemoteWaterFill = false
|
|
spawnHeartExplosion()
|
|
startParticlesTimer()
|
|
return
|
|
}
|
|
// 요구사항 변경: 물 채우기(1초) 연출 제거.
|
|
// 가득 찬 하트를 잠깐(0.15초) 보여준 뒤 폭발 이펙트를 실행.
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.remoteWaterTimer?.cancel()
|
|
self.remoteWaterTimer = nil
|
|
self.remoteWavePhase = 0
|
|
self.remoteWaterProgress = 1.0
|
|
self.isShowRemoteBigHeart = true
|
|
DEBUG_LOG("BIG_HEART: show filled heart, then explode after 0.30s")
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.30) { [weak self] in
|
|
guard let self = self else { return }
|
|
self.isShowRemoteBigHeart = false
|
|
self.remoteWaterProgress = 0
|
|
self.remoteWavePhase = 0
|
|
self.spawnHeartExplosion()
|
|
self.startParticlesTimer()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - BIG HEART - Remote Water Fill
|
|
private func startRemoteWaterFill(duration: Double, completion: @escaping () -> Void) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.isShowRemoteBigHeart = true
|
|
self.remoteWaterProgress = 0
|
|
self.remoteWavePhase = 0
|
|
self.remoteWaterTimer?.cancel()
|
|
self.remoteWaterTimer = nil
|
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
|
let dt: Double = 1.0 / 60.0
|
|
var elapsed: Double = 0
|
|
timer.schedule(deadline: .now(), repeating: dt)
|
|
timer.setEventHandler { [weak self] in
|
|
guard let self = self else { return }
|
|
elapsed += dt
|
|
let p = min(max(elapsed / duration, 0), 1)
|
|
self.remoteWaterProgress = p
|
|
self.remoteWavePhase += 0.35
|
|
if p >= 1.0 {
|
|
self.remoteWaterTimer?.cancel()
|
|
self.remoteWaterTimer = nil
|
|
// 물 채움 종료 후 하트 숨김 처리
|
|
self.isShowRemoteBigHeart = false
|
|
self.remoteWaterProgress = 0
|
|
self.remoteWavePhase = 0
|
|
completion()
|
|
}
|
|
}
|
|
timer.resume()
|
|
self.remoteWaterTimer = timer
|
|
}
|
|
}
|
|
|
|
// MARK: - BIG HEART - Explosion Particles
|
|
private func spawnHeartExplosion() {
|
|
// 중심(0,0)에서 폭발하는 작은 하트 파편 생성
|
|
// 색상은 View에서 #ff959a로 렌더링
|
|
var particles: [BigHeartParticle] = []
|
|
let count = Int.random(in: 20...35) // 요구사항: 파편 개수 20~35개
|
|
for i in 0..<count {
|
|
// 각도 분산 + 약간의 랜덤성
|
|
let baseAngle = (Double(i) / Double(count)) * .pi * 2
|
|
let jitter = Double.random(in: -0.35...0.35)
|
|
let angle = baseAngle + jitter
|
|
// 속도 (px/s)와 위로 향하는 바이어스
|
|
let speed = CGFloat.random(in: 220...360)
|
|
let vx = CGFloat(cos(angle)) * speed
|
|
var vy = CGFloat(sin(angle)) * speed
|
|
// 조금 더 위로 포물선을 그리도록 초기 위쪽 임펄스 추가
|
|
vy -= CGFloat.random(in: 120...220)
|
|
// 크기: 20~65pt
|
|
let size = CGFloat.random(in: 20...65)
|
|
let scale: CGFloat = 1.0 // 최종 크기 유지(시간 경과에 따라 약간 축소)
|
|
let life: Double = Double.random(in: 1.0...1.5)
|
|
let particle = BigHeartParticle(
|
|
id: UUID(),
|
|
x: 0,
|
|
y: 0,
|
|
vx: vx,
|
|
vy: vy,
|
|
opacity: 1.0,
|
|
scale: scale,
|
|
rotation: Double.random(in: 0...360),
|
|
life: life,
|
|
size: size,
|
|
isRain: false,
|
|
gravityScale: 1.0
|
|
)
|
|
particles.append(particle)
|
|
}
|
|
// 기록: 낙하 효과에 동일 개수/크기를 사용하기 위해 저장
|
|
self.lastExplosionCount = count
|
|
self.lastExplosionSizes = particles.map { $0.size }
|
|
DispatchQueue.main.async {
|
|
self.bigHeartParticles = particles
|
|
}
|
|
// 폭발 파편이 모두 사라진 직후 낙하 하트를 생성하도록 플래그 설정
|
|
self.shouldSpawnRainAfterExplosionEnds = true
|
|
}
|
|
|
|
// MARK: - BIG HEART - Rain Particles (after explosion)
|
|
private func spawnHeartRainFromLastExplosion() -> [BigHeartParticle] {
|
|
let count = max(0, self.lastExplosionCount)
|
|
guard count > 0 else { return [] }
|
|
let bounds = UIScreen.main.bounds
|
|
let startYBase = -bounds.height * 0.5 - 40 // 화면 위쪽 바깥에서 시작(기본)
|
|
var rains: [BigHeartParticle] = []
|
|
rains.reserveCapacity(count)
|
|
for i in 0..<count {
|
|
// 화면 너비에 균등 분포 + 약간의 지터
|
|
let ratio = (CGFloat(i) + 0.5) / CGFloat(count)
|
|
var x = (ratio - 0.5) * bounds.width
|
|
x += CGFloat.random(in: -20...20)
|
|
// 크기는 폭발과 동일 인덱스를 사용(없으면 기본 범위)
|
|
let size: CGFloat
|
|
if i < self.lastExplosionSizes.count {
|
|
size = self.lastExplosionSizes[i]
|
|
} else {
|
|
size = CGFloat.random(in: 20...65)
|
|
}
|
|
let vx = CGFloat.random(in: -60...60)
|
|
let vy = CGFloat.random(in: 10...500) // 상한 220→500로 확대하여 낙하 속도 분산 강화
|
|
let startY = startYBase + CGFloat.random(in: -80...20) // 시작 높이 지터로 낙하 거리 다양화
|
|
let sizeNorm = max(0, min(1, (size - 30) / 120))
|
|
var gScale = 0.8 + 0.8 * sizeNorm // 0.8..1.6 (size가 클수록 빠르게)
|
|
gScale += CGFloat.random(in: -0.15...0.15) // 약간의 지터
|
|
// variation 1.3x: 평균(1.0) 대비 편차를 1.3배 확대
|
|
gScale = 1.0 + (gScale - 1.0) * CGFloat(1.3)
|
|
gScale = max(0.5, min(1.5, gScale))
|
|
let p = BigHeartParticle(
|
|
id: UUID(),
|
|
x: x,
|
|
y: startY,
|
|
vx: vx,
|
|
vy: vy,
|
|
opacity: 1.0,
|
|
scale: 1.0,
|
|
rotation: Double.random(in: 0...360),
|
|
life: 4.0, // 최대 4초 유지
|
|
size: size,
|
|
isRain: true,
|
|
gravityScale: gScale
|
|
)
|
|
rains.append(p)
|
|
}
|
|
return rains
|
|
}
|
|
|
|
private func startParticlesTimer() {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
// 이미 타이머가 동작 중이면 재시작하지 않음
|
|
guard self.bigHeartParticleTimer == nil else { return }
|
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
|
let dt: Double = 1.0 / 60.0
|
|
let gravity: CGFloat = 600 // px/s^2
|
|
timer.schedule(deadline: .now(), repeating: dt)
|
|
timer.setEventHandler { [weak self] in
|
|
guard let self = self else { return }
|
|
// 폭발 파편 존재 여부(이전 프레임)
|
|
let hadExplosionBefore = self.bigHeartParticles.contains(where: { !$0.isRain })
|
|
var next: [BigHeartParticle] = []
|
|
next.reserveCapacity(self.bigHeartParticles.count)
|
|
// 화면 기준(오버레이 중심 좌표계)
|
|
let bounds = UIScreen.main.bounds
|
|
let floorY = bounds.height * 0.5 - 60 // 바닥 임계치
|
|
for var p in self.bigHeartParticles {
|
|
// 물리 업데이트
|
|
p.vy += gravity * p.gravityScale * CGFloat(dt)
|
|
p.x += p.vx * CGFloat(dt)
|
|
p.y += p.vy * CGFloat(dt)
|
|
// 회전 (속도에 비례한 간단한 모델)
|
|
p.rotation += Double((abs(p.vx) + abs(p.vy)) * 0.02 * CGFloat(dt))
|
|
// 수명/투명도 감소(폭발/낙하 각각의 기준 수명 적용)
|
|
p.life -= dt
|
|
let baseLife: Double = p.isRain ? 4.0 : 1.2
|
|
p.opacity = max(0, min(1, p.life / baseLife))
|
|
// 잔상감 주기 위해 살짝 스케일 감소
|
|
p.scale *= 0.995
|
|
// 낙하 파편은 바닥에 닿으면 제거
|
|
if p.isRain && p.y >= floorY {
|
|
continue
|
|
}
|
|
if p.life > 0 && p.opacity > 0.01 {
|
|
next.append(p)
|
|
}
|
|
}
|
|
// 폭발 파편 존재 여부(현재 프레임)
|
|
let hasExplosionAfter = next.contains(where: { !$0.isRain })
|
|
// 요구사항: 폭발 파편이 모두 사라진 직후 비를 시작
|
|
if hadExplosionBefore && !hasExplosionAfter && self.shouldSpawnRainAfterExplosionEnds {
|
|
let rains = self.spawnHeartRainFromLastExplosion()
|
|
if !rains.isEmpty {
|
|
next.append(contentsOf: rains)
|
|
}
|
|
self.shouldSpawnRainAfterExplosionEnds = false
|
|
}
|
|
self.bigHeartParticles = next
|
|
if next.isEmpty {
|
|
self.bigHeartParticleTimer?.cancel()
|
|
self.bigHeartParticleTimer = nil
|
|
}
|
|
}
|
|
timer.resume()
|
|
self.bigHeartParticleTimer = timer
|
|
}
|
|
}
|
|
|
|
private func invalidateChat() {
|
|
messageChangeFlag.toggle()
|
|
if messages.count > 100 {
|
|
messages.remove(at: 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
|
func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
|
|
let activeSpeakerIds = speakers
|
|
.filter { $0.volume > 0 }
|
|
.map { $0.uid }
|
|
|
|
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: AgoraRtmClientDelegate {
|
|
func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveMessageEvent event: AgoraRtmMessageEvent) {
|
|
DEBUG_LOG("Message received.\n channel: \(event.channelName), publisher: \(event.publisher)")
|
|
|
|
let rawMessage = event.message.rawData
|
|
let textMessage = event.message.stringData
|
|
let publisher = event.publisher
|
|
let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(publisher)!)
|
|
|
|
if let message = rawMessage {
|
|
let rawMessageString = String(data: message, 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 = "\(nickname)님이 스피커 요청을 했어요!\n스퍼커로 초대할까요?"
|
|
self.popupCancelTitle = "건너뛰기"
|
|
self.popupCancelAction = {
|
|
self.isShowPopup = false
|
|
}
|
|
self.popupConfirmTitle = "스피커로 초대"
|
|
self.popupConfirmAction = {
|
|
self.isShowPopup = false
|
|
if self.liveRoomInfo!.speakerList.count <= 5 {
|
|
self.requestSpeakerAllow(publisher)
|
|
} 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()
|
|
}
|
|
}
|
|
|
|
do {
|
|
let jsonDecoder = JSONDecoder()
|
|
let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: message)
|
|
|
|
if decoded.type == .SECRET_DONATION {
|
|
self.messages.append(
|
|
LiveRoomDonationChat(
|
|
memberId: Int(publisher)!,
|
|
profileUrl: profileUrl,
|
|
nickname: nickname,
|
|
chat: decoded.message,
|
|
can: decoded.can,
|
|
donationMessage: decoded.donationMessage ?? ""
|
|
)
|
|
)
|
|
|
|
if let signature = decoded.signature {
|
|
self.addSignature(signature: signature)
|
|
} else if let imageUrl = decoded.signatureImageUrl {
|
|
self.addSignatureImage(imageUrl: imageUrl)
|
|
}
|
|
} else if decoded.type == .DONATION {
|
|
self.messages.append(
|
|
LiveRoomDonationChat(
|
|
memberId: Int(publisher)!,
|
|
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()
|
|
} else if decoded.type == .HEART_DONATION {
|
|
self.addHeartMessage(nickname: nickname)
|
|
self.totalHeartCount += decoded.can
|
|
self.addHeart()
|
|
} else if decoded.type == .BIG_HEART_DONATION {
|
|
self.addHeartMessage(nickname: nickname)
|
|
self.totalHeartCount += decoded.can
|
|
self.addBigHeartAnimation()
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
}
|
|
|
|
if let message = textMessage {
|
|
let memberId = Int(publisher) ?? 0
|
|
let rank = getUserRank(userId: memberId)
|
|
|
|
if !message.trimmingCharacters(in: .whitespaces).isEmpty && !blockedMemberIdList.contains(memberId) {
|
|
messages.append(LiveRoomNormalChat(userId: memberId, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: message))
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async { [unowned self] in
|
|
self.invalidateChat()
|
|
}
|
|
}
|
|
|
|
func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceivePresenceEvent event: AgoraRtmPresenceEvent) {
|
|
DEBUG_LOG("didReceivePresenceEvent - \(event.type) - \(String(describing: event.publisher))")
|
|
let eventType = event.type
|
|
|
|
if let memberId = event.publisher {
|
|
if eventType == .remoteJoinChannel {
|
|
getRoomInfo(userId: Int(memberId)!) { [unowned self] nickname in
|
|
if !nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && isEntryMessageEnabled {
|
|
DispatchQueue.main.async { [unowned self] in
|
|
self.messages.append(LiveRoomJoinChat(nickname: nickname))
|
|
self.invalidateChat()
|
|
}
|
|
}
|
|
}
|
|
} else if eventType == .remoteLeaveChannel {
|
|
if let liveRoomInfo = liveRoomInfo, liveRoomInfo.creatorId != Int(memberId)! {
|
|
getRoomInfo()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
|
|
DEBUG_LOG("Signaling link state change current state is: \(event.currentState.rawValue) previous state is :\(event.previousState.rawValue)")
|
|
}
|
|
}
|