// // GroupChatViewModel.swift // QuickLocation // // Created by 八条 on 2026/6/5. // import UIKit import RxSwift import RxCocoa import OpenIMSDK import Differentiator enum ChatSectionItem { case send(ChatMessage) case received(ChatMessage) case emojiSend(ChatMessage) case emojiReceived(ChatMessage) case voiceSend(ChatMessage) case voiceReceived(ChatMessage) case notification(String) } typealias ChatSectionModel = SectionModel final class GroupChatViewModel { struct Input { let sendMessage: AnyObserver } struct Output { let messages: Observable<[ChatSectionItem]> } let input: Input let output: Output private let messagesSubject = BehaviorRelay<[ChatSectionItem]>(value: []) private let sendMessageSubject = PublishSubject() private var lastTimeGap: TimeInterval = 0 /// 两条消息间隔超过此值(秒)显示时间戳 private let timeGapThreshold: TimeInterval = 300 // 5 minutes var groupModel: GroupInfoModel? var memberList: [GroupMemberModel] = [] var groupId: String = "" // MARK: - Init init() { input = Input(sendMessage: sendMessageSubject.asObserver()) output = Output(messages: messagesSubject.asObservable()) sendMessageSubject .subscribe(onNext: { [weak self] text in self?.onSendMessage(text) }) .disposed(by: disposeBag) } private let disposeBag = DisposeBag() // MARK: - Avatar func getUserAvatar(id: String) -> UIImage { if let member = memberList.first(where: { $0.user_id == id }) { return member.userIcon } return UIImage(named: "GroupIcon1") ?? UIImage() } func getUserNickName(id: String) -> String { memberList.first { id == $0.user_id }?.nick_name ?? "" } // MARK: - Send private func onSendMessage(_ text: String) { let msg = OIMMessageInfo.createTextMessage(text) OIMManager.manager.sendMessage(msg, recvID: "", groupID: groupId, offlinePushInfo: nil, onSuccess: { [weak self] _ in self?.appendMessage(msg) }, onProgress: nil as OIMNumberCallback?, onFailure: { code, errMsg in print("send failed: \(code) \(errMsg ?? "")") }) } // MARK: - Load func loadMessages() { guard !groupId.isEmpty else { return } let param = OIMGetAdvancedHistoryMessageListParam() param.conversationID = "sg_\(groupId)" param.count = 30 OIMManager.manager.getAdvancedHistoryMessageList( param, onSuccess: { [weak self] result in guard let self = self, let list = result?.messageList else { return } var items: [ChatSectionItem] = [] for msg in list { if let item = self.toSectionItem(msg) { items.append(item) } } // messages come newest-first from API, sort by time ascending items.sort { let t1 = self.timestampFrom(item: $0) let t2 = self.timestampFrom(item: $1) return t1 < t2 } // set showTime flag for i in items.indices { let ts = self.timestampFrom(item: items[i]) if i == 0 || ts - self.timestampFrom(item: items[i-1]) >= self.timeGapThreshold { items[i] = self.setShowTime(items[i], true) } } if let last = items.last { self.lastTimeGap = self.timestampFrom(item: last) } DispatchQueue.main.async { self.messagesSubject.accept(items) } }, onFailure: { code, msg in print("loadMessages failed: \(code) \(msg ?? "")") }) } // MARK: - Receive func onReceiveMessage(_ msg: OIMMessageInfo) { guard let item = toSectionItem(msg) else { return } let ts = timestampFrom(item: item) var items = messagesSubject.value let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold lastTimeGap = ts items.append(showTime ? setShowTime(item, true) : item) DispatchQueue.main.async { self.messagesSubject.accept(items) } } // MARK: - Append sent message private func appendMessage(_ msg: OIMMessageInfo) { guard let item = toSectionItem(msg) else { return } let ts = timestampFrom(item: item) var items = messagesSubject.value let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold lastTimeGap = ts items.append(showTime ? setShowTime(item, true) : item) DispatchQueue.main.async { self.messagesSubject.accept(items) } } private func timestampFrom(item: ChatSectionItem) -> TimeInterval { switch item { case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m), let .voiceSend(m), let .voiceReceived(m): return m.timestamp case .notification: return 0 } } private func setShowTime(_ item: ChatSectionItem, _ show: Bool) -> ChatSectionItem { switch item { case var .send(m): m.showTime = show; return .send(m) case var .received(m): m.showTime = show; return .received(m) case var .emojiSend(m): m.showTime = show; return .emojiSend(m) case var .emojiReceived(m):m.showTime = show; return .emojiReceived(m) case var .voiceSend(m): m.showTime = show; return .voiceSend(m) case var .voiceReceived(m):m.showTime = show; return .voiceReceived(m) case .notification: return item } } private func convert(_ msg: OIMMessageInfo) -> ChatMessage { let isSelf = msg.isSelf() let ts = TimeInterval(msg.sendTime) / 1000.0 let content: String if let sound = msg.soundElem { content = "\(sound.duration)" } else { content = msg.textElem?.content ?? "" } return ChatMessage( id: msg.clientMsgID ?? UUID().uuidString, isSelf: isSelf, avatar: getUserAvatar(id: msg.sendID ?? ""), senderName: msg.senderNickname ?? "", content: content, timestamp: ts, showTime: false ) } // emoji pattern: js_emoji:数字 private let emojiPattern = try? NSRegularExpression(pattern: "^js_emoji:(\\d+)$", options: []) private func toSectionItem(_ msg: OIMMessageInfo) -> ChatSectionItem? { // 通知消息 if msg.contentType.rawValue == 1501 || msg.contentType.rawValue == 1510, let noti = msg.notificationElem { let text = parseNotification(noti) if !text.isEmpty { return .notification(text) } } // 语音消息 if msg.contentType.rawValue == 103 || msg.contentType.rawValue == 104 { let chatMsg = convert(msg) return chatMsg.isSelf ? .voiceSend(chatMsg) : .voiceReceived(chatMsg) } // 普通文本消息 let chatMsg = convert(msg) if chatMsg.content.isEmpty { return nil } // 检测是否为纯 emoji 消息 if let pattern = emojiPattern, pattern.firstMatch(in: chatMsg.content, options: [], range: NSRange(location: 0, length: chatMsg.content.utf16.count)) != nil { return chatMsg.isSelf ? .emojiSend(chatMsg) : .emojiReceived(chatMsg) } return chatMsg.isSelf ? .send(chatMsg) : .received(chatMsg) } private func parseNotification(_ elem: OIMNotificationElem) -> String { guard let data = elem.detail?.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let group = json["group"] as? [String: Any], let _ = json["opUser"] as? [String: Any] else { return "" } // 判断是否是当前用户:如果是自己创建的群聊显示"你创建了群聊",否则显示"XXX创建了群聊" let ownerID = group["ownerUserID"] as? String ?? "" let ownerNickName = getUserNickName(id: ownerID) let displayStr = ownerID == AppContextManager.shared.userId ? "圈子已经创建" : "\(ownerNickName) 创建了圈子" return displayStr } }