// // LNRoomViewModel.swift // Gami // // Created by OneeChan on 2026/3/9. // import Foundation import AtomicXCore import Combine protocol LNRoomViewModelNotify { func onRoomMessageChanged(messages: [LNRoomMessageItem]) func onRoomSeatsChanged() func onRoomSpeakingUsersChanged() func onRoomSeatApplyChanged() func onRoomInfoChanged() func onMySeatApplyChanged() func onRoomClosed() } extension LNRoomViewModelNotify { func onRoomMessageChanged(messages: [LNRoomMessageItem]) { } func onRoomSeatsChanged() { } func onRoomSpeakingUsersChanged() { } func onRoomSeatApplyChanged() { } func onRoomInfoChanged() { } func onMySeatApplyChanged() { } func onRoomClosed() { } } enum LNRoomSeatNum: Int, CaseIterable, Comparable { case none = -1 case host = 0 case guest case mic1 case mic2 case mic3 case mic4 case mic5 case mic6 case mic7 case mic8 static func < (lhs: LNRoomSeatNum, rhs: LNRoomSeatNum) -> Bool { lhs.rawValue < rhs.rawValue } var title: String { if case .host = self { .init(key: "A00328") } else if case .guest = self { .init(key: "A00329") } else { .init(key: "A00326", rawValue - 1) } } var giftHeaderTitle: String { if case .host = self { .init(key: "A00328") } else if case .guest = self { .init(key: "A00329") } else { "Mic \(rawValue - 1)" } } } enum LNApplyingSeatType: Int, Decodable { case none case guest case playmate } enum LNApplySeatErrCode: Int { case noPermission = 50001 } class LNRoomViewModel: NSObject { let roomId: String private let seatStore: LiveSeatStore private let guestStore: CoGuestStore private let messageStore: BarrageStore private let audienceStore: LiveAudienceStore private(set) var seatsInfo: [LNRoomSeatItem] = [] private(set) var roomInfo = LNRoomInfo() private(set) var speakingUser: [String] = [] private(set) var lastmessage: Barrage? = nil private(set) var seatApplyCount: Int = 0 private(set) var waitingForSeat: LNApplyingSeatType = .none { didSet { if oldValue != waitingForSeat { LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onMySeatApplyChanged() } } } } private let systemHandlerQueue = DispatchQueue(label: "com.gami.room.system.message", attributes: .concurrent) private var isLoadingGiftList = false private(set) var giftList: [LNGiftItemVO] = [] init(roomId: String) { self.roomId = roomId seatStore = LiveSeatStore.create(liveID: roomId) guestStore = CoGuestStore.create(liveID: roomId) messageStore = BarrageStore.create(liveID: roomId) audienceStore = LiveAudienceStore.create(liveID: roomId) super.init() setupSeatObservers() setupMessageObservers() setupRoomInfoObserver() setupAudienceEventObservers() reloadGiftList() LNRoomManager.shared.updateCurRoom(self) } func closeRoom() { LNRoomManager.shared.closeRoom { success in LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() } } } func leaveRoom() { LNRoomManager.shared.leaveRoom { success in LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() } } } } // MARK: 麦位管理 - 普通用户 extension LNRoomViewModel { var mySeatInfo: LNRoomSeatItem? { seatsInfo.first { $0.uid.isMyUid } } private func setupSeatObservers() { seatStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in guard let self else { return } let wasOnMic = mySeatInfo != nil let seats = state.seatList var hasChanged = seats.count != seatsInfo.count var newSeats: [LNRoomSeatItem] = [] for seat in seats { let item = seatsInfo.first(where: { $0.index.rawValue == seat.index }) ?? LNRoomSeatItem(index: LNRoomSeatNum(rawValue: seat.index) ?? .none) hasChanged = item.update(seat) || hasChanged newSeats.append(item) } if hasChanged { seatsInfo = newSeats if !wasOnMic, mySeatInfo != nil { // 自己上麦 waitingForSeat = .none if roomInfo.forbidAudio { DeviceStore.shared.closeLocalMicrophone() } else { DeviceStore.shared.openLocalMicrophone(completion: nil) } } LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatsChanged() } } let speakings = state.speakingUsers.filter { $0.value > 0 }.map { $0.key }.sorted { $1 > $0 } if speakings != speakingUser { speakingUser = speakings LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSpeakingUsersChanged() } } }.store(in: &cancellables) } // 申请列表 func getApplyList(type: LNRoomApplySeatType, next: String?, filter: String?, handler: @escaping (LNRoomMicApplyListResponse?) -> Void) { LNHttpManager.shared.getApplySeatList(roomId: roomId, searchType: type, filter: filter, size: 30, next: next ?? "") { res, err in runOnMain { handler(res) } res?.list.forEach { LNProfileManager.shared.updateProfileCache(uid: $0.user.userNo, name: $0.user.nickname, avatar: $0.user.avatar) } if let err { showToast(err.errorDesc) } } } // 申请列表的技能类别 func getApplyListCategory(handler: @escaping (LNRoomGameCategoryResponse?) -> Void) { LNHttpManager.shared.getApplySeatCategory(roomId: roomId) { res, err in runOnMain { handler(res) } if let err { showToast(err.errorDesc) } } } // 申请上麦 func applySeat(index: LNRoomSeatNum, handler: @escaping (Bool) -> Void) { LNPermissionHelper.requestMicrophoneAccess { [weak self] success in guard let self else { return } guard success else { showToast(.init(key: "A00362")) return } LNHttpManager.shared.applySeat(roomId: roomId, index: index.rawValue) { [weak self] err in runOnMain { handler(err == nil) } if let err { if case .serverError(let code, _) = err, code == LNApplySeatErrCode.noPermission.rawValue { showToast(err.errorDesc, icon: .icAbout.withTintColor(.fill)) } else { showToast(err.errorDesc) } } else if let self { waitingForSeat = index == .guest ? .guest : index != .host ? .playmate : .none } } } } // 取消上麦申请 func cancelSeatApply(handler: @escaping (Bool) -> Void) { LNHttpManager.shared.cancelApplySeat(roomId: roomId) { [weak self] err in runOnMain { handler(err == nil) } if let err { showToast(err.errorDesc) } else if let self { waitingForSeat = .none } } } // 主动下麦 func leaveSeat(handler: @escaping (Bool) -> Void) { seatStore.leaveSeat { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } } // MARK: 麦位管理 - 管理员 extension LNRoomViewModel { // 踢人下麦 func kickUserOffSeat(uid: String, handler: @escaping (Bool) -> Void) { seatStore.kickUserOutOfSeat(userID: uid) { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } func clearApplyList(type: LNRoomApplySeatType, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.clearApplySeatList(roomId: roomId, searchType: type) { err in runOnMain { handler(err == nil) } if let err { showToast(err.errorDesc) } } } // 接受上麦申请 func acceptSeatApply(applyId: String, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.handleApplySeat(roomId: roomId, applyId: applyId, accept: true) { err in runOnMain { handler(err == nil) } if let err { showToast(err.errorDesc) } } } // 拒绝上麦申请 func rejectSeatApply(applyId: String, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.handleApplySeat(roomId: roomId, applyId: applyId, accept: false) { err in runOnMain { handler(err == nil) } if let err { showToast(err.errorDesc) } } } // 邀请上麦 func inviteUserToSeat(uid: String, index: LNRoomSeatNum, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.inviteUserToSeat(roomId: roomId, uid: uid, index: index.rawValue) { err in runOnMain { handler(err == nil) } if let err { showToast(err.errorDesc) } } } // 关闭麦位 func lockSeat(num: Int, handler: @escaping (Bool) -> Void) { seatStore.lockSeat(seatIndex: num) { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } // 解锁麦位 func unlockSeat(num: Int, handler: @escaping (Bool) -> Void) { seatStore.unlockSeat(seatIndex: num) { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } // 用户列表 func getRoomUserList(next: String?, playmete: Bool, filter: String?, handler: @escaping (LNRoomUserListResponse?) -> Void) { LNHttpManager.shared.getRoomUserList( roomId: roomId, size: 30, next: next ?? "", playmete: playmete, filter: filter) { res, err in runOnMain { handler(res) } res?.list.forEach { LNProfileManager.shared.updateProfileCache(uid: $0.user.userNo, name: $0.user.nickname, avatar: $0.user.avatar) } if let err { showToast(err.errorDesc) } } } // 申请列表的技能类别 func getPlaymateCategory(handler: @escaping (LNRoomGameCategoryResponse?) -> Void) { LNHttpManager.shared.getRoomPlaymateCategory(roomId: roomId) { res, err in runOnMain { handler(res) } if let err { showToast(err.errorDesc) } } } } // MARK: 麦克风管理 - 普通用户 extension LNRoomViewModel { // 关闭自己麦克风 func muteMySeat() { seatStore.muteMicrophone() } // 打开自己麦克风 func unmuteMySeat(handler: @escaping (Bool) -> Void) { let unmute = { [weak self] in guard let self else { return } seatStore.unmuteMicrophone { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } if DeviceStore.shared.state.value.microphoneStatus == .off { DeviceStore.shared.openLocalMicrophone { result in switch result { case .success: unmute() case .failure(let err): showTencentError(err) handler(false) } } } else { unmute() } } } // MARK: 麦克风管理 - 管理员 extension LNRoomViewModel { // 禁止某人的麦克风 func muteSeat(uid: String, handler: @escaping (Bool) -> Void) { seatStore.closeRemoteMicrophone(userID: uid) { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } // 解锁某人麦克风 func unmuteSeat(uid: String, handler: @escaping (Bool) -> Void) { seatStore.openRemoteMicrophone(userID: uid, policy: .unlockOnly) { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } } // MARK: 公屏 extension LNRoomViewModel { private func setupMessageObservers() { messageStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in guard let self else { return } var newMessage: [Barrage] = [] if let lastId = lastmessage?.sequence, let index = state.messageList.lastIndex(where: { $0.sequence == lastId }) { newMessage.append(contentsOf: state.messageList[(index + 1)...]) } else { newMessage.append(contentsOf: state.messageList) } lastmessage = newMessage.last systemHandlerQueue.async { [weak self] in guard let self else { return } var chatMessage: [LNRoomMessageItem] = [] var systemMessage: [LNRoomPushMessage] = [] newMessage.forEach { switch $0.messageType { case .text: if let messageItems = LNRoomUserMessage(info: $0)?.messageItems, !messageItems.isEmpty { chatMessage.append(contentsOf: messageItems) } case .custom: if let item = LNRoomPushMessage(info: $0) { systemMessage.append(item) if let messageItems = item.messageItems, !messageItems.isEmpty { chatMessage.append(contentsOf: messageItems) } } default: break } } if !chatMessage.isEmpty { LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomMessageChanged(messages: chatMessage) } } handleSystemMessage(messages: systemMessage) } }.store(in: &cancellables) } func sendChatMessage(text: String, handler: @escaping (Bool) -> Void) { messageStore.sendTextMessage(text: text, extensionInfo: [ LNRoomChatMessageTypeKey: LNRoomChatMessageType.chat.rawValue ]) { result in switch result { case .success: handler(true) case .failure(let err): showTencentError(err) handler(false) } } } } // MARK: 房间信息 extension LNRoomViewModel { private func setupRoomInfoObserver() { LNRoomManager.shared.liveListStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in guard let self else { return } if state.currentLive.liveID.isEmpty { return } if !state.currentLive.roomType.contains(.playmate) { showToast(.init(key: "A00389")) leaveRoom() return } if roomInfo.update(state.currentLive) { LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomInfoChanged() } } let oldCount = seatApplyCount if state.currentLive.applySeatCount != seatApplyCount { seatApplyCount = state.currentLive.applySeatCount } if oldCount != seatApplyCount { LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatApplyChanged() } } }.store(in: &cancellables) LNRoomManager.shared.liveListStore.liveListEventPublisher.receive(on: DispatchQueue.main).sink { [weak self] event in guard let self else { return } switch event { case .onLiveEnded(let roomId, _, _): if self.roomId == roomId { LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() } } case .onKickedOutOfLive(let roomId, _, _): if self.roomId == roomId { LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() } } default: break } }.store(in: &cancellables) } func updateRoomInfo(name: String, cover: String, forbidAudio: Bool, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.updateRoom(roomId: roomId, roomTitle: name, roomCover: cover, forbidAudio: forbidAudio) { err in runOnMain { handler(err == nil) } if let err { showToast(err.errorDesc) } } } } // MARK: 观众事件 extension LNRoomViewModel { private func setupAudienceEventObservers() { audienceStore.liveAudienceEventPublisher.receive(on: DispatchQueue.main).sink { [weak self] event in guard let self else { return } switch event { case .onAudienceJoined(let info): messageStore.appendLocalTip(message: Barrage.welcomeMessage(userInfo: info)) default: break } }.store(in: &cancellables) } } // MARK: 礼物 extension LNRoomViewModel { func sendGift(gift: LNGiftItemVO, to: [String], count: Int, handler: @escaping (Bool) -> Void) { let param = LNSendGiftParams() param.roomId = roomId param.giftId = gift.id param.userIds = to param.quantity = count LNGiftManager.shared.sendGift(params: param) { success in handler(success) } } func reloadGiftList() { guard !isLoadingGiftList else { return } isLoadingGiftList = true LNGiftManager.shared.fetchGiftList(roomId: roomId) { [weak self] list in guard let self else { return } if let list, !list.isEmpty { giftList = list } isLoadingGiftList = false } } } extension LNRoomViewModel { private func handleSystemMessage(messages: [LNRoomPushMessage]) { messages.forEach { message in if message.cmd == .MicClear, let cmd: LNRoomMicClearMessage = message.decodeCmdMessage(), cmd.type == self.waitingForSeat { self.waitingForSeat = .none } } } }