私聊的vm

This commit is contained in:
fengshuo
2024-02-27 23:34:55 +08:00
parent a764188646
commit 166f2bde57
57 changed files with 4289 additions and 2828 deletions

View File

@@ -24,7 +24,7 @@ private extension BaseTabBarViewController {
let home: (BaseViewController, String, UIImage?, UIImage?) = (PlanetStarVC(), "", UIImage(named: "tabbar_icon_planet"), UIImage(named: "tabbar_icon_planet_sel"))
let message: (BaseViewController, String, UIImage?, UIImage?) = (ChatVC(), "",UIImage(named: "tabbar_icon_chat"), UIImage(named: "tabbar_icon_chat_sel"))
let message: (BaseViewController, String, UIImage?, UIImage?) = (ChatListVC(), "",UIImage(named: "tabbar_icon_chat"), UIImage(named: "tabbar_icon_chat_sel"))
let person: (BaseViewController, String, UIImage?, UIImage?) = (UserInfoVC(), "",UIImage(named: "tabbar_icon_user"), UIImage(named: "tabbar_icon_user_sel"))

View File

@@ -0,0 +1,110 @@
//
// Date+.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import Foundation
extension Date {
///
/// - Returns:
static func getNowTimeStamp() -> Int {
let nowDate = Date.init()
//10
let interval = Int(nowDate.timeIntervalSince1970)
return interval
}
///
/// - Returns:
static func getNowTimeString(dateFormat: String) -> String {
let dateformatter = DateFormatter()
dateformatter.dateFormat = dateFormat
let nowDate = Date.init()
return dateformatter.string(from: nowDate)
}
///
/// - Parameters:
/// - timeStamp:
/// - dateFormat: yyyy-MM-dd HH:mm:ss
/// - Returns:
static func getTimeString(timeStamp: Int, dateFormat: String) -> String {
let date = Date(timeIntervalSince1970: TimeInterval.init(timeStamp))
let dateformatter = DateFormatter()
dateformatter.dateFormat = dateFormat
return dateformatter.string(from: date)
}
/// Date
/// - Parameters:
/// - timeString:
/// - dateFormat: yyyy-MM-dd HH:mm:ss
/// - Returns: Date
static func getDate(timeString: String, dateFormat: String) -> Date {
let dateformatter = DateFormatter()
dateformatter.dateFormat = dateFormat
let date = dateformatter.date(from: timeString) ?? Date()
return date
}
///
/// - Parameters:
/// - timeString:
/// - dateFormat: yyyy-MM-dd HH:mm:ss
/// - Returns:
static func getTimeStamp(timeString: String, dateFormat: String) -> Int {
let dateformatter = DateFormatter()
dateformatter.dateFormat = dateFormat
let date = self.getDate(timeString: timeString, dateFormat: dateFormat)
return Int(date.timeIntervalSince1970)
}
/// date
/// - Parameters:
/// - timeStamp:
/// - Returns: date
static func getDateWith(timeStamp: Int) -> Date {
let date = Date(timeIntervalSince1970: TimeInterval.init(timeStamp))
return date
}
///
/// - Returns:
func getTime() -> (String, String, String, String, String, String) {
let dateformatter = DateFormatter()
dateformatter.dateFormat = "yyyy"
let y = dateformatter.string(from: self)
dateformatter.dateFormat = "MM"
let mo = dateformatter.string(from: self)
dateformatter.dateFormat = "dd"
let d = dateformatter.string(from: self)
dateformatter.dateFormat = "HH"
let h = dateformatter.string(from: self)
dateformatter.dateFormat = "mm"
let m = dateformatter.string(from: self)
dateformatter.dateFormat = "ss"
let s = dateformatter.string(from: self)
return (y, mo, d, h, m, s)
}
///
/// - Parameter dateFormat: yyyy-MM-dd HH:mm:ss
/// - Returns:
func getStringTime(dateFormat: String) -> String {
let dateformatter = DateFormatter()
dateformatter.dateFormat = dateFormat
return dateformatter.string(from: self)
}
}

View File

@@ -0,0 +1,18 @@
//
// ChatListVC.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import UIKit
class ChatListVC: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}

