188 lines
6.1 KiB
Swift
188 lines
6.1 KiB
Swift
//
|
||
// 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 ?? "-")』"
|
||
}
|
||
}
|