私聊页面的自定义U

This commit is contained in:
fengshuo
2024-03-01 14:22:06 +08:00
parent 678297f771
commit 41ad01515d
49 changed files with 1088 additions and 71 deletions

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_more_album@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_more_album@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_voice@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_voice@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_audio_playing_first@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_audio_playing_first@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_audio_playing_second@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_audio_playing_second@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_audio_playing_third@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_audio_playing_third@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_voice_record_cancel@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_voice_record_cancel@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_voice_record_first@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_voice_record_first@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_voice_record_second@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_voice_record_second@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chat_voice_record_third@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chat_voice_record_third@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "public_avatar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "public_avatar@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -19,8 +19,7 @@ class ChatVC: BaseViewController, HiddenNavigationBarProtocol {
public init(session: NIMSession) {
vm = ChatViewModel(session: session)
super.init(nibName: nil, bundle: nil)
// vm.delegate = self
// NIMSDK.shared().mediaManager.add(self)
vm.delegate = self
}
required init?(coder: NSCoder) {
@@ -33,9 +32,19 @@ class ChatVC: BaseViewController, HiddenNavigationBarProtocol {
.darkContent
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if NIMSDK.shared().mediaManager.isPlaying() {
NIMSDK.shared().mediaManager.stopPlay()
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeColor(hexStr: "#F8F8FB")
requestInfo()
loadChatList()
registerChatCell()
let height = ScreenHeight - (ToolBarLastH + NavHeight)
chatTableView.frame = CGRect(x: 0, y: NavHeight, width: ScreenWidth, height: height)
navView.frame = CGRect(x: 0, y: 0, width: ScreenWidth, height: NavHeight)
@@ -47,17 +56,16 @@ class ChatVC: BaseViewController, HiddenNavigationBarProtocol {
///cell
private func registerChatCell() {
chatTableView.register(cellType: ChatTextCell.self)
chatTableView.register(cellType: ChatTimeCell.self)
}
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 = ThemeColor(hexStr: "#F8F8FB")
tableView.backgroundColor = .clear
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
}
@@ -84,6 +92,12 @@ class ChatVC: BaseViewController, HiddenNavigationBarProtocol {
// MARK: - ChatKeyboardViewDelegate
extension ChatVC: ChatKeyboardViewDelegate {
func keyboard(_ keyboard: ChatKeyboardView, voiceDidFinish path: String) {
vm.sendAudioMessage(filePath: path) { error in
}
}
func keyboard(_ keyboard: ChatKeyboardView, DidFinish content: String) {
///
vm.sendTextMessage(text: content) { error in
@@ -106,6 +120,138 @@ extension ChatVC: ChatKeyboardViewDelegate {
}
}
extension ChatVC {
func requestInfo() {
let params = ["uid": vm.session.sessionId]
RequestGet(path: "user/get", parma: params) { data in
if let info = Deserialized<UserObject>.toModel(with: data) {
self.navView.name = info.nick
}
} fail: { code, msg in
}
let par:[String : Any] = ["uid": AuthManager.userUid, "isLikeUid": vm.session.sessionId]
RequestGet(path: "fans/isLike", parma: par) { data in
if let isLike = data as? Bool {
self.navView.isLike = isLike
}
} fail: { code, msg in
}
}
func loadChatList() {
weak var weakSelf = self
vm.queryRoamMsgHasMoreTime_v2 { error, historyEnd, newEnd, models, index in
if let ms = models, ms.count > 0 {
weakSelf?.chatTableView.reloadData()
if weakSelf?.vm.isHistoryAnchorChat == true {
let indexPath = IndexPath(row: index, section: 0)
weakSelf?.chatTableView.scrollToRow(at: indexPath, at: .none, animated: false)
} else {
if let tempArrayJJom = weakSelf?.vm.messageObjects, tempArrayJJom.count > 0 {
DispatchQueue.main.async {
weakSelf?.chatTableView.scrollToRow(
at: IndexPath(row: tempArrayJJom.count - 1, section: 0),
at: .bottom,
animated: false
)
}
}
}
} else if let err = error {
HUDTool.show(with: err.localizedDescription)
}
}
}
private func insertRows() {
let oldRows = chatTableView.numberOfRows(inSection: 0)
if oldRows == 0 {
chatTableView.reloadData()
return
}
if oldRows == vm.messageObjects.count {
chatTableView.reloadData()
return
}
var indexs = [IndexPath]()
for (i, _) in vm.messageObjects.enumerated() {
if i >= oldRows {
indexs.append(IndexPath(row: i, section: 0))
}
}
if !indexs.isEmpty {
chatTableView.insertRows(at: indexs, with: .none)
chatTableView.scrollToRow(
at: IndexPath(row: vm.messageObjects.count - 1, section: 0),
at: .bottom,
animated: false
)
}
}
public func tableViewDeleteIndexs(_ indexs: [IndexPath]) {
chatTableView.beginUpdates()
chatTableView.deleteRows(at: indexs, with: .none)
chatTableView.endUpdates()
}
public func tableViewReloadIndexs(_ indexs: [IndexPath]) {
chatTableView.beginUpdates()
chatTableView.reloadRows(at: indexs, with: .none)
chatTableView.endUpdates()
}
}
extension ChatVC: ChatViewModelDelegate{
public func onRecvMessages(_ messages: [NIMMessage]) {
insertRows()
vm.markRead(messages: messages) { error in
}
}
public func willSend(_ message: NIMMessage) {
}
public func didAppend(_ message: NIMMessage) {
}
public func send(_ message: NIMMessage, progress: Float) {}
public func send(_ message: NIMMessage, didCompleteWithError error: Error?) {
}
private func indexPathsWithMessags(_ messages: [NIMMessage]) -> [IndexPath] {
var indexPaths = [IndexPath]()
for messageModel in messages {
for (i, model) in vm.messageObjects.enumerated() {
if model.msg?.messageId == messageModel.messageId {
indexPaths.append(IndexPath(row: i, section: 0))
}
}
}
return indexPaths
}
public func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath]) {
if atIndexs.isEmpty {
return
}
tableViewDeleteIndexs(atIndexs)
}
}
extension ChatVC: UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return vm.messageObjects.count
@@ -117,6 +263,9 @@ extension ChatVC: UITableViewDelegate, UITableViewDataSource, UIScrollViewDelega
if model?.type == .text {
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ChatTextCell.self)
cell.model = model
} else if model?.type == .time {
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ChatTimeCell.self)
cell.model = model
}
return UITableViewCell()
}

View File

@@ -77,7 +77,7 @@ class ChatGrowingTextView: UITextView {
self.returnKeyType = .send
self.layer.cornerRadius = 4
self.layer.borderWidth = 1
self.layer.borderWidth = 0
self.layer.borderColor = ThemeColor(hexStr: "#2C363E").cgColor
self.layer.masksToBounds = true
//

View File

@@ -6,8 +6,10 @@
//
import UIKit
import NIMSDK
protocol ChatKeyboardViewDelegate: NSObjectProtocol {
///
func keyboard(_ keyboard: ChatKeyboardView, voiceDidFinish path: String)
///
func keyboard(_ keyboard: ChatKeyboardView, DidFinish content: String)
/// /
@@ -30,14 +32,14 @@ class ChatKeyboardView: UIView {
private let kLeft: CGFloat = 12.0
private let kSpace: CGFloat = 12.0
private let kViewWH: CGFloat = 25.0
private let kViewWH: CGFloat = 26.0
private let kLineHeight: CGFloat = 0.75
// MARK: - var lazy
weak var delegate: ChatKeyboardViewDelegate?
fileprivate var toolBarHeight: CGFloat = 52.0
fileprivate var toolBarHeight: CGFloat = (52.0 + SafeAraeBottomHeight)
fileprivate var lastTextHeight: CGFloat = 34.0
fileprivate var keyboardHeight: CGFloat = 0.0
@@ -48,14 +50,29 @@ class ChatKeyboardView: UIView {
///
fileprivate var isShowKeyboard = false
///
private lazy var sendVoiceBtn: UIButton = {
let button = UIButton(type: .custom)
let w: CGFloat = ScreenWidth - self.kViewWH * 2 - self.kSpace * 3 - self.kSpace
button.frame = CGRect(x: self.kSpace + self.kLeft + self.kViewWH, y: self.kSpace, width: w, height: 36)
button.setTitle("按住说话", for: .normal)
button.setTitle("松开结束", for: .normal)
button.backgroundColor = ThemeColor(hexStr: "F1F1FA")
button.setTitleColor(ThemeColor(hexStr: "#282828"), for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
button.layer.masksToBounds = true
button.layer.cornerRadius = 4
button.isHidden = true
return button
}()
///
fileprivate lazy var changeButton : UIButton = {
let button = UIButton(type: .custom)
let x: CGFloat = self.kLeft
button.frame = CGRect(x: x, y: self.kSpace, width: self.kViewWH, height: self.kViewWH)
button.setImage(UIImage(named: "chat_more"), for: .normal)
button.setImage(UIImage(named: "chat_more"), for: .highlighted)
button.addTarget(self, action: #selector(moreDidAction(_:)), for: .touchUpInside)
button.frame = CGRect(x: x, y: self.kSpace + 5, width: self.kViewWH, height: self.kViewWH)
button.setImage(UIImage(named: "chat_input_text"), for: .normal)
button.setImage(UIImage(named: "chat_voice"), for: .highlighted)
button.addTarget(self, action: #selector(changeDidAction(_:)), for: .touchUpInside)
return button
}()
@@ -63,7 +80,7 @@ class ChatKeyboardView: UIView {
fileprivate lazy var moreButton : UIButton = {
let button = UIButton(type: .custom)
let x: CGFloat = ScreenWidth - self.kViewWH - self.kSpace
button.frame = CGRect(x: x, y: self.kSpace, width: self.kViewWH, height: self.kViewWH)
button.frame = CGRect(x: x, y: self.kSpace + 5, width: self.kViewWH, height: self.kViewWH)
button.setImage(UIImage(named: "chat_more"), for: .normal)
button.setImage(UIImage(named: "chat_more"), for: .highlighted)
button.addTarget(self, action: #selector(moreDidAction(_:)), for: .touchUpInside)
@@ -109,11 +126,17 @@ class ChatKeyboardView: UIView {
return view
}()
private lazy var voiceView: ChatSendVoiceView = {
let view = ChatSendVoiceView()
view.backgroundColor = .clear
return view
}()
// MARK: - life cycle
override init(frame: CGRect) {
super.init(frame: frame)
NIMSDK.shared().mediaManager.add(self)
setupKeyboardView()
registerNotification()
}
@@ -128,15 +151,23 @@ class ChatKeyboardView: UIView {
func setupKeyboardView() {
self.backgroundColor = .clear
self.isUserInteractionEnabled = true
addSendVoiceAction()
addSubview(toolBarView)
toolBarView.addSubview(moreButton)
toolBarView.addSubview(changeButton)
toolBarView.addSubview(chatTextView)
toolBarView.addSubview(sendVoiceBtn)
addSubview(contentView)
contentView.addSubview(moreMenuView)
}
private func addSendVoiceAction() {
sendVoiceBtn.addTarget(self, action: #selector(audioTouchDownAction), for: .touchDown)
sendVoiceBtn.addTarget(self, action: #selector(audioTouchUpOutsideAction), for: .touchUpOutside)
sendVoiceBtn.addTarget(self, action: #selector(audioTouchUpInsideAction), for: .touchUpInside)
sendVoiceBtn.addTarget(self, action: #selector(audioTouchDragEnterAction), for: .touchDragEnter)
sendVoiceBtn.addTarget(self, action: #selector(audioTouchDragExitAction), for: .touchDragExit)
}
// MARK: -
private func registerNotification() {
@@ -171,18 +202,30 @@ class ChatKeyboardView: UIView {
}
deinit {
NIMSDK.shared().mediaManager.remove(self)
self.removeObserver(self, forKeyPath: "frame")
}
}
extension ChatKeyboardView: NIMMediaManagerDelegate {
func recordAudioInterruptionBegin() {
voiceView.cancelAudioRecord()
}
func recordAudioProgress(_ currentTime: TimeInterval) {
voiceView.updateAudioRecordProgress(recordTime: currentTime)
}
func recordAudio(_ filePath: String?, didCompletedWithError error: Error?) {
if let path = filePath {
delegate?.keyboard(self, voiceDidFinish: path)
}
}
}
// MARK: - ChatMoreMenuViewDelegate
extension ChatKeyboardView: ChatMoreMenuViewDelegate {
func menu(_ view: ChatMoreMenuView, DidSelected type: ChatMoreMenuType) {
delegate?.keyboard(self, DidMoreMenu: type)
}
}
@@ -209,7 +252,6 @@ extension ChatKeyboardView: UITextViewDelegate {
///
private func sendChatMessage() {
delegate?.keyboard(self, DidFinish: self.chatTextView.text ?? "")
changeKeyboardHeight(height: lastTextHeight)
chatTextView.text = ""
chatTextView.attributedText = NSAttributedString(string: "")
@@ -240,12 +282,12 @@ extension ChatKeyboardView {
let option = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as! Int
var changedY = y - self.toolBarHeight - NavHeight - contentHeight
var changedY = y - self.toolBarHeight - contentHeight
if (isShowMore) { //
isShowMore = false
self.moreMenuView.isHidden = true
self.moreMenuView.hidePageController = true
changedY = y - self.toolBarHeight - NavHeight
changedY = y - self.toolBarHeight
}
UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: UIView.AnimationOptions.RawValue(option)), animations: {
@@ -270,7 +312,7 @@ extension ChatKeyboardView {
isShowKeyboard = false
let option = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as! Int
let changedY = ScreenHeight - NavHeight - self.toolBarHeight - SafeAraeBottomHeight - self.contentHeight
let changedY = ScreenHeight - self.toolBarHeight - self.contentHeight
UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: UIView.AnimationOptions.RawValue(option)), animations: {
self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: self.toolBarHeight + self.contentHeight)
}, completion: nil)
@@ -293,10 +335,69 @@ extension ChatKeyboardView {
// MARK: - Action
extension ChatKeyboardView {
@objc func audioTouchDownAction() {
sendVoiceBtn.isSelected = true
//
if voiceView.superview == nil {
UIApplication.shared.keyWindow?.addSubview(voiceView)
voiceView.translatesAutoresizingMaskIntoConstraints = false
voiceView.snp.makeConstraints { make in
make.center.equalToSuperview()
}
voiceView.configAudioRecord(imageName: "chat_voice_record_first", title: "手指上滑,取消发送", isAnimation: true)
voiceView.beginAudioRecord()
}
}
@objc func audioTouchUpOutsideAction() {
sendVoiceBtn.isSelected = false
voiceView.cancelAudioRecord()
voiceView.removeFromSuperview()
}
@objc func audioTouchUpInsideAction() {
sendVoiceBtn.isSelected = false
voiceView.finishAudioRecord()
voiceView.removeFromSuperview()
}
@objc func audioTouchDragEnterAction() {
voiceView.configAudioRecord(imageName: "chat_voice_record_first", title: "手指上滑,取消发送", isAnimation: true)
}
@objc func audioTouchDragExitAction() {
voiceView.configAudioRecord(imageName: "chat_voice_record_cancel", title: "松开手指,取消发送", isAnimation: false)
}
@objc func changeDidAction(_ button: UIButton) {
button.isSelected = !button.isSelected
isShowMore = false
contentView.isHidden = true
moreMenuView.isHidden = true
contentHeight = 0.0
restToolbarContentHeight(true)
if button.isSelected {
chatTextView.resignFirstResponder()
sendVoiceBtn.isHidden = false
chatTextView.isHidden = true
} else {
chatTextView.becomeFirstResponder()
sendVoiceBtn.isHidden = true
chatTextView.isHidden = false
}
delegate?.keyboard(self, DidBecome: false)
}
///
@objc func moreDidAction(_ button: UIButton) {
//
if isShowMore {
isShowMore = false
contentView.isHidden = true
moreMenuView.isHidden = true
contentHeight = 0.0
restToolbarContentHeight(true)
delegate?.keyboard(self, DidBecome: false)
return
}
@@ -314,13 +415,13 @@ extension ChatKeyboardView {
///
func restToolbarContentHeight(_ isRest: Bool = false) {
var changedY = ScreenHeight - self.toolBarHeight - NavHeight - contentHeight
var changedY = ScreenHeight - self.toolBarHeight - contentHeight
if (isRest) {
if (isShowMore) {
isShowMore = false
}
changedY = ScreenHeight - self.toolBarHeight - NavHeight - contentHeight - SafeAraeBottomHeight
changedY = ScreenHeight - self.toolBarHeight - contentHeight
}
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
@@ -355,10 +456,10 @@ extension ChatKeyboardView {
isShowMore = false
}
let changedY = ScreenHeight - keyboardHeight - toolBarHeight - NavHeight
let changedY = ScreenHeight - keyboardHeight - toolBarHeight
self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: toolBarView.height + contentView.height)
}else {
let changedY = ScreenHeight - NavHeight - (toolBarView.height + contentView.height)
let changedY = ScreenHeight - (toolBarView.height + contentView.height)
self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: toolBarView.height + contentView.height)
}

View File

@@ -67,8 +67,7 @@ class ChatMoreMenuView: UIView {
lazy var dataSource: [ChatMoreMnueConfig] = {
let configs = [
ChatMoreMnueConfig(title: "图片", image: "ic_more_album", type: .album),
ChatMoreMnueConfig(title: "拍照", image: "ic_more_camera", type: .camera)
ChatMoreMnueConfig(title: "图片", image: "chat_more_album", type: .album),
]
return configs
}()
@@ -138,7 +137,7 @@ class ChatMoreMenuView: UIView {
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseOut, animations: {
self.pageControl.alpha = 1.0
self.pageControl.isHidden = false
self.pageControl.isHidden = self.dataSource.count > 8 ? false : true
}, completion: nil)
DispatchQueue.main.async {

View File

@@ -0,0 +1,124 @@
//
// ChatSendVoiceView.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/3/1.
//
import UIKit
import NIMSDK
class ChatSendVoiceView: UIView {
private var backView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.layer.masksToBounds = true
view.layer.cornerRadius = 10
return view
}()
private var logoImageView: UIImageView = {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
let firstImage = UIImage(named: "chat_voice_record_first")!
let secondImage = UIImage(named: "chat_voice_record_second")!
let thirdImage = UIImage(named: "chat_voice_record_third")!
imageView.animationImages = [firstImage, secondImage, thirdImage]
imageView.animationDuration = 1
imageView.animationRepeatCount = .max
return imageView
}()
private var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 15)
label.textColor = .white
label.textAlignment = .center
return label
}()
private var timeLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17, weight: .medium)
label.textColor = .white
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
loadSubViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configAudioRecord(imageName: String, title: String, isAnimation: Bool) {
logoImageView.image = UIImage(named: imageName)
if isAnimation {
logoImageView.startAnimating()
} else {
logoImageView.stopAnimating()
}
titleLabel.text = title
}
func beginAudioRecord() {
timeLabel.text = "00:00"
NIMSDK.shared().mediaManager.record(forDuration: 60)
}
func cancelAudioRecord() {
timeLabel.text = "00:00"
NIMSDK.shared().mediaManager.cancelRecord()
}
func finishAudioRecord() {
timeLabel.text = "00:00"
NIMSDK.shared().mediaManager.stopRecord()
}
func updateAudioRecordProgress(recordTime: TimeInterval) {
let minutes = Int(recordTime) / 60
let seconds = Int(recordTime) % 60
timeLabel.text = String(format: "%02d:%02d", minutes, seconds)
}
// Private Method
private func loadSubViews() {
addSubview(backView)
backView.addSubview(logoImageView)
backView.addSubview(titleLabel)
backView.addSubview(timeLabel)
translatesAutoresizingMaskIntoConstraints = false
self.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 250, height: 250))
}
timeLabel.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(25)
}
backView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
logoImageView.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 90, height: 72))
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(50)
}
titleLabel.snp.makeConstraints { make in
make.left.right.equalTo(backView).inset(0)
make.top.equalTo(logoImageView.snp.bottom).offset(25)
}
}
}

View File

@@ -12,6 +12,7 @@ public enum SessionType: Int {
case text = 1
case image
case time
case voice
}
@@ -66,7 +67,7 @@ class ChatTextObject: ChatBaseObject {
let style = NSMutableParagraphStyle()
style.lineSpacing = 6
let attributeStr = NSMutableAttributedString(string: msg?.text ?? "", attributes: [NSAttributedString.Key.font: ChatUIConfig.ui.messageFont, NSAttributedString.Key.paragraphStyle: style])
let attributeStr = NSMutableAttributedString(string: msg?.text ?? "", attributes: [NSAttributedString.Key.font: ChatUIConfig.ui.messageFont, NSAttributedString.Key.foregroundColor:ThemeColor(hexStr: "#2B2D33"), 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))
@@ -83,23 +84,86 @@ class ChatTextObject: ChatBaseObject {
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)
text = timestrToTimeSecond("\(msg?.timestamp ?? 0)")
contentSize = CGSize(width: ScreenWidth, height: 30)
height = Float(50)
}
}
class ChatImageObject: ChatBaseObject {
var url:String = ""
required init(msg: NIMMessage?) {
super.init(msg: msg)
type = .image
if let content = msg?.messageObject as? NIMImageObject {
url = content.url ?? content.thumbUrl ?? ""
}
contentSize = CGSize(width: 200 , height: 200)
height = Float(200 + ChatUIConfig.layout.cellContentInsets.bottom + ChatUIConfig.layout.cellContentInsets.top)
}
}
class ChatVoiceObject: ChatBaseObject {
var filPath:String = ""
var duartion:Int = 0
required init(msg: NIMMessage?) {
super.init(msg: msg)
type = .voice
let voiceHeight = 30.0
if let content = msg?.messageObject as? NIMAudioObject {
let scale = 2*atan((Double(content.duration)/1000.0-1)/10.0)/M_PI;
let low = (ChatUIConfig.layout.contentMaxWidth - 180)
let max = (ChatUIConfig.layout.contentMaxWidth - 100)
let width = (max - low) * scale + low
if let path = content.path {
filPath = path
}
duartion = content.duration
contentSize = CGSize(width: width, height: voiceHeight)
height = Float(voiceHeight + ChatUIConfig.layout.cellContentInsets.bottom + ChatUIConfig.layout.cellContentInsets.top)
} else {
let low = (ChatUIConfig.layout.contentMaxWidth - 180)
contentSize = CGSize(width: low , height: voiceHeight)
height = Float(voiceHeight + ChatUIConfig.layout.cellContentInsets.bottom + ChatUIConfig.layout.cellContentInsets.top)
}
}
}
func timestrToTimeSecond(_ timeStr: String) -> String {
let interval = TimeInterval(timeStr) ?? 0
let date = Date(timeIntervalSince1970: interval)
return stringFromDate(date)
}
func stringFromDate(_ date: Date) -> String {
let currentFormatter = DateFormatter()
if isDateInToday(date) {
currentFormatter.dateFormat = "HH:mm"
} else {
if date.timeIntervalSince1970 > 0 {
currentFormatter.dateFormat = "MM月dd日 HH:mm"
} else {
currentFormatter.dateFormat = "yyyy年MM月dd日 HH:mm"
}
}
let dateString = currentFormatter.string(from: date)
return dateString
}
func isDateInToday(_ date: Date) -> Bool {
let secondsPerDay: TimeInterval = 24 * 60 * 60
let today = Date()
let tomorrow = today.addingTimeInterval(secondsPerDay)
let yesterday = today.addingTimeInterval(-secondsPerDay)
let todayString = String(describing: today).prefix(10)
let yesterdayString = String(describing: yesterday).prefix(10)
let tomorrowString = String(describing: tomorrow).prefix(10)
let dateString = String(describing: date).prefix(10)
return dateString == todayString
}

View File

@@ -46,9 +46,9 @@ public class ChatUIConfig {
///
public var avatarSize: CGFloat = 34.0
/// cell
public var cellContentInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
public var cellContentInsets = UIEdgeInsets(top: 10, left: 16, bottom: 8, right: 16)
///
public var textInsets = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)
public var textInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
///
public var contentMaxWidth: CGFloat = (UIScreen.main.bounds.size.width - 170)
///

View File

@@ -406,8 +406,6 @@ public class ChatViewModel: NSObject,
let timeMsg = NIMMessage()
timeMsg.timestamp = message.timestamp
let model = ChatTimeObject(msg: timeMsg)
model.type = .time
model.text = "2024-2-28"
return model
}
@@ -418,11 +416,12 @@ public class ChatViewModel: NSObject,
switch message.messageType {
case .text:
model = ChatTextObject(msg: message)
case .audio:
model = ChatVoiceObject(msg: message)
case .image:
model = ChatImageObject(msg: message)
default:
return nil
// text
// message.text = ""
// model = MAIMessageContentModel(message: message)
}
if let uid = message.from {

View File

@@ -7,6 +7,7 @@
import UIKit
import Reusable
import NIMSDK
protocol ChatBaseCellProtocol: NSObjectProtocol {
func cell(_ cell: ChatBaseCell, didTapAvatarAt model: ChatBaseObject)
}
@@ -15,7 +16,16 @@ class ChatBaseCell: UITableViewCell, Reusable{
weak var delegate: ChatBaseCellProtocol?
var model:ChatSessionProtocol? {
didSet {
guard let _ = model else {return}
guard let model = model else {return}
if let sessionId = model.msg?.session?.sessionId {
let user = NIMSDK.shared().userManager.userInfo(sessionId)
if let url = user?.userInfo?.avatarUrl {
self.avatarImgView.kf.setImage(with: URL(string: url),
placeholder: UIImage(named: "public_avatar"))
} else {
}
}
layoutMessageCell()
}
}
@@ -45,7 +55,7 @@ class ChatBaseCell: UITableViewCell, Reusable{
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
backgroundColor = ThemeColor(hexStr: "#F8F8FB")
contentView.addSubview(avatarImgView)
contentView.addSubview(bubbleView)
contentView.addSubview(activityIndicatorView)
@@ -61,11 +71,15 @@ class ChatBaseCell: UITableViewCell, Reusable{
imageView.isUserInteractionEnabled = true
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = ChatUIConfig.layout.avatarSize / 2.0
return imageView
}()
lazy var bubbleView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = .white
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 8
return imageView
}()

View File

@@ -0,0 +1,88 @@
//
// ChatImageCell.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/3/1.
//
import UIKit
class ChatImageCell: ChatBaseCell {
override var model: ChatSessionProtocol? {
didSet {
if let model = model as? ChatImageObject {
backImageView.kf.setImage(with: URL(string: model.url),
placeholder: UIImage(named: "public_avatar"))
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
bubbleView.addSubview(backImageView)
backImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var backImageView: UIImageView = {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
}
extension ChatImageCell {
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.remakeConstraints { 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)
}
backImageView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
// 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.remakeConstraints { make in
make.size.equalTo(model.contentSize)
make.top.equalTo(avatarImgView)
make.left.equalTo(avatarImgView.snp.right).offset(10)
}
backImageView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
}
}
}

View File

@@ -97,7 +97,7 @@ class ChatListCell: UITableViewCell, Reusable {
let user = users?[safe:0]
if let url = user?.userInfo?.avatarUrl {
self.avatarImgView.kf.setImage(with: URL(string: url),
placeholder: nil)
placeholder: UIImage(named: "public_avatar"))
}
self.nameLb.text = user?.userInfo?.nickName

View File

@@ -9,6 +9,18 @@ import UIKit
class ChatNavView: BaseView {
var isLike:Bool? {
didSet {
self.likeBtn.isHidden = isLike ?? false
}
}
var name:String? {
didSet {
self.titleLb.text = name
}
}
override func loadSubViews() {
addSubview(backBtn)
addSubview(titleLb)
@@ -57,10 +69,11 @@ class ChatNavView: BaseView {
let button = UIButton(type: .custom)
button.setTitle("关注", for: .normal)
button.setTitleColor(ThemeColor(hexStr: "#FFDA24"), for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
button.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .medium)
button.layer.masksToBounds = true
button.layer.cornerRadius = 26
button.isEnabled = false
button.layer.cornerRadius = 11
button.layer.borderWidth = 0.5
button.layer.borderColor = ThemeColor(hexStr: "#FFDA24").cgColor
button.addTarget(self, action: #selector(likeClick), for: .touchUpInside)
return button
}()

View File

@@ -19,18 +19,19 @@ class ChatTextCell: ChatBaseCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initSubview()
}
func initSubview() {
bubbleView.addSubview(textLb)
avatarImgView.snp.remakeConstraints { (make) in
avatarImgView.snp.makeConstraints { (make) in
make.size.equalTo(ChatUIConfig.layout.avatarSize)
make.left.equalToSuperview().offset(10)
make.top.equalTo(contentView).offset(10)
}
bubbleView.snp.remakeConstraints { (make) in
bubbleView.snp.makeConstraints { (make) in
make.top.equalTo(contentView.snp.top).offset(2)
make.bottom.equalTo(textLb.snp.bottom).offset(2)
make.left.equalTo(avatarImgView.snp.right)
@@ -38,13 +39,13 @@ class ChatTextCell: ChatBaseCell {
make.height.equalTo(textLb).offset(26)
}
textLb.snp.remakeConstraints { (make) in
textLb.snp.makeConstraints { (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
activityIndicatorView.snp.makeConstraints { (make) in
make.centerY.equalTo(bubbleView)
make.centerX.equalToSuperview()
make.width.height.equalTo(30)
@@ -78,7 +79,7 @@ extension ChatTextCell {
make.top.equalToSuperview().offset(ChatUIConfig.layout.cellContentInsets.top)
}
bubbleView.snp.makeConstraints { make in
bubbleView.snp.remakeConstraints { make in
make.size.equalTo(model.contentSize)
make.top.equalTo(avatarImgView)
make.right.equalTo(avatarImgView.snp.left).offset(-10)
@@ -89,8 +90,8 @@ extension ChatTextCell {
make.width.height.equalTo(30)
}
textLb.snp.makeConstraints { make in
make.edges.equalTo(ChatUIConfig.layout.textInsets)
textLb.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(ChatUIConfig.layout.textInsets)
}
// start
@@ -103,14 +104,14 @@ extension ChatTextCell {
make.top.equalToSuperview().offset(ChatUIConfig.layout.cellContentInsets.top)
}
bubbleView.snp.makeConstraints { make in
bubbleView.snp.remakeConstraints { 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)
textLb.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(ChatUIConfig.layout.textInsets)
}
}
}

View File

@@ -0,0 +1,43 @@
//
// ChatTimeCell.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/3/1.
//
import UIKit
import Reusable
class ChatTimeCell: UITableViewCell, Reusable {
var model:ChatSessionProtocol? {
didSet {
if let model = model as? ChatTimeObject {
self.timeLb.text = model.text
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = ThemeColor(hexStr: "#F8F8FB")
contentView.addSubview(timeLb)
timeLb.snp.makeConstraints { make in
make.edges.equalTo(contentView)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var timeLb: UILabel = {
let label = UILabel()
label.textColor = ThemeColor(hexStr: "#A2A7B8")
label.font = UIFont.systemFont(ofSize: 12)
label.textAlignment = .center
return label
}()
}

View File

@@ -0,0 +1,156 @@
//
// ChatVoiceCell.swift
// yinmeng-ios
//
// Created by MaiMang on 2024/3/1.
//
import UIKit
import NIMSDK
class ChatVoiceCell: ChatBaseCell, NIMMediaManagerDelegate {
override var model: ChatSessionProtocol? {
didSet {
if let model = model as? ChatVoiceObject {
timeLabel.text = "\(model.duartion / 1000)"
}
}
}
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.textColor = ThemeColor(hexStr: "#2B2D33")
label.font = UIFont.systemFont(ofSize: 14, weight: .medium)
return label
}()
private lazy var voiceImageView: UIImageView = {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
let firstImage = UIImage(named:"chat_audio_playing_first")!
let second = UIImage(named:"chat_audio_playing_first")!
let third = UIImage(named:"chat_audio_playing_first")!
imageView.animationImages = [firstImage, second, third];
imageView.animationDuration = 1
imageView.animationRepeatCount = 100
imageView.image = third
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
NIMSDK.shared().mediaManager.setNeedProximityMonitor(false)
NIMSDK.shared().mediaManager.add(self)
loadSubViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NIMSDK.shared().mediaManager.remove(self)
}
func loadSubViews() {
bubbleView.addSubview(timeLabel)
bubbleView.addSubview(voiceImageView)
timeLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.centerY.equalToSuperview()
}
voiceImageView.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 20, height: 20))
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-10)
}
}
func playAudio(_ filePath: String, didBeganWithError error: Error?) {
if filePath.isEmpty || error != nil { return }
voiceImageView.startAnimating()
}
func playAudio(_ filePath: String, didCompletedWithError error: Error?) {
voiceImageView.stopAnimating()
let thirdImage = UIImage(named: "chat_audio_playing_third")
voiceImageView.image = thirdImage
}
@objc func didTapBackRecognizer() {
if NIMSDK.shared().mediaManager.isPlaying() {
NIMSDK.shared().mediaManager.stopPlay()
}
if let path = self.model as? ChatVoiceObject , path.filPath.count > 0{
NIMSDK.shared().mediaManager.play(path.filPath)
}
}
}
extension ChatVoiceCell {
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.remakeConstraints { 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)
}
timeLabel.snp.remakeConstraints { make in
make.left.equalToSuperview().offset(10)
make.centerY.equalToSuperview()
}
voiceImageView.snp.remakeConstraints { make in
make.size.equalTo(CGSize(width: 20, height: 20))
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-10)
}
// 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.remakeConstraints { make in
make.size.equalTo(model.contentSize)
make.top.equalTo(avatarImgView)
make.left.equalTo(avatarImgView.snp.right).offset(10)
}
timeLabel.snp.remakeConstraints { make in
make.left.equalToSuperview().offset(10)
make.centerY.equalToSuperview()
}
voiceImageView.snp.remakeConstraints { make in
make.size.equalTo(CGSize(width: 20, height: 20))
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-10)
}
}
}
}