View File

@@ -3,27 +3,64 @@
// yinmeng-ios
//
// Created by MaiMang on 2024/2/25.
//
//
import UIKit
import NIMSDK
class ChatVC: BaseViewController {
public init(session: NIMSession) {
vm = ChatViewModel(session: session)
super.init(nibName: nil, bundle: nil)
// vm.delegate = self
// NIMSDK.shared().mediaManager.add(self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var vm:ChatViewModel
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
private lazy var chatTableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = UIView()
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
}
return tableView
}()
}
extension ChatVC: UITableViewDelegate, UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return vm.messageObjects.count
}
public func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
public func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
let m = vm.messageObjects[safe:indexPath.row]
return CGFloat(m?.height ?? 0)
}
}

View File

@@ -0,0 +1,105 @@
//
// ChatBaseObject.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import Foundation
import NIMSDK
public enum SessionType: Int {
case text = 1
case image
case time
}
public protocol ChatSessionProtocol: NSObjectProtocol {
var msg:NIMMessage? {get set}
// cell
var contentSize: CGSize { get set }
///
var height: Float { get set }
///id
var userID: String? { get set }
///
var name: String? { get set }
///
var avatar: String? { get set }
///
var type:SessionType{get set}
init(msg: NIMMessage?)
}
open class ChatBaseObject:NSObject, ChatSessionProtocol {
public var msg: NIMMessage?
public var contentSize: CGSize
public var height: Float
public var userID: String?
public var name: String?
public var avatar: String?
public var type: SessionType = .text
public required init(msg: NIMMessage?) {
self.msg = msg
if let uid = msg?.from {
self.userID = uid
let user = NIMSDK.shared().userManager.userInfo(uid)
self.avatar = user?.userInfo?.avatarUrl
self.name = user?.userInfo?.nickName
}
contentSize = CGSize(width: 32.0, height: ChatUIConfig.layout.bubbleMinHeight)
height = Float(ChatUIConfig.layout.bubbleMinHeight + ChatUIConfig.layout.margin)
}
}
class ChatTextObject: ChatBaseObject {
public var attribute: NSMutableAttributedString?
required init(msg: NIMMessage?) {
super.init(msg: msg)
type = .text
let style = NSMutableParagraphStyle()
style.lineSpacing = 6
let attributeStr = NSMutableAttributedString(string: msg?.text ?? "", attributes: [NSAttributedString.Key.font: ChatUIConfig.ui.messageFont, NSAttributedString.Key.paragraphStyle: style])
attribute = attributeStr
let textSize = ChatAttributeTool.boundingRect(attribute: attributeStr, font: ChatUIConfig.ui.messageFont, maxSize: CGSize(width: ChatUIConfig.layout.contentMaxWidth, height: CGFloat.greatestFiniteMagnitude))
var h = ChatUIConfig.layout.bubbleMinHeight
h = textSize.height + 1 + ChatUIConfig.layout.textInsets.top + ChatUIConfig.layout.textInsets.bottom
if h < 36 {
h = 36
}
contentSize = CGSize(width: textSize.width + ChatUIConfig.layout.textInsets.left + ChatUIConfig.layout.textInsets.right + 1, height: h)
height = Float(contentSize.height + ChatUIConfig.layout.cellContentInsets.bottom + ChatUIConfig.layout.cellContentInsets.top)
}
}
class ChatTimeObject: ChatBaseObject {
var text:String = ""
public var attribute: NSMutableAttributedString?
required init(msg: NIMMessage?) {
super.init(msg: msg)
type = .time
let style = NSMutableParagraphStyle()
style.lineSpacing = 6
let attributeStr = NSMutableAttributedString(string: msg?.text ?? "", attributes: [NSAttributedString.Key.font: ChatUIConfig.ui.messageFont, NSAttributedString.Key.paragraphStyle: style])
attribute = attributeStr
let textSize = ChatAttributeTool.boundingRect(attribute: attributeStr, font: ChatUIConfig.ui.messageFont, maxSize: CGSize(width: ChatUIConfig.layout.contentMaxWidth, height: CGFloat.greatestFiniteMagnitude))
var h = ChatUIConfig.layout.bubbleMinHeight
h = textSize.height + 1 + ChatUIConfig.layout.textInsets.top + ChatUIConfig.layout.textInsets.bottom
if h < 36 {
h = 36
}
contentSize = CGSize(width: textSize.width + ChatUIConfig.layout.textInsets.left + ChatUIConfig.layout.textInsets.right + 1, height: h)
height = Float(contentSize.height + ChatUIConfig.layout.cellContentInsets.bottom + ChatUIConfig.layout.cellContentInsets.top)
}
}

