jsdw_ios/QuickLocation/Section/Group/GroupChat/GroupChatViewModel.swift

287 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 imageSend(ChatMessage)
case imageReceived(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), let .imageSend(m), let .imageReceived(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 var .imageSend(m): m.showTime = show; return .imageSend(m)
case var .imageReceived(m):m.showTime = show; return .imageReceived(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
let voiceUrl: String
let imageUrl: String
let imageW: CGFloat
let imageH: CGFloat
// :
let maxWH: CGFloat = 200
var msgImageW: CGFloat = 0
var msgImageH: CGFloat = 0
if let width = msg.pictureElem?.sourcePicture?.width,
let height = msg.pictureElem?.bigPicture?.height {
if width >= height {
msgImageW = maxWH
msgImageH = maxWH * CGFloat(height) / CGFloat(width)
} else {
msgImageH = maxWH
msgImageW = maxWH * CGFloat(width) / CGFloat(height)
}
}
if let sound = msg.soundElem {
content = "\(sound.duration)"
voiceUrl = sound.sourceUrl ?? ""
imageUrl = ""
imageW = 0; imageH = 0
} else if let pic = msg.pictureElem {
content = ""
voiceUrl = ""
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
imageW = msgImageW
imageH = msgImageH
} else {
content = msg.textElem?.content ?? ""
voiceUrl = ""
imageUrl = ""
imageW = 0; imageH = 0
}
return ChatMessage(
id: msg.clientMsgID ?? UUID().uuidString,
isSelf: isSelf,
avatar: getUserAvatar(id: msg.sendID ?? ""),
senderName: msg.senderNickname ?? "",
content: content,
voiceUrl: voiceUrl,
imageUrl: imageUrl,
imageWidth: imageW,
imageHeight: imageH,
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)
}
//
if msg.contentType.rawValue == 102 {
let chatMsg = convert(msg)
return chatMsg.isSelf ? .imageSend(chatMsg) : .imageReceived(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
}
}