| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- //
- // JCChatView.swift
- // JChat
- //
- // Created by deng on 2017/2/28.
- // Copyright © 2017年 HXHG. All rights reserved.
- //
- import UIKit
- public protocol JCChatViewDataSource: class {
-
- func numberOfItems(in chatView: JCChatView)
-
- func chatView(_ chatView: JCChatView, itemAtIndexPath: IndexPath)
-
- }
- var isWait = false
- @objc public protocol JCChatViewDelegate: NSObjectProtocol {
-
- @objc optional func chatView(_ chatView: JCChatView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool
- @objc optional func chatView(_ chatView: JCChatView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool
- @objc optional func chatView(_ chatView: JCChatView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?)
- @objc optional func refershChatView(chatView: JCChatView)
- @objc optional func tapImageMessage(image: UIImage?, indexPath: IndexPath)
-
- @objc optional func deleteMessage(message: JCMessageType)
- @objc optional func copyMessage(message: JCMessageType)
- @objc optional func forwardMessage(message: JCMessageType)
- @objc optional func withdrawMessage(message: JCMessageType)
- @objc optional func indexPathsForVisibleItems(chatView: JCChatView, items: [IndexPath])
- }
- @objc open class JCChatView: UIView {
-
- public init(frame: CGRect, chatViewLayout: JCChatViewLayout) {
- _chatViewData = JCChatViewData()
- _chatViewLayout = chatViewLayout
- let containerViewFrame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
- _chatContainerView = JCChatContainerView(frame: containerViewFrame, collectionViewLayout: chatViewLayout)
- super.init(frame: frame)
- _commonInit()
- }
-
- public required init?(coder aDecoder: NSCoder) {
- // decode layout
- guard let chatViewLayout = JCChatViewLayout(coder: aDecoder) else {
- return nil
- }
- // decode container view
- guard let chatContainerView = JCChatContainerView(coder: aDecoder) else {
- return nil
- }
- // init data
- _chatViewData = JCChatViewData()
- // init to layout & container view
- _chatViewLayout = chatViewLayout
- _chatContainerView = chatContainerView
- // init super
- super.init(coder: aDecoder)
- // init other data
- _commonInit()
- }
-
- open weak var delegate: JCChatViewDelegate?
- open weak var dataSource: JCChatViewDataSource?
- open weak var messageDelegate: JCMessageDelegate?
-
- func insert(_ newMessage: JCMessageType, at index: Int) {
- _batchBegin()
- _batchItems.append(.insert(newMessage, at: index))
- _batchCommit()
- }
- func insert(contentsOf newMessages: Array<JCMessageType>, at index: Int) {
- _batchBegin()
- _batchItems.append(contentsOf: newMessages.map({ .insert($0, at: index) }))
- _batchCommit(true)
- }
-
- func update(_ newMessage: JCMessageType, at index: Int) {
- _batchBegin()
- _batchItems.append(.update(newMessage, at: index))
- _batchCommit()
- }
-
- func removeAll() {
- _batchBegin()
- for index in 0..<_chatViewData.count {
- _batchItems.append(.remove(at: index))
- }
- _batchCommit()
- }
-
- func remove(at index: Int) {
- _batchBegin()
- _batchItems.append(.remove(at: index))
- _batchCommit()
- }
- func remove(contentOf indexs: Array<Int>) {
- _batchBegin()
- _batchItems.append(contentsOf: indexs.map({ .remove(at: $0) }))
- _batchCommit()
- }
-
- func move(at index1: Int, to index2: Int) {
- _batchBegin()
- _batchItems.append(.move(at: index1, to: index2))
- _batchCommit()
- }
-
- func append(_ newMessage: JCMessageType) {
- insert(newMessage, at: _chatViewData.count)
- }
- func append(contentsOf newMessages: Array<JCMessageType>) {
- insert(contentsOf: newMessages, at: _chatViewData.count)
- }
-
- fileprivate func _batchBegin() {
- _chatContainerView.messageDelegate = self.messageDelegate
- objc_sync_enter(_batchItems)
- _batchRequiredCount = max(_batchRequiredCount + 1, 1)
- objc_sync_exit(_batchItems)
- }
- fileprivate func _batchCommit(_ isInsert: Bool = false) {
- objc_sync_enter(_batchItems)
- _batchRequiredCount = max(_batchRequiredCount - 1, 0)
- guard _batchRequiredCount == 0 else {
- objc_sync_exit(_batchItems)
- return
- }
- let oldData = _chatViewData
- let newData = JCChatViewData()
- let updateItems = _batchItems
- _batchItems.removeAll()
- objc_sync_exit(_batchItems)
-
- _ = _chatContainerView.numberOfItems(inSection: 0)
- let update = JCChatViewUpdate(newData: newData, oldData: oldData, updateItems: updateItems)
- // exec
- _chatViewData = newData
- _chatContainerView.performBatchUpdates(with: update, isInsert, completion: nil)
- }
-
- fileprivate lazy var _batchItems: Array<JCChatViewUpdateChangeItem> = []
- fileprivate lazy var _batchRequiredCount: Int = 0
-
- private func _commonInit() {
-
- backgroundColor = UIColor(netHex: 0xe8edf3)
- let header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(_onPullToFresh))
- header?.stateLabel.isHidden = true
- _chatContainerView.mj_header = header
- _chatContainerView.allowsSelection = false
- _chatContainerView.allowsMultipleSelection = false
- _chatContainerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- _chatContainerView.keyboardDismissMode = .onDrag
- _chatContainerView.backgroundColor = UIColor(netHex: 0xE8EDF3)
- _chatContainerView.dataSource = self
- _chatContainerView.delegate = self
-
- addSubview(_chatContainerView)
- #if READ_VERSION
- _chatContainerView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
- #endif
- }
-
- fileprivate var _chatViewData: JCChatViewData
-
- fileprivate var _chatViewLayout: JCChatViewLayout
- fileprivate var _chatContainerView: JCChatContainerView
-
- fileprivate lazy var _chatContainerRegistedTypes: Set<String> = []
-
- @objc func _onPullToFresh() {
- delegate?.refershChatView?(chatView: self)
- }
- func stopRefresh() {
- _chatContainerView.mj_header.endRefreshing()
- }
-
- func scrollToLast(animated: Bool) {
- let count = _chatContainerView.numberOfItems(inSection: 0)
- if count > 0 {
- _chatContainerView.scrollToItem(at: IndexPath(row: count - 1, section: 0), at: .bottom, animated: animated)
- }
- }
- deinit {
- #if READ_VERSION
- _chatContainerView.removeObserver(self, forKeyPath: "contentOffset")
- #endif
- }
- }
- internal class JCChatContainerView: UICollectionView {
-
- weak var messageDelegate: JCMessageDelegate?
-
- var currentUpdate: JCChatViewUpdate? {
- return _currentUpdate
- }
-
- func performBatchUpdates(with update: JCChatViewUpdate, _ isInsert: Bool = false, completion:((Bool) -> Void)?) {
-
- // read changes
- guard let changes = update.updateChanges else {
- return
- }
- _currentUpdate = update
-
- // TODO: 不是最优
- if update.updateItems.count > 0 {
- for item in update.updateItems {
- switch item {
- case .update:
- self.performBatchUpdates({
- self.reloadItems(at: [IndexPath(row: item.at, section: 0)])
- }, completion: nil)
- return
- default:
- break
- }
- }
- }
-
-
- var oldContent = self.contentSize
- // self.contentSize = CGSize(width: self.contentSize.width, height: 3725)
- // self.setContentOffset(CGPoint(x: 0, y: 3725 - oldContent.height), animated: false)
- // self.layoutIfNeeded()
- // self.setContentOffset(CGPoint(x: 0, y: 250232320), animated: false)
- // self.layoutIfNeeded()
- // commit changes
-
- UIView.animate(withDuration: 0) {
- if isInsert {
- self.isHidden = true
- }
- self.performBatchUpdates({
- // apply move
- changes.filter({ $0.isMove }).forEach({
- self.moveItem(at: .init(item: max($0.from, 0), section: 0),
- to: .init(item: max($0.to, 0), section: 0))
- })
- // print(oldContent)
- // apply insert/remove/update
- self.insertItems(at: changes.filter({ $0.isInsert }).map({ .init(item: max($0.to, 0), section: 0) }))
- self.reloadItems(at: changes.filter({ $0.isUpdate }).map({ .init(item: max($0.from, 0), section: 0) }))
- self.deleteItems(at: changes.filter({ $0.isRemove }).map({ .init(item: max($0.from, 0), section: 0) }))
-
- }, completion: { finished in
- if isInsert {
- UIView.animate(withDuration: 0, animations: {
- if self.contentSize.height > oldContent.height && oldContent.height != 0 {
- self.setContentOffset(CGPoint(x: 0, y: self.contentSize.height - oldContent.height), animated: false)
- self.layoutIfNeeded()
- oldContent = self.contentSize
- }
- })
- self.isHidden = false
- }
-
- completion?(finished)
- })
- }
-
- _currentUpdate = nil
- }
-
- private var _currentUpdate: JCChatViewUpdate?
- }
- extension JCChatView: UICollectionViewDataSource, JCChatViewLayoutDelegate {
-
- open var isRoll: Bool {
- return _chatContainerView.isDragging || _chatContainerView.isDecelerating
- }
-
- open dynamic var indexPathsForVisibleItems: [IndexPath] {
- return _chatContainerView.indexPathsForVisibleItems
- }
-
- open dynamic var contentSize: CGSize {
- set { return _chatContainerView.contentSize = newValue }
- get { return _chatContainerView.contentSize }
- }
- open dynamic var contentOffset: CGPoint {
- set { return _chatContainerView.contentOffset = newValue }
- get { return _chatContainerView.contentOffset }
- }
- open dynamic var contentInset: UIEdgeInsets {
- set { return _chatContainerView.contentInset = newValue }
- get { return _chatContainerView.contentInset }
- }
- open dynamic var scrollIndicatorInsets: UIEdgeInsets {
- set { return _chatContainerView.scrollIndicatorInsets = newValue }
- get { return _chatContainerView.scrollIndicatorInsets }
- }
-
- open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- return _chatViewData.count
- }
-
- open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
- let message = _chatViewData[indexPath.item]
-
- // let options = (message.options.showsCard.hashValue << 0) | (message.options.showsAvatar.hashValue << 1)
- let alignment = message.options.alignment.rawValue
- let identifier = NSStringFromClass(type(of: message.content)) + ".\(alignment)"
-
- if !_chatContainerRegistedTypes.contains(identifier) {
- _chatContainerRegistedTypes.insert(identifier)
- _chatContainerView.register(JCChatViewCell.self, forCellWithReuseIdentifier: identifier)
- }
- let cell = _chatContainerView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! JCChatViewCell
- cell.delegate = messageDelegate
- cell.updateView()
- return cell
- }
-
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, itemAt indexPath: IndexPath) -> JCMessageType {
- return _chatViewData[indexPath.item]
- }
-
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
- guard let collectionViewLayout = collectionViewLayout as? JCChatViewLayout else {
- return .zero
- }
- guard let layoutAttributesInfo = collectionViewLayout.layoutAttributesInfoForItem(at: indexPath) else {
- return .zero
- }
- let size = layoutAttributesInfo.layoutedBoxRect(with: .all).size
- return .init(width: collectionView.frame.width, height: size.height)
- }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAvatarOf style: JCMessageStyle) -> CGSize {
- // 78 * 78
- return .init(width: 40, height: 40)
- }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemCardOf style: JCMessageStyle) -> CGSize {
- return .init(width: 0, height: 18)
- }
-
- public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemTipsOf style: JCMessageStyle) -> CGSize {
- return .init(width: 100, height: 21)
- }
-
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemOf style: JCMessageStyle) -> UIEdgeInsets {
- switch style {
- case .bubble:
- // bubble content edg, 2x
- // +----12--+-+---+
- // | | | |
- // 16 4 40 16
- // | | | |
- // +----12--+-+---+
- return .init(top: 6, left: 8, bottom: 6, right: 2 + 20 + 8)
-
- case .notice:
- // default edg
- // +----10----+
- // 20 20
- // +----10----+
- return .init(top: 10, left: 20, bottom: 10, right: 20)
-
- // default:
- // // default edg
- // // +----10----+
- // // 10 10
- // // +----10----+
- // return .init(top: 10, left: 10, bottom: 10, right: 10)
- }
- }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemCardOf style: JCMessageStyle) -> UIEdgeInsets {
- return .init(top: 0, left: 8, bottom: 2, right: 8)
- }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemAvatarOf style: JCMessageStyle) -> UIEdgeInsets {
- return .init(top: 0, left: 2, bottom: 2, right: 2)
- }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemBubbleOf style: JCMessageStyle) -> UIEdgeInsets {
- // return .init(top: -2, left: 0, bottom: -2, right: 0)
- return .init(top: 0, left: 8, bottom: 0, right: 0)
- }
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemContentOf style: JCMessageStyle) -> UIEdgeInsets {
- switch style {
- case .bubble:
- // bubble image edg, scale: 2x, radius: 15
- // /--------16-------\
- // | +-----04-----+ |
- // 20 04 04 20
- // | +-----04-----+ |
- // \--------16-------/
- // return .init(top: 8 + 2, left: 10 + 2, bottom: 8 + 2, right: 10 + 2)
- return .init(top: 2, left: 5 + 2, bottom: 2, right: 2)
-
- case .notice:
- // notice edg
- // /------4-------\
- // 10 10
- // \------4-------/
- return .init(top: 4, left: 10, bottom: 4, right: 10)
-
- }
- }
-
- open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
- return true
- }
-
- open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
- let message = _chatViewData[indexPath.item]
- if message.content is JCMessageNoticeContent || message.content is JCMessageTimeLineContent {
- return false
- }
- if let _ = message.content as? JCMessageTextContent {
- if action == #selector(copyMessage(_:)) {
- return true
- }
- }
- if action == #selector(deleteMessage(_:)) {
- return true
- }
-
- if action == #selector(forwardMessage(_:)) {
- return true
- }
-
- if action == #selector(withdrawMessage(_:)) {
- if let sender = message.sender {
- if sender.isEqual(to: JMSGUser.myInfo()) {
- return true
- }
- }
- return false
- }
-
- return false
- }
-
- @objc func copyMessage(_ sender: Any) {}
- @objc func deleteMessage(_ sender: Any) {}
- @objc func forwardMessage(_ sender: Any) {}
- @objc func withdrawMessage(_ sender: Any) {}
-
- open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
- let message = _chatViewData[indexPath.item]
- if action == #selector(copyMessage(_:)) {
- if let content = message.content as? JCMessageTextContent {
- let pas = UIPasteboard.general
- pas.string = content.text.string
- }
- }
- if action == #selector(deleteMessage(_:)) {
- remove(at: indexPath.item)
- delegate?.deleteMessage?(message: message)
- }
- // if action == #selector(paste(_:)) {
- // move(at: indexPath.item, to: _chatViewData.count - 1)
- // }
- if action == #selector(forwardMessage(_:)) {
- delegate?.forwardMessage?(message: message)
- }
-
- if action == #selector(withdrawMessage(_:)) {
- delegate?.withdrawMessage?(message: message)
- }
- }
- }
- extension JCChatView {
- override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
- if keyPath == "contentOffset" {
- DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
- if !isWait {
- isWait = true
- DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
- self.delegate?.indexPathsForVisibleItems?(chatView: self, items: self._chatContainerView.indexPathsForVisibleItems)
- isWait = false
- }
- }
- }
- }
- }
- }
- extension JCChatView: SAIInputBarScrollViewType {
- }
|