View File

@@ -0,0 +1,62 @@
//
// ChatUIConfig.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import Foundation
public class ChatUIConfig {
/// UI
public static let ui = UI()
///
public static let layout = Layout()
public class UI {
public enum AvatarType {
case cycle //
case rectangle(CGFloat) //
}
///
public var avatarType: AvatarType = .cycle
///
public var avatarPlaceholderImage = UIImage()
/// /
public var tipsColor = UIColor.lightGray
/// /
public var tipsFont = UIFont.systemFont(ofSize: 12)
/// ()
public var messageFont = UIFont.systemFont(ofSize: 14)
///
public var messageNameFont = UIFont.systemFont(ofSize: 13)
///
public var messageNameColor = UIColor(red: 5/255.0, green: 19/255.0, blue: 47/255.0, alpha: 1)
/// ()
public var leftMessageColor = UIColor.black
/// ()
public var rightMessageColor = UIColor.black
}
public class Layout {
///
public var avatarSize: CGFloat = 34.0
/// cell
public var cellContentInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
///
public var textInsets = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)
///
public var contentMaxWidth: CGFloat = (UIScreen.main.bounds.size.width - 170)
///
public var pictureMaxSize = CGSize(width: 150, height: 150)
///
public var bubbleMinHeight: CGFloat = 40.0
///
public var margin: CGFloat = 8.0
}
}

View File

@@ -0,0 +1,28 @@
//
// ChatAttributeTool.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import Foundation
public class ChatAttributeTool {
// size
public class func boundingRect(attribute: NSAttributedString, font: UIFont, maxSize: CGSize) -> CGSize {
if attribute.length == 0 {
return CGSize.zero
}
var sizeRec = attribute.boundingRect(
with: maxSize,
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
).size
if attribute.length > 0, sizeRec.width == 0, sizeRec.height == 0 {
sizeRec = maxSize
}
return CGSize(width: ceil(sizeRec.width), height: ceil(sizeRec.height))
}
}

View File

