// // SessionBaseModel.swift // Crush // // Created by Leon on 2025/8/18. // import UIKit import Foundation import NIMSDK protocol SessionCellContentDelegate { /// contentView 高度 func contentSize(model: SessionBaseModel) -> CGSize /// 气泡在cell中的边距(外) func cellInsets(model: SessionBaseModel) -> UIEdgeInsets /// contentView距离在气泡内距离四周的边距(bubble内) func contentInsets(model: SessionBaseModel) -> UIEdgeInsets /// 是否只显示contentView, 默认false。 true时,局中显示 func onlyShowContent(model: SessionBaseModel) -> Bool /// 获取当前conten View的类名 func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type } enum SessionCellType: String, Codable { case unknown case text // 普通文本消息 case aimsg // AI Msg case image // 图片 case timeStamp // 时间戳 case create // 创建关系 case keyword // 敏感词 case tips // 提示 case gift // 送礼 case phonecall // im phone call case aiLoading } class SessionBaseModel: CustomStringConvertible, CustomDebugStringConvertible{ /// 有气泡时,content 最大宽度 static let maxBubbleContentWidth = floor(UIScreen.width * 0.65) /// 无气泡时,content 最大宽度 static let maxContentWidth = UIScreen.width - 32 var cellType: SessionCellType = .unknown /// Seconds timestam; var timeStamp: TimeInterval = 0 var v2msg: V2NIMMessage? /// 根据message创建的数据缓存 var baseRemoteInfo: IMBaseRemoteInfo? /// 是否是派生出来的model, 一般是一条消息显示两个cell中的卡片cell,本地追加创建的那种 默认NO var isDeriveModel = false /// 是否显示为卡片view var shouldShowCardView = false /// 是否显示为tips消息 var shouldShowTips = false /// 是否靠左显示,根据消息发送方来确定 var shouldShowLeft = false /// 默认YES,会显示气泡 var shouldShowBubble = true /// 缓存高度 var cacheCellHeight: CGFloat = 0 /// content配置类 var config: IMContentBaseConfig! var speechModel : SpeechModel? var autoPlayAudioOnce: Bool = false var autoPlayAlreadyPlayed : Bool = false // 已经播放过了 /// 清空高度缓存 public func clearCacheData() { cacheCellHeight = 0 } public func cellHeight() -> CGFloat { if cacheCellHeight > 0 { return cacheCellHeight } // 获取content的间距 let size = config.contentSize(model: self) let cellInsets = config.cellInsets(model: self) let contentInsets = config.contentInsets(model: self) // 计算高度 let height: CGFloat = size.height + cellInsets.top + cellInsets.bottom + contentInsets.top + contentInsets.bottom cacheCellHeight = height return height } // MARK: Audio About func prepareAudio(info: IMAIUserInfo?, completion:((_ model: SpeechModel?)-> Void)?){ guard cellType == .aimsg, let text = v2msg?.text, text.count > 0, let voiceType = info?.voiceType else{ return } guard AudioPlayTool.audioChannelFreeToUse() else{ dlog("Audio Channel 不可用") return } if let path = self.speechModel?.path, path.count > 0 { completion?(self.speechModel) return } let saywords = String.removeBracketContents(from: text) guard saywords.count > 0 else{ return } let speechRate = info?.dialogueSpeechRate ?? "0" let loudnessRate = info?.dialoguePitch ?? "0" var params = [String: Any]() params.updateValue(saywords, forKey: "text") params.updateValue(voiceType, forKey: "voiceType") params.updateValue(speechRate, forKey: "speechRate") params.updateValue(loudnessRate, forKey: "pitchRate") if let aiId = IMAIViewModel.shared.aiIMInfo?.aiId{ params.updateValue(aiId, forKey: "aiId") } AICowProvider.request(.voiceTts(params: params), modelType: String.self) {[weak self] result in switch result { case .success(let model): if let audioString = model{ self?.speechModel = SpeechModel.modelWithBase64String(audioString) //self?.speechModel?.refreshPath(path: audioString) completion?(self?.speechModel) } case .failure: completion?(nil) } } } // MARK: - Static // MARK: Equatable static func == (lhs: SessionBaseModel, rhs: SessionBaseModel) -> Bool { return lhs === rhs } /// 获取不同配置类 public static func getContentConfigClass(type: SessionCellType) -> IMContentBaseConfig.Type { switch type { case .unknown: return IMContentBaseConfig.self case .text: return IMTextContentConfig.self case .aimsg: return IMAIMsgContentConfig.self case .timeStamp: return IMTimeStampContentConfig.self case .image: return IMImageContentConfig.self case .tips, .create, .keyword: return IMTipsContentConfig.self case .gift: return IMGiftContentConfig.self case .phonecall: return IMPhoneCallConfig.self case .aiLoading: return IMAIMsgLoadingConfig.self // default: // return IMContentBaseConfig.self } } // MARK: - Other Debug var description: String { var attchmentString = "" if let attchmentRaw = v2msg?.attachment?.raw{ attchmentString = attchmentRaw } var icon = (v2msg?.isSelf ?? false) ? "✉️" :"💌" return "\(icon)[\(cellType.rawValue)] 『\(v2msg?.text ?? "-")』raw:\(attchmentString)" } var debugDescription: String { return "[\(cellType.rawValue)] 『\(v2msg?.text ?? "-")』" } }