LNRoomViewModel.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. //
  2. // LNRoomViewModel.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/9.
  6. //
  7. import Foundation
  8. import AtomicXCore
  9. import Combine
  10. protocol LNRoomViewModelNotify {
  11. func onRoomMessageChanged()
  12. func onRoomSeatsChanged()
  13. func onRoomSpeakingUsersChanged()
  14. func onRoomSeatApplyChanged()
  15. func onRoomInfoChanged()
  16. func onRoomClosed()
  17. }
  18. extension LNRoomViewModelNotify {
  19. func onRoomMessageChanged() { }
  20. func onRoomSeatsChanged() { }
  21. func onRoomSpeakingUsersChanged() { }
  22. func onRoomSeatApplyChanged() { }
  23. func onRoomInfoChanged() { }
  24. func onRoomClosed() { }
  25. }
  26. class LNRoomViewModel: NSObject {
  27. let roomId: String
  28. private let seatStore: LiveSeatStore
  29. private let guestStore: CoGuestStore
  30. private let messageStore: BarrageStore
  31. private(set) var seatsInfo: [LNRoomSeatItem] = []
  32. private(set) var roomInfo = LNRoomInfo()
  33. private(set) var speakingUser: [String] = []
  34. private let maxMessageCount = 300
  35. private let messageRefreshInterval: TimeInterval = 1
  36. private var messageRefreshTask: String?
  37. init(roomId: String) {
  38. self.roomId = roomId
  39. seatStore = LiveSeatStore.create(liveID: roomId)
  40. guestStore = CoGuestStore.create(liveID: roomId)
  41. messageStore = BarrageStore.create(liveID: roomId)
  42. super.init()
  43. setupSeatObservers()
  44. setupApplyObservers()
  45. setupMessageObservers()
  46. setupRoomInfoObserver()
  47. }
  48. func closeRoom() {
  49. LNRoomManager.shared.closeRoom { success in
  50. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() }
  51. }
  52. }
  53. }
  54. // MARK: 麦位管理 - 普通用户
  55. extension LNRoomViewModel {
  56. var isOnMic: Bool {
  57. seatsInfo.contains { $0.uid == myUid }
  58. }
  59. private func setupSeatObservers() {
  60. seatStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  61. guard let self else { return }
  62. let seats = state.seatList
  63. var hasChanged = seats.count != seatsInfo.count
  64. var newSeats: [LNRoomSeatItem] = []
  65. for seat in seats {
  66. let item = seatsInfo.first(where: { $0.index == seat.index }) ?? LNRoomSeatItem(index: seat.index)
  67. hasChanged = hasChanged || item.update(seat)
  68. newSeats.append(item)
  69. }
  70. if hasChanged {
  71. seatsInfo = newSeats
  72. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatsChanged() }
  73. }
  74. let speakings = state.speakingUsers.filter { $0.value > 0 }.map { $0.key }.sorted { $1 > $0 }
  75. if speakings != speakingUser {
  76. speakingUser = speakings
  77. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSpeakingUsersChanged() }
  78. }
  79. }.store(in: &cancellables)
  80. }
  81. // 申请上麦
  82. func applySeat(handler: @escaping (Bool) -> Void) {
  83. guestStore.applyForSeat(timeout: 0, extraInfo: nil) { result in
  84. switch result {
  85. case .success:
  86. handler(true)
  87. case .failure(let err):
  88. showToast(err.localizedDescription)
  89. handler(false)
  90. }
  91. }
  92. }
  93. // 取消上麦申请
  94. func cancelSeatApply(handler: @escaping (Bool) -> Void) {
  95. guestStore.cancelApplication { result in
  96. switch result {
  97. case .success:
  98. handler(true)
  99. case .failure(let err):
  100. showToast(err.localizedDescription)
  101. handler(false)
  102. }
  103. }
  104. }
  105. // 主动下麦
  106. func offSeat(handler: @escaping (Bool) -> Void) {
  107. guestStore.disConnect { result in
  108. switch result {
  109. case .success:
  110. handler(true)
  111. case .failure(let err):
  112. showToast(err.localizedDescription)
  113. handler(false)
  114. }
  115. }
  116. }
  117. // 接受邀请
  118. func acceptSeatInvite(uid: String, handler: @escaping (Bool) -> Void) {
  119. guestStore.acceptInvitation(inviterID: uid) { result in
  120. switch result {
  121. case .success:
  122. handler(true)
  123. case .failure(let err):
  124. showToast(err.localizedDescription)
  125. handler(false)
  126. }
  127. }
  128. }
  129. // 拒绝邀请
  130. func rejectSeatInvite(uid: String, handler: @escaping (Bool) -> Void) {
  131. guestStore.rejectInvitation(inviterID: uid) { result in
  132. switch result {
  133. case .success:
  134. handler(true)
  135. case .failure(let err):
  136. showToast(err.localizedDescription)
  137. handler(false)
  138. }
  139. }
  140. }
  141. }
  142. // MARK: 麦位管理 - 管理员
  143. extension LNRoomViewModel {
  144. var curSeatApplications: [LNRoomSeatApplyItem] {
  145. guestStore.state.value.applicants.map {
  146. LNRoomSeatApplyItem(userNo: $0.userID, avatar: $0.avatarURL,
  147. name: $0.userName, gender: .unknow,
  148. time: 0, category: "")
  149. }
  150. }
  151. private func setupApplyObservers() {
  152. guestStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  153. guard let self else { return }
  154. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatApplyChanged() }
  155. }.store(in: &cancellables)
  156. }
  157. // 踢人下麦
  158. func kickUserOffSeat(uid: String, handler: @escaping (Bool) -> Void) {
  159. seatStore.kickUserOutOfSeat(userID: uid) { result in
  160. switch result {
  161. case .success:
  162. handler(true)
  163. case .failure(let err):
  164. showToast(err.localizedDescription)
  165. handler(false)
  166. }
  167. }
  168. }
  169. // 接受上麦申请
  170. func acceptSeatApply(uid: String, handler: @escaping (Bool) -> Void) {
  171. guestStore.acceptApplication(userID: uid) { result in
  172. switch result {
  173. case .success:
  174. handler(true)
  175. case .failure(let err):
  176. showToast(err.localizedDescription)
  177. handler(false)
  178. }
  179. }
  180. }
  181. // 拒绝上麦申请
  182. func rejectSeatApply(uid: String, handler: @escaping (Bool) -> Void) {
  183. guestStore.rejectApplication(userID: uid) { result in
  184. switch result {
  185. case .success:
  186. handler(true)
  187. case .failure(let err):
  188. showToast(err.localizedDescription)
  189. handler(false)
  190. }
  191. }
  192. }
  193. // 邀请上麦
  194. func inviteUserToSeat(uid: String, handler: @escaping (Bool) -> Void) {
  195. guestStore.inviteToSeat(userID: uid, timeout: 0, extraInfo: nil) { result in
  196. switch result {
  197. case .success:
  198. handler(true)
  199. case .failure(let err):
  200. showToast(err.localizedDescription)
  201. handler(false)
  202. }
  203. }
  204. }
  205. // 关闭麦位
  206. func lockSeat(num: Int, handler: @escaping (Bool) -> Void) {
  207. seatStore.lockSeat(seatIndex: num) { result in
  208. switch result {
  209. case .success:
  210. handler(true)
  211. case .failure(let err):
  212. showToast(err.localizedDescription)
  213. handler(false)
  214. }
  215. }
  216. }
  217. // 解锁麦位
  218. func unlockSeat(num: Int, handler: @escaping (Bool) -> Void) {
  219. seatStore.unlockSeat(seatIndex: num) { result in
  220. switch result {
  221. case .success:
  222. handler(true)
  223. case .failure(let err):
  224. showToast(err.localizedDescription)
  225. handler(false)
  226. }
  227. }
  228. }
  229. }
  230. // MARK: 麦克风管理 - 普通用户
  231. extension LNRoomViewModel {
  232. // 关闭自己麦克风
  233. func muteMySeat() {
  234. seatStore.muteMicrophone()
  235. }
  236. // 打开自己麦克风
  237. func unmuteMySeat(handler: @escaping (Bool) -> Void) {
  238. seatStore.unmuteMicrophone { result in
  239. switch result {
  240. case .success:
  241. handler(true)
  242. case .failure(let err):
  243. showToast(err.localizedDescription)
  244. handler(false)
  245. }
  246. }
  247. }
  248. }
  249. // MARK: 麦克风管理 - 管理员
  250. extension LNRoomViewModel {
  251. // 禁止某人的麦克风
  252. func muteSeat(uid: String, handler: @escaping (Bool) -> Void) {
  253. seatStore.closeRemoteMicrophone(userID: uid) { result in
  254. switch result {
  255. case .success:
  256. handler(true)
  257. case .failure(let err):
  258. showToast(err.localizedDescription)
  259. handler(false)
  260. }
  261. }
  262. }
  263. // 解锁某人麦克风
  264. func unmuteSeat(uid: String, handler: @escaping (Bool) -> Void) {
  265. seatStore.openRemoteMicrophone(userID: uid, policy: .unlockOnly) { result in
  266. switch result {
  267. case .success:
  268. handler(true)
  269. case .failure(let err):
  270. showToast(err.localizedDescription)
  271. handler(false)
  272. }
  273. }
  274. }
  275. }
  276. // MARK: 公屏
  277. private extension BarrageType {
  278. var toMessageType: LNRoomMessageItemType {
  279. switch self {
  280. case .text:
  281. .chat
  282. case .custom:
  283. .system
  284. default:
  285. .unknown
  286. }
  287. }
  288. }
  289. extension LNRoomViewModel {
  290. var curMessage: [LNRoomMessageItem] {
  291. messageStore.state.value.messageList.suffix(maxMessageCount).map {
  292. LNRoomMessageItem(type: $0.messageType.toMessageType, sender: $0.sender.userID, text: $0.textContent)
  293. }
  294. }
  295. private func setupMessageObservers() {
  296. messageStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  297. guard let self else { return }
  298. guard messageRefreshTask == nil else {
  299. return
  300. }
  301. messageRefreshTask = LNDelayTask.perform(delay: self.messageRefreshInterval, task: { [weak self] in
  302. guard let self else { return }
  303. messageRefreshTask = nil
  304. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomMessageChanged() }
  305. })
  306. }.store(in: &cancellables)
  307. }
  308. func sendMessage(text: String, handler: @escaping (Bool) -> Void) {
  309. messageStore.sendTextMessage(text: text, extensionInfo: nil) { result in
  310. switch result {
  311. case .success:
  312. handler(true)
  313. case .failure(let err):
  314. showToast(err.localizedDescription)
  315. handler(false)
  316. }
  317. }
  318. }
  319. }
  320. // MARK: 房间信息
  321. extension LNRoomViewModel {
  322. private func setupRoomInfoObserver() {
  323. LNRoomManager.shared.liveListStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  324. guard let self else { return }
  325. if roomInfo.update(state.currentLive) {
  326. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomInfoChanged() }
  327. }
  328. }.store(in: &cancellables)
  329. }
  330. func updateRoomInfo(name: String, cover: String, handler: @escaping (Bool) -> Void) {
  331. var info = LiveInfo()
  332. info.liveID = roomInfo.liveID
  333. info.liveName = name
  334. info.coverURL = cover
  335. LNRoomManager.shared.liveListStore.updateLiveInfo(info, modifyFlag: [.liveName, .coverURL]) { result in
  336. switch result {
  337. case .success:
  338. handler(true)
  339. case .failure(let err):
  340. showToast(err.localizedDescription)
  341. handler(false)
  342. }
  343. }
  344. }
  345. }