@@ -0,0 +1,666 @@
//
// ChatViewModel.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import Foundation
import NIMSDK
public typealias ChatProviCompletion = ((Error?, [NIMMessage]?) -> ())
public enum LoadMessageDirection: Int {
case old = 1
case new
}
public protocol ChatViewModelDelegate: NSObjectProtocol {
func didAppend(_ message: NIMMessage)
func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath])
func send(_ message: NIMMessage, didCompleteWithError error: Error?)
func send(_ message: NIMMessage, progress: Float)
func onRecvMessages(_ messages: [NIMMessage])
func willSend(_ message: NIMMessage)
}
public class ChatViewModel: NSObject,
NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate {
private var userInfo = [String: NIMUser]()
public let messagPageNum: UInt = 100
public var session: NIMSession
///
public var messageObjects: [ChatSessionProtocol] = .init()
//
private var oldMsg: NIMMessage?
//
private var newMsg: NIMMessage?
//
public var credibleTimestamp: TimeInterval = 0
public var anchor: NIMMessage?
internal var isHistoryAnchorChat = false
public weak var delegate: ChatViewModelDelegate?
private func addMessageListener() {
NIMSDK.shared().chatManager.add(self)
NIMSDK.shared().conversationManager.add(self)
NIMSDK.shared().systemNotificationManager.add(self)
}
init(session: NIMSession) {
self.session = session
super.init()
addMessageListener()
}
///
/// - Parameters:
/// - message:
/// - completion:
public func sendMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) {
NIMSDK.shared().chatManager.send(message, to: session, completion: completion)
}
////
public func sendAudioMessage(filePath: String, _ completion: @escaping (Error?) -> Void) {
let audioObject = NIMAudioObject(sourcePath: filePath)
let audioMessage = NIMMessage()
audioMessage.messageObject = audioObject
audioMessage.apnsContent = "发来了一段语音"
let setting = NIMMessageSetting()
setting.teamReceiptEnabled = false
audioMessage.setting = setting
sendMessage(message: audioMessage, completion)
}
///
public func sendImageMessage(image: UIImage, _ completion: @escaping (Error?) -> Void) {
let imageMessage = NIMMessage()
let imageOpt = NIMImageOption()
imageOpt.compressQuality = 0.8
let imageObject = NIMImageObject(image: image)
imageObject.option = imageOpt
imageMessage.messageObject = imageObject
imageMessage.apnsContent = "发送了一张图片"
sendMessage(message: imageMessage, completion)
}
public func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) {
if text.count <= 0 {
return
}
let textMessage = NIMMessage()
textMessage.text = text
sendMessage(message: textMessage, completion)
}
//
public func reloadRemoteHistoryMessage(direction: LoadMessageDirection, updateCredible: Bool,
option: NIMHistoryMessageSearchOption,
_ completion: @escaping (Error?, NSInteger,
[ChatSessionProtocol]?) -> Void) {
weak var weakSelf = self
NIMSDK.shared().conversationManager.fetchMessageHistory(session, option: option) { error, messages in
if error == nil {
if let messageArray = messages, messageArray.count > 0 {
if direction == .old {
weakSelf?.oldMsg = messageArray.last
} else {
weakSelf?.newMsg = messageArray.first
}
for msg in messageArray {
if let model = weakSelf?.modelTransformMessage(message: msg) {
weakSelf?.addTimeMessage(msg)
weakSelf?.messageObjects.insert(model, at: 0)
}
}
if let updateMessage = messageArray.first, updateCredible {
//
weakSelf?.credibleTimestamp = updateMessage.timestamp
}
completion(error, messageArray.count, weakSelf?.messageObjects)
} else {
completion(error, 0, weakSelf?.messageObjects)
}
} else {
completion(error, 0, nil)
}
}
}
//
public func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?)
-> Void) {
let isCredible = isMessageCredible(message: oldMsg ?? NIMMessage())
if isCredible { //
getMoreMessageHistory(completion)
} else {
let option = NIMHistoryMessageSearchOption()
option.startTime = 0
option.endTime = oldMsg?.timestamp ?? 0
option.limit = messagPageNum
option.sync = true
//
reloadRemoteHistoryMessage(
direction: .old,
updateCredible: false,
option: option,
completion
)
}
}
public func queryRoamMsgHasMoreTime_v2(_ completion: @escaping (Error?, NSInteger, NSInteger,
[ChatSessionProtocol]?, Int) -> Void) {
weak var weakSelf = self
NIMSDK.shared().conversationManager.incompleteSessionInfo(by: session) { error, sessionInfos in
if error == nil {
let sessionInfo = sessionInfos?.first
//
weakSelf?.credibleTimestamp = sessionInfo?.timestamp ?? 0
weakSelf?.getMessageHistory(self.newMsg) { error, value, models in
completion(error, value, 0, models, 0)
}
}
}
}
//
public func getMessageHistory(_ message: NIMMessage?,
_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?)
-> Void) {
NIMSDK.shared().conversationManager.messages(in: session, message: message, limit: Int(messagPageNum), completion: result(completion))
func result(_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> ()) -> ChatProviCompletion {
return { [weak self] error, messages in
if let messageArray = messages, messageArray.count > 0 {
self?.oldMsg = messageArray.first
for msg in messageArray {
if let model = self?.modelTransformMessage(message: msg) {
self?.addTimeMessage(msg)
self?.messageObjects.append(model)
}
}
completion(error, messageArray.count, self?.messageObjects)
self?.markRead(messages: messageArray) { error in
}
} else {
completion(error, 0, self?.messageObjects)
}
}
}
}
//
public func getMoreMessageHistory(_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?)
-> Void) {
let messageParam = oldMsg ?? newMsg
weak var weakSelf = self
NIMSDK.shared().conversationManager.messages(in: session, message: messageParam, limit: Int(messagPageNum)) { error, messages in
if let messageArray = messages, messageArray.count > 0 {
weakSelf?.oldMsg = messageArray.first
// 使
let isCredible = weakSelf?
.isMessageCredible(message: messageArray.first ?? NIMMessage())
if let isTrust = isCredible, isTrust {
for msg in messageArray.reversed() {
if let model = weakSelf?.modelTransformMessage(message: msg) {
weakSelf?.addTimeMessage(msg)
weakSelf?.messageObjects.insert(model, at: 0)
}
}
completion(error, messageArray.count, weakSelf?.messageObjects)
} else {
let option = NIMHistoryMessageSearchOption()
option.startTime = 0
option.endTime = weakSelf?.oldMsg?.timestamp ?? 0
option.limit = weakSelf?.messagPageNum ?? 100
option.sync = true
weakSelf?.reloadRemoteHistoryMessage(
direction: .old,
updateCredible: true,
option: option,
completion
)
}
weakSelf?.markRead(messages: messageArray) { error in
}
} else {
if let messageArray = messages, messageArray.isEmpty,
weakSelf?.credibleTimestamp ?? 0 > 0 {
//
let option = NIMHistoryMessageSearchOption()
option.startTime = 0
option.endTime = weakSelf?.oldMsg?.timestamp ?? 0
option.limit = weakSelf?.messagPageNum ?? 100
weakSelf?.reloadRemoteHistoryMessage(
direction: .old,
updateCredible: true,
option: option,
completion
)
} else {
completion(error, 0, weakSelf?.messageObjects)
}
}
}
}
//
public func searchMessageHistory(direction: LoadMessageDirection, startTime: TimeInterval,
endTime: TimeInterval,
_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?)
-> Void) {
let option = NIMMessageSearchOption()
option.startTime = startTime
option.endTime = endTime
option.order = .asc
option.limit = messagPageNum
NIMSDK.shared().conversationManager.searchMessages(session, option: option) { [weak self] error, messages in
if error == nil {
if let messageArray = messages, messageArray.count > 0 {
var newMessages = [NIMMessage]()
for msg in messageArray {
newMessages.append(msg)
if let model = self?.modelTransformMessage(message: msg) {
self?.addTimeMessage(msg)
self?.messageObjects.append(model)
}
}
if direction == .old {
self?.oldMsg = newMessages.first
} else {
self?.newMsg = newMessages.last
}
completion(error, newMessages.count, self?.messageObjects)
} else {
completion(error, 0, self?.messageObjects)
}
} else {
completion(error, 0, nil)
}
}
}
//
public func isMessageCredible(message: NIMMessage) -> Bool {
return credibleTimestamp <= 0 || message.timestamp >= credibleTimestamp
}
public func markRead(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) {
NIMSDK.shared().conversationManager.markAllMessagesRead(in: session) { error in
}
}
@discardableResult
public func resendMessage(message: NIMMessage) -> Error? {
var e: Error? = nil
do {
try NIMSDK.shared().chatManager.resend(message)
} catch {
e = error
}
return e
}
public func getUserInfo(userId: String) -> NIMUser? {
return NIMSDK.shared().userManager.userInfo(userId)
}
public func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? {
// return repo.getTeamMemberList(userId: userId, teamId: teamId)
return nil
}
public func deleteMessage(message: NIMMessage) {
NIMSDK.shared().conversationManager.delete(message)
deleteMessageUpdateUI(message)
}
// MARK: collection
func addColletion(_ message: NIMMessage,
completion: @escaping (NSError?, NIMCollectInfo?) -> Void) {
let param = NIMAddCollectParams()
var string: String?
if message.messageType == .text {
string = message.text
param.type = 1024
} else {
switch message.messageType {
case .audio:
if let obj = message.messageObject as? NIMAudioObject {
string = obj.url
}
param.type = message.messageType.rawValue
case .image:
if let obj = message.messageObject as? NIMImageObject {
string = obj.url
}
param.type = message.messageType.rawValue
case .video:
if let obj = message.messageObject as? NIMVideoObject {
string = obj.url
}
param.type = message.messageType.rawValue
default:
param.type = 0
}
param.data = string ?? ""
}
param.uniqueId = message.serverID
// repo.collectMessage(param, completion)
}
// MARK: revoke
// history message insert message at first of messages, send message add last of messages
private func addTimeMessage(_ message: NIMMessage) {
let lastTs = messageObjects.last?.msg?.timestamp ?? 0.0
let curTs = message.timestamp
let dur = curTs - lastTs
print("curTs:\(curTs) lastTs:\(lastTs)")
if (dur / 60) > 5 {
messageObjects.append(timeModel(message))
}
}
private func ddTimeForHistoryMessage(_ message: NIMMessage) {
let firstTs = messageObjects.first?.msg?.timestamp ?? 0.0
let curTs = message.timestamp
let dur = firstTs - curTs
print("HistorycurTs:\(curTs) firstTs:\(firstTs)")
if (dur / 60) > 5 {
messageObjects.insert(timeModel(message), at: 0)
}
}
private func timeModel(_ message: NIMMessage) -> ChatSessionProtocol {
let timeMsg = NIMMessage()
timeMsg.timestamp = message.timestamp
let model = ChatTimeObject(msg: timeMsg)
model.type = .time
model.text = "2024-2-28"
return model
}
private func modelTransformMessage(message: NIMMessage) -> ChatSessionProtocol? {
var model: ChatSessionProtocol
switch message.messageType {
case .text:
model = ChatTextObject(msg: message)
default:
return nil
// text
// message.text = ""
// model = MAIMessageContentModel(message: message)
}
if let uid = message.from {
model.userID = uid
let user = getUserInfo(userId: uid)
var fullName = uid
if let nickName = user?.userInfo?.nickName {
fullName = nickName
}
model.avatar = user?.userInfo?.avatarUrl
if session.sessionType == .team {
// team
let teamMember = getTeamMember(userId: uid, teamId: session.sessionId)
if let teamNickname = teamMember?.nickname {
fullName = teamNickname
}
}
if let alias = user?.alias {
fullName = alias
}
model.name = fullName
}
return model
}
func deleteMessageUpdateUI(_ message: NIMMessage) {
var index = -1
for (i, model) in messageObjects.enumerated() {
if model.msg?.serverID == message.serverID {
index = i
break
}
}
var indexs = [IndexPath]()
if index >= 0 {
// remove time tip
let last = index - 1
// if last >= 0, let timeModel = messages[last] as? MAIMessageTipsModel,
// timeModel.type == .time {
// messageObjects.removeSubrange(last ... index)
// indexs.append(IndexPath(row: last, section: 0))
// indexs.append(IndexPath(row: index, section: 0))
// } else {
messageObjects.remove(at: index)
indexs.append(IndexPath(row: index, section: 0))
// }
}
delegate?.onDeleteMessage(message, atIndexs: indexs)
}
private func getUserInfo(_ userId: String, _ completion: @escaping (NIMUser?, NSError?) -> Void) {
if let user = userInfo[userId] {
completion(user, nil)
}
if let user = getUserInfo(userId: userId) {
userInfo[userId] = user
completion(user, nil)
}
}
// p2p >ID team: ID
private func getShowName(userId: String, teamId: String?) -> String {
let user = getUserInfo(userId: userId)
var fullName = userId
if let nickName = user?.userInfo?.nickName {
fullName = nickName
}
// model.avatar = user?.userInfo?.thumbAvatarUrl
if let tID = teamId, session.sessionType == .team {
// team
let teamMember = getTeamMember(userId: userId, teamId: tID)
if let teamNickname = teamMember?.nickname {
fullName = teamNickname
}
}
if let alias = user?.alias {
fullName = alias
}
return fullName
}
public func fetchMessageAttachment(_ message: NIMMessage,
_ completion: @escaping (Error?) -> Void) {
do {
try NIMSDK.shared().chatManager.fetchMessageAttachment(message)
} catch let error {
completion(error)
}
}
public func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?,
_ completion: NIMDownloadCompleteBlock?) {
NIMSDK.shared().resourceManager.download(urlString, filepath: filePath, progress: progress, completion: completion)
}
public func getUrls() -> [String] {
var urls = [String]()
messageObjects.forEach { model in
if model.type == .image, let message = model.msg?.messageObject as? NIMImageObject {
if let url = message.url {
urls.append(url)
} else {
if let path = message.path, FileManager.default.fileExists(atPath: path) {
urls.append(path)
}
}
}
}
return urls
}
public func sendInputTypingState() {
if session.sessionType == .P2P {
setTypingCustom(1)
}
}
public func sendInputTypingEndState() {
if session.sessionType == .P2P {
setTypingCustom(0)
}
}
func setTypingCustom(_ typing: Int) {
let message = NIMMessage()
if message.setting == nil {
message.setting = NIMMessageSetting()
}
message.setting?.apnsEnabled = false
message.setting?.shouldBeCounted = false
let noti =
NIMCustomSystemNotification(content: getJSONStringFromDictionary(["typing": typing]))
NIMSDK.shared().systemNotificationManager.sendCustomNotification(noti, to: session)
}
public func getHandSetEnable() -> Bool {
return false
// return repo.getHandsetMode()
}
public func getMessageRead() -> Bool {
return NIMSDK.shared().conversationManager.allUnreadCount() > 0
}
// MARK: NIMConversationManagerDelegate
private func getJSONStringFromDictionary(_ dictionary: [String: Any]) -> String {
if !JSONSerialization.isValidJSONObject(dictionary) {
print("not parse to json string")
return ""
}
if let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []),
let JSONString = String(data: data, encoding: .utf8) {
return JSONString
}
return ""
}
private func getDictionaryFromJSONString(_ jsonString: String) -> NSDictionary? {
if let jsonData = jsonString.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(
with: jsonData,
options: .mutableContainers
) as? NSDictionary {
return dict
}
return nil
}
deinit {
print("deinit")
}
}
extension ChatViewModel: NIMChatManagerDelegate {
// MARK: NIMChatManagerDelegate
public func send(_ message: NIMMessage, didCompleteWithError error: Error?) {
for (i, msg) in messageObjects.enumerated() {
if message.messageId == msg.msg?.messageId {
messageObjects[i].msg = message
break
}
}
delegate?.send(message, didCompleteWithError: error)
}
//
public func onRecvMessages(_ messages: [NIMMessage]) {
for msg in messages {
if msg.session?.sessionId == session.sessionId {
if let model = modelTransformMessage(message: msg) {
newMsg = msg
addTimeMessage(msg)
self.messageObjects.append(model)
}
}
}
delegate?.onRecvMessages(messages)
}
public func willSend(_ message: NIMMessage) {
if message.session?.sessionId != session.sessionId {
return
}
guard let model = modelTransformMessage(message: message) else { return }
if newMsg == nil {
newMsg = message
}
var isResend = false
for (i, msg) in messageObjects.enumerated() {
if message.messageId == msg.msg?.messageId {
messageObjects[i].msg = message
isResend = true
break
}
}
if !isResend {
addTimeMessage(message)
messageObjects.append(model)
}
delegate?.didAppend(message)
}
public func onReceive(_ notification: NIMCustomSystemNotification) {
}
public func send(_ message: NIMMessage, progress: Float) {
delegate?.send(message, progress: progress)
}
}

