242 lines
8.7 KiB
Swift
242 lines
8.7 KiB
Swift
//
|
||
// 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<String, ChatSectionItem>
|
||
|
||
final class GroupChatViewModel {
|
||
|
||
struct Input {
|
||
let sendMessage: AnyObserver<String>
|
||
}
|
||
|
||
struct Output {
|
||
let messages: Observable<[ChatSectionItem]>
|
||
}
|
||
|
||
let input: Input
|
||
let output: Output
|
||
|
||
private let messagesSubject = BehaviorRelay<[ChatSectionItem]>(value: [])
|
||
private let sendMessageSubject = PublishSubject<String>()
|
||
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
|
||
}
|
||
}
|