View File

@@ -0,0 +1,78 @@
//
// ChatBaseCell.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import UIKit
protocol ChatBaseCellProtocol: NSObjectProtocol {
func cell(_ cell: ChatBaseCell, didTapAvatarAt model: ChatBaseObject)
}
class ChatBaseCell: UITableViewCell {
weak var delegate: ChatBaseCellProtocol?
var model:ChatBaseObject? {
didSet {
guard let _ = model else {return}
layoutMessageCell()
}
}
///
open func layoutMessageCell() { }
///
open func activityStartAnimating() {
guard let sendType = model?.type, sendType.rawValue == 0 else {
return
}
guard let sendDate = model?.msg?.timestamp as? Int else {
return
}
let nowDate = Date.getNowTimeStamp()
if ((nowDate - sendDate) <= 1) {
self.activityIndicatorView.isHidden = false
self.activityIndicatorView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
self.activityIndicatorView.startAnimating()
self.activityIndicatorView.isHidden = true
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.addSubview(avatarImgView)
contentView.addSubview(bubbleView)
contentView.addSubview(activityIndicatorView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var avatarImgView: UIImageView = {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
lazy var bubbleView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
lazy var activityIndicatorView: UIActivityIndicatorView = {
let activityView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.medium)
activityView.backgroundColor = .clear
activityView.isHidden = true
return activityView
}()
}

View File

@@ -0,0 +1,117 @@
//
// ChatTextCell.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/2/27.
//
import UIKit
import SnapKit
class ChatTextCell: ChatBaseCell {
private lazy var textLb: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.isEnabled = false
label.numberOfLines = 0
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
func initSubview() {
bubbleView.addSubview(textLb)
avatarImgView.snp.remakeConstraints { (make) in
make.size.equalTo(ChatUIConfig.layout.avatarSize)
make.left.equalToSuperview().offset(10)
make.top.equalTo(contentView).offset(10)
}
bubbleView.snp.remakeConstraints { (make) in
make.top.equalTo(contentView.snp.top).offset(2)
make.bottom.equalTo(textLb.snp.bottom).offset(2)
make.left.equalTo(avatarImgView.snp.right)
make.width.equalTo(ChatUIConfig.layout.contentMaxWidth)
make.height.equalTo(textLb).offset(26)
}
textLb.snp.remakeConstraints { (make) in
make.top.equalTo(bubbleView).offset(ChatUIConfig.layout.textInsets.top);
make.left.equalTo(bubbleView).offset(ChatUIConfig.layout.textInsets.left);
make.right.equalTo(bubbleView).offset(-ChatUIConfig.layout.textInsets.right);
}
activityIndicatorView.snp.remakeConstraints { (make) in
make.centerY.equalTo(bubbleView)
make.centerX.equalToSuperview()
make.width.height.equalTo(30)
}
bubbleView.setContentHuggingPriority(.required, for: .horizontal)
}
override func layoutMessageCell() {
super.layoutMessageCell()
if let textModel = model as? ChatTextObject {
self.textLb.attributedText = textModel.attribute
setupCellLayout()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension ChatTextCell {
func setupCellLayout() {
guard let model = model else {return}
if model.msg?.isOutgoingMsg == true { //
avatarImgView.snp.remakeConstraints { (make) in
make.size.equalTo(ChatUIConfig.layout.avatarSize)
make.right.equalToSuperview().offset(-10)
make.top.equalToSuperview().offset(ChatUIConfig.layout.cellContentInsets.top)
}
bubbleView.snp.makeConstraints { make in
make.size.equalTo(model.contentSize)
make.top.equalTo(avatarImgView)
make.right.equalTo(avatarImgView.snp.left).offset(-10)
}
activityIndicatorView.snp.remakeConstraints { (make) in
make.centerY.equalTo(bubbleView)
make.right.equalTo(bubbleView.snp.left)
make.width.height.equalTo(30)
}
textLb.snp.makeConstraints { make in
make.edges.equalTo(ChatUIConfig.layout.textInsets)
}
// start
activityStartAnimating()
}else {
avatarImgView.snp.remakeConstraints { (make) in
make.size.equalTo(ChatUIConfig.layout.avatarSize)
make.left.equalToSuperview().offset(10)
make.top.equalToSuperview().offset(ChatUIConfig.layout.cellContentInsets.top)
}
bubbleView.snp.makeConstraints { make in
make.size.equalTo(model.contentSize)
make.top.equalTo(avatarImgView)
make.left.equalTo(avatarImgView.snp.right).offset(10)
}
textLb.snp.makeConstraints { make in
make.edges.equalTo(ChatUIConfig.layout.textInsets)
}
}
}
}