diff --git a/ChatK!t.podspec b/ChatK!t.podspec index 7a151ded..0e3a5ffa 100755 --- a/ChatK!t.podspec +++ b/ChatK!t.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/chat-sdk/chat-sdk-ios.git", :tag => s.version.to_s } s.module_name = 'ChatKit' - s.platform = :ios, '11.0' + s.platform = :ios, '13.0' s.requires_arc = true s.swift_version = "5.0" @@ -20,6 +20,7 @@ Pod::Spec.new do |s| s.dependency 'ChatSDKKeepLayout' s.dependency 'NextGrowingTextView' s.dependency 'CollectionKit' + s.dependency 'SVPullToRefresh' s.subspec 'ChatSDK' do |s| s.source_files = ['ChatK!t/ChatSDK/**/*'] diff --git a/ChatK!t/ChatSDK/CKChatModelDelegate.swift b/ChatK!t/ChatSDK/CKChatModelDelegate.swift new file mode 100644 index 00000000..22fb1bf9 --- /dev/null +++ b/ChatK!t/ChatSDK/CKChatModelDelegate.swift @@ -0,0 +1,54 @@ +// +// CKMessagesModelDelegate.swift +// ChatK!t +// +// Created by ben3 on 04/05/2021. +// + +import Foundation +import ChatSDK +import RXPromise + +public class CKChatModelDelegate: ChatModelDelegate { + + public override func loadMessages(with oldestMessage: Message?) -> Single<[Message]> { + return Single<[Message]>.create { [weak self] single in + if let model = self?._model, let thread = BChatSDK.db().fetchEntity(withID: model.thread().threadId(), withType: bThreadEntity) as? PThread { + _ = BChatSDK.thread().loadMoreMessages(from: oldestMessage?.messageDate(), for: thread).thenOnMain({ success in + if let messages = success as? [PMessage] { + single(.success(CKChatModelDelegate.convert(messages))) + } else { + single(.success([])) + } + return success + }, { error in + single(.success([])) + return error + }) + } + return Disposables.create {} + } + } + + public override func initialMessages() -> [Message] { + var messages = [Message]() + if let model = _model, let thread = BChatSDK.db().fetchEntity(withID: model.thread().threadId(), withType: bThreadEntity) as? PThread { + for message in BChatSDK.db().loadMessages(for: thread, newest: 15) { + if let message = message as? PMessage { + messages.insert(CKMessage(message: message), at: 0) + } + } + } + return messages + } + + public static func convert(_ messages: [PMessage]) -> [Message] { + var output = [Message]() + for message in messages { + output.append(CKMessage(message: message)) + } + return output + } + +} + diff --git a/ChatK!t/ChatSDK/CKMessage.swift b/ChatK!t/ChatSDK/CKMessage.swift index c315fa46..ebeff287 100644 --- a/ChatK!t/ChatSDK/CKMessage.swift +++ b/ChatK!t/ChatSDK/CKMessage.swift @@ -26,39 +26,39 @@ public class CKMessage: Message { self.message = message } - public func messageId() -> String { + public override func messageId() -> String { return entityId! } - public func messageDate() -> Date { + public override func messageDate() -> Date { return date! } - public func messageText() -> String? { + public override func messageText() -> String? { return text } - public func messageSender() -> User { + public override func messageSender() -> User { return sender } - public func messageImageUrl() -> URL? { + public override func messageImageUrl() -> URL? { return imageUrl } - public func messageType() -> String { + public override func messageType() -> String { return type! } - public func messageMeta() -> [AnyHashable: Any]? { + public override func messageMeta() -> [AnyHashable: Any]? { return meta! } - public func messageDirection() -> MessageDirection { + public override func messageDirection() -> MessageDirection { return direction } - public func messageReadStatus() -> MessageReadStatus { + public override func messageReadStatus() -> MessageReadStatus { if BChatSDK.readReceipt() != nil && messageDirection() == .outgoing { if let status = message.messageReadStatus?() { if status == bMessageReadStatusRead { @@ -75,7 +75,7 @@ public class CKMessage: Message { return .none } - public func messageReply() -> Reply? { + public override func messageReply() -> Reply? { if message.isReply() { // Get the user's name var fromUser: PUser? diff --git a/ChatK!t/ChatSDK/CKThread.swift b/ChatK!t/ChatSDK/CKThread.swift index 1b8f1e6c..b2d18f3c 100644 --- a/ChatK!t/ChatSDK/CKThread.swift +++ b/ChatK!t/ChatSDK/CKThread.swift @@ -50,17 +50,7 @@ open class CKThread: Thread { } return users } - - open func threadMessages() -> [Message] { - var messages = [Message]() - for message in _thread.messagesOrderedByDateOldestFirst() { - if let message = message as? PMessage { - messages.append(CKMessage(message: message)) - } - } - return messages - } - + open func threadType() -> ThreadType { return _threadType } diff --git a/ChatK!t/Core/ChatKit.swift b/ChatK!t/Core/ChatKit.swift index 05841830..0b044ab2 100644 --- a/ChatK!t/Core/ChatKit.swift +++ b/ChatK!t/Core/ChatKit.swift @@ -13,6 +13,7 @@ public class ChatKit { public static let instance = ChatKit() + public static func shared() -> ChatKit { return instance } @@ -39,6 +40,7 @@ public class ChatKit { public static func provider() -> Provider { return shared().provider } + } diff --git a/ChatK!t/Core/ChatModel.swift b/ChatK!t/Core/ChatModel.swift index cbc29a27..dcafce88 100644 --- a/ChatK!t/Core/ChatModel.swift +++ b/ChatK!t/Core/ChatModel.swift @@ -8,7 +8,31 @@ import Foundation import RxSwift -public class ChatModel: NSObject { +public protocol PChatModelDelegate: PMessagesModelDelegate { +} + +public class ChatModelDelegate: PChatModelDelegate { + + weak var _model: ChatModel? + + public init() { + } + + public func setModel(_ model: ChatModel) { + _model = model + } + + public func loadMessages(with oldestMessage: Message?) -> Single<[Message]> { + preconditionFailure("This method must be overridden") + } + + public func initialMessages() -> [Message] { + preconditionFailure("This method must be overridden") + } + +} + +public class ChatModel { public let _thread: Thread public var _options = [Option]() @@ -17,13 +41,16 @@ public class ChatModel: NSObject { public var _keyboardOverlays = [String: KeyboardOverlay]() public var _view: PChatViewController? + public let _delegate: ChatModelDelegate public lazy var _messagesModel = { - return ChatKit.provider().messagesModel(_thread) + return ChatKit.provider().messagesModel(_thread, delegate: _delegate) }() - public init(_ thread: Thread) { + public init(_ thread: Thread, delegate: ChatModelDelegate) { _thread = thread + _delegate = delegate + _delegate.setModel(self) } open func messagesModel() -> MessagesModel { @@ -43,7 +70,7 @@ public class ChatModel: NSObject { if _thread.threadType() == .private1to1 { if let user = _thread.threadOtherUser() { if user.userIsOnline() { - return t(Strings.online) + return Strings.t(Strings.online) } else if let lastOnline = user.userLastOnline() as NSDate?, let text = lastOnline.lastSeenTimeAgo() { return text } @@ -69,7 +96,7 @@ public class ChatModel: NSObject { */ open func initialSubtitle() -> String? { if ChatKit.config().userChatInfoEnabled { - return t(Strings.tapHereForContactInfo) + return Strings.t(Strings.tapHereForContactInfo) } return nil } @@ -114,8 +141,12 @@ public class ChatModel: NSObject { _view = view } - public func loadMessages() { - _messagesModel.loadMessages() + public func loadInitialMessages() { + _messagesModel.loadInitialMessages() + } + + public func thread() -> Thread { + return _thread } } diff --git a/ChatK!t/Core/ChatViewController.swift b/ChatK!t/Core/ChatViewController.swift index c9344c26..1eea189a 100644 --- a/ChatK!t/Core/ChatViewController.swift +++ b/ChatK!t/Core/ChatViewController.swift @@ -82,9 +82,10 @@ public class ChatViewController: UIViewController { setupKeyboardListener() setupKeyboardOverlays() - model.loadMessages() + model.loadInitialMessages() } + override public func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() @@ -99,6 +100,7 @@ public class ChatViewController: UIViewController { override public func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() updateMessageViewBottomInset() + print("New Height after layout: \(messagesView._tableView.contentSize.height)") } public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -108,11 +110,13 @@ public class ChatViewController: UIViewController { } public func updateMessageViewBottomInset(keyboardHeight: CGFloat? = nil) { - var keyboardHeight = keyboardHeight ?? -(sendBarViewBottomConstraint?.constant ?? 0) + var height = keyboardHeight ?? -(sendBarViewBottomConstraint?.constant ?? 0) if (replyView.isVisible() || replyView.willShow) && !replyView.willHide { - keyboardHeight += replyView.frame.size.height + height += replyView.frame.size.height } - messagesView.setBottomInset(height: keyboardHeight + sendBarView.frame.size.height) + height += sendBarView.frame.size.height + print("Height: ", height) + messagesView.setBottomInset(height: height) } override public func viewWillAppear(_ animated: Bool) { @@ -248,9 +252,9 @@ public class ChatViewController: UIViewController { } } - public func clearSelection() { - model.messagesModel().clearSelection(true) - } +// public func clearSelection() { +// model.messagesModel().clearSelection() +// } public func setuptoolbar() { view.addSubview(toolbar) diff --git a/ChatK!t/Core/Customization/Config.swift b/ChatK!t/Core/Customization/Config.swift index cb107fdf..85bfd3e5 100644 --- a/ChatK!t/Core/Customization/Config.swift +++ b/ChatK!t/Core/Customization/Config.swift @@ -53,6 +53,8 @@ public class Config { public var messagesViewRefreshHeight: CGFloat = 300 public var messagesViewSectionViewCornerRadius: CGFloat = 5 + + public var messagesViewSectionHeight: CGFloat = 40 // The reply view that shows inside the message bubble public var messageReplyViewHeight: CGFloat = 50 diff --git a/ChatK!t/Core/Customization/Provider.swift b/ChatK!t/Core/Customization/Provider.swift index f788cd20..d8c60714 100644 --- a/ChatK!t/Core/Customization/Provider.swift +++ b/ChatK!t/Core/Customization/Provider.swift @@ -82,7 +82,7 @@ public class Provider { return SendBarView() } - public func messagesModel(_ thread: Thread) -> MessagesModel { - return MessagesModel(thread) + public func messagesModel(_ thread: Thread, delegate: PMessagesModelDelegate) -> MessagesModel { + return MessagesModel(thread, delegate: delegate) } } diff --git a/ChatK!t/Core/Entities/Message.swift b/ChatK!t/Core/Entities/Message.swift index 4b011ae0..9673d3d9 100644 --- a/ChatK!t/Core/Entities/Message.swift +++ b/ChatK!t/Core/Entities/Message.swift @@ -7,7 +7,7 @@ import Foundation -public protocol Message { +public protocol IMessage { func messageId() -> String func messageType() -> String @@ -22,13 +22,86 @@ public protocol Message { } -public extension Message { - +public class Message: IMessage, Hashable, Equatable { + + var selected = false + func sameDayAs(_ message: Message) -> Bool { return Calendar.current.isDate(messageDate(), inSameDayAs: message.messageDate()) } + + public func messageId() -> String { + preconditionFailure("This method must be overridden") + } + + public func messageType() -> String { + preconditionFailure("This method must be overridden") + } + + public func messageDate() -> Date { + preconditionFailure("This method must be overridden") + } + + public func messageText() -> String? { + return nil + } + + public func messageSender() -> User { + preconditionFailure("This method must be overridden") + } + + public func messageImageUrl() -> URL? { + return nil + } + + public func messageMeta() -> [AnyHashable: Any]? { + return nil + } + + public func messageDirection() -> MessageDirection { + preconditionFailure("This method must be overridden") + } + + public func messageReadStatus() -> MessageReadStatus { + preconditionFailure("This method must be overridden") + } + + public func messageReply() -> Reply? { + return nil + } + + public func isSelected() -> Bool { + return selected + } + + public func setSelected(_ selected: Bool) { + self.selected = selected + } + + public func toggleSelected() { + selected = !selected + } - static func ==(lhs: Self, rhs: Self) -> Bool { + public static func == (lhs: Message, rhs: Message) -> Bool { return lhs.messageId() == rhs.messageId() } + +// +//// public var hashValue: Int { +//// return self.messageId().hashValue +//// } +// +// public static func == (lhs: Message, rhs: Message) -> Bool { +// return lhs.messageId() == rhs.messageId() +// } +// + public func hash(into hasher: inout Hasher) { + print("Ok1111") + hasher.combine(messageId()) + } + } + +//extension Message: Hashable, Equatable { +// +//} diff --git a/ChatK!t/Core/Entities/Thread.swift b/ChatK!t/Core/Entities/Thread.swift index d7b61c24..48854aef 100644 --- a/ChatK!t/Core/Entities/Thread.swift +++ b/ChatK!t/Core/Entities/Thread.swift @@ -13,7 +13,6 @@ public protocol Thread { func threadName() -> String func threadImageUrl() -> URL? func threadUsers() -> [User] - func threadMessages() -> [Message] func threadType() -> ThreadType func threadOtherUser() -> User? diff --git a/ChatK!t/Core/Message/Cells/MessageCell.swift b/ChatK!t/Core/Message/Cells/MessageCell.swift index c998eea6..7e547748 100644 --- a/ChatK!t/Core/Message/Cells/MessageCell.swift +++ b/ChatK!t/Core/Message/Cells/MessageCell.swift @@ -51,7 +51,7 @@ public class MessageCell: UITableViewCell { } } - public func bind(message: Message, model: MessagesModel, selected: Bool = false) { + public func bind(message: Message, model: MessagesModel) { if let content = self.content { content.bind(message: message) } @@ -65,10 +65,10 @@ public class MessageCell: UITableViewCell { if content?.showBubble() ?? true { switch message.messageDirection() { case .incoming: - setBubbleColor(color: model.incomingBubbleColor(selected: selected)) + setBubbleColor(color: model.incomingBubbleColor(selected: message.isSelected())) setBubbleMaskPosition(position: .topLeft) case .outgoing: - setBubbleColor(color: model.outgoingBubbleColor(selected: selected)) + setBubbleColor(color: model.outgoingBubbleColor(selected: message.isSelected())) setBubbleMaskPosition(position: .bottomRight) } } diff --git a/ChatK!t/Core/Message/Content/TextMessageContent.swift b/ChatK!t/Core/Message/Content/TextMessageContent.swift index a6efe764..711dc8e8 100644 --- a/ChatK!t/Core/Message/Content/TextMessageContent.swift +++ b/ChatK!t/Core/Message/Content/TextMessageContent.swift @@ -26,7 +26,6 @@ public class TextMessageContent: DefaultMessageContent { view.clipsToBounds = true view.addSubview(label) view.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(replyView) label.keepLeftInset.equal = ChatKit.config().bubbleInsets.left label.keepBottomInset.equal = ChatKit.config().bubbleInsets.bottom diff --git a/ChatK!t/Core/Message/Model/LazyReloadManager.swift b/ChatK!t/Core/Message/Model/LazyReloadManager.swift new file mode 100644 index 00000000..f0ff226e --- /dev/null +++ b/ChatK!t/Core/Message/Model/LazyReloadManager.swift @@ -0,0 +1,57 @@ +// +// LazyReloadManager.swift +// ChatK!t +// +// Created by ben3 on 04/05/2021. +// + +import Foundation +import RxSwift + +public class LazyReloadManager { + + var _loadingMoreMesssages = false + var _active = false + + weak var _messagesModel: MessagesModel? + let _messageAdder: (([Message]) -> Completable?) + + public init(_ messagesModel: MessagesModel, messageAdder: @escaping ([Message]) -> Completable?) { + _messagesModel = messagesModel + _messageAdder = messageAdder + } + + public func tableViewDidScroll(_ tableView: UITableView) { + loadMessages(tableView) + } + + public func loadMessages(_ tableView: UITableView) { + let offset = tableView.contentOffset.y + tableView.adjustedContentInset.top + if _active && !_loadingMoreMesssages && offset < 10, let model = _messagesModel, let indexPath = tableView.indexPathsForVisibleRows?.first, let message = model.adapter().message(for: indexPath) { + _loadingMoreMesssages = true + _ = model.loadMessages().subscribe(onSuccess: { [weak self] messages in + if !messages.isEmpty { + _ = self?._messageAdder(messages)?.subscribe(onCompleted: { + + if let newIndexPath = model.adapter().indexPath(for: message) { + tableView.scrollToRow(at: newIndexPath, at: .top, animated: false) + } + + self?._loadingMoreMesssages = false + }) + } + }, onFailure: { [weak self] error in + self?._loadingMoreMesssages = false + }) + } + } + + public func scrollViewWillBeginDragging(scrollView: UIScrollView) { + _active = true + } + + public func scrollViewDidEndDecelerating(scrollView: UIScrollView) { + _active = false + } + +} diff --git a/ChatK!t/Core/Message/Model/MessagesListAdapter.swift b/ChatK!t/Core/Message/Model/MessagesListAdapter.swift index fa544fe3..367ed4b2 100644 --- a/ChatK!t/Core/Message/Model/MessagesListAdapter.swift +++ b/ChatK!t/Core/Message/Model/MessagesListAdapter.swift @@ -12,25 +12,6 @@ public class MessagesListAdapter { var _sections = [Section]() var _sectionsIndex = [Date: Section]() - public func message(for indexPath: IndexPath) -> Message? { - if isValid(indexPath) { - return _sections[indexPath.section].message(for: indexPath) - } - return nil - } - - public func messages(for indexPaths: [IndexPath]) -> [Message] { - var messages = [Message]() - - for indexPath in indexPaths { - if let message = message(for: indexPath) { - messages.append(message) - } - } - - return messages - } - public func message(exists message: Message) -> Bool { for section in _sections { if section.exists(message) { @@ -41,7 +22,6 @@ public class MessagesListAdapter { } public func sectionExists(_ section: Section) -> Bool { -// return _sections.contains(section) return _sectionsIndex[section.date()] != nil } @@ -53,17 +33,6 @@ public class MessagesListAdapter { } return nil } - - public func indexPath(for message: Message) -> IndexPath? { - if let s = section(for: message), let section = index(of: s), let row = s.index(for: message) { - return IndexPath(row: row, section: section) - } - return nil - } - - public func isValid(_ indexPath: IndexPath) -> Bool { - indexPath.section < _sections.count - } public func sectionCount() -> Int { _sections.count @@ -76,94 +45,67 @@ public class MessagesListAdapter { return nil } - public func addSection(for message: Message) -> Int? { - if let day = message.messageDate().day(), _sectionsIndex[day] == nil { - let section = Section(date: day) - _sections.append(section) - _sectionsIndex[day] = section - sort() - return _sections.firstIndex(of: section) + public func addSection(for message: Message) -> Section? { + if let day = message.messageDate().day() { + if let section = _sectionsIndex[day] { + return section + } else { + let section = Section(date: day) + _sections.append(section) + _sectionsIndex[day] = section + sort() + return section + } } return nil } + + public func addMessage(_ message: Message) { + if let section = addSection(for: message) { + section.addMessage(message: message) + } + } - public func addSections(for messages: [Message]) -> [Int] { - var sections = [Int]() + public func addMessages(_ messages: [Message]) { for message in messages { - if let section = addSection(for: message) { - sections.append(section) - } + addMessage(message) } - return sections } - - public func indexPaths(for messages: [Message]) -> [IndexPath] { - var indexPaths = [IndexPath]() + + public func addMessages(toStart messages: [Message]) { for message in messages { - if let indexPath = indexPath(for: message) { - indexPaths.append(indexPath) - } + addMessage(toStart: message) } - return indexPaths } - public func addMessage(_ message: Message) -> IndexPath? { - if let section = section(for: message), let index = _sections.firstIndex(of: section), let row = section.addMessage(message: message) { - return IndexPath(row: row, section: index) + public func addMessage(toStart message: Message) { + if let section = addSection(for: message) { + section.addMessage(toStart: message) } - return nil } - - public func addMessages(_ messages: [Message]) -> TableUpdate { - let update = TableUpdate(.add) - update.sections = addSections(for: messages) + + public func addMessages(toEnd messages: [Message]) { for message in messages { - if let indexPath = addMessage(message) { - update.add(indexPath: indexPath) - } + addMessage(toEnd: message) } - return update } - - public func addMessages(toStart messages: [Message]) -> TableUpdate { - let update = TableUpdate(.add) - update.sections = addSections(for: messages) - - for message in messages { - if let indexPath = addMessage(toStart: message) { - update.add(indexPath: indexPath) - } + + public func addMessage(toEnd message: Message) { + if let section = addSection(for: message) { + section.addMessage(toEnd: message) } - return update } - public func addMessage(toStart message: Message) -> IndexPath? { - if let section = section(for: message), let index = _sections.firstIndex(of: section), let row = section.addMessage(toStart: message) { - return IndexPath(row: row, section: index) - } - return nil + public func oldestMessage() -> Message? { + return _sections.first?.messages().first } - public func removeMessages(_ messages: [Message]) -> TableUpdate { - let update = TableUpdate(.remove) - - // First get the index paths of all the messages - var indexPaths = [String: IndexPath]() - - for message in messages { - indexPaths[message.messageId()] = indexPath(for: message) - } - + public func removeMessages(_ messages: [Message]) { for message in messages { - if let section = section(for: message), let i = index(of: section), let _ = section.removeMessage(message), let ip = indexPaths[message.messageId()] { - update.add(indexPath: ip) - if section.isEmpty() { - removeSection(section) - update.add(section: ip.section) - } + if let section = section(for: message) { + section.removeMessage(message) } } - return update } public func removeSection(_ section: Section) { @@ -175,47 +117,6 @@ public class MessagesListAdapter { } } - public func removeMessages2(_ messages: [Message]) -> TableUpdate { - let update = TableUpdate(.remove) - - // First get the index paths of all the messages - var indexPaths = [String: IndexPath]() - - for message in messages { - indexPaths[message.messageId()] = indexPath(for: message) - } - - for message in messages { - if let section = section(for: message), let i = index(of: section), let row = section.removeMessage(message) { - update.add2(indexPath: IndexPath(row: row, section: i)) - if section.isEmpty() { - _sections.remove(at: i) - update.add(section: i) - } - } - } - return update - } - - public func addMessages(toEnd messages: [Message]) -> TableUpdate { - let update = TableUpdate(.add) - update.sections = addSections(for: messages) - - for message in messages { - if let indexPath = addMessage(toEnd: message) { - update.add(indexPath: indexPath) - } - } - return update - } - - public func addMessage(toEnd message: Message) -> IndexPath? { - if let section = section(for: message), let index = _sections.firstIndex(of: section), let row = section.addMessage(toEnd: message) { - return IndexPath(row: row, section: index) - } - return nil - } - public func sort() { _sections.sort { $0.date().compare($1.date()) == .orderedAscending @@ -232,65 +133,21 @@ public class MessagesListAdapter { public func index(of section: Section) -> Int? { return _sections.firstIndex(of: section) } -} - -public class TableUpdate { - - public enum UpdateType { - case add - case remove - case update - } - - var indexPaths = [IndexPath]() - var sections = [Int]() - let type: UpdateType - var allowConcurrent = false - var animation: UITableView.RowAnimation = .none - init(_ type: UpdateType, indexPaths: [IndexPath]? = nil, sections: [Int]? = nil) { - self.type = type - if let indexPaths = indexPaths { - self.indexPaths.append(contentsOf: indexPaths) - } - if let sections = sections { - self.sections.append(contentsOf: sections) - } + public func sections() -> [Section] { + return _sections } - public func add(indexPath: IndexPath) { - if !indexPaths.contains(indexPath) { - indexPaths.append(indexPath) - } - } - - public func add2(indexPath: IndexPath) { - indexPaths.append(indexPath) - } - - public func add(section: Int) { - if !sections.contains(section) { - sections.append(section) + public func indexPath(for message: Message) -> IndexPath? { + if let section = section(for: message), let index = index(of: section), let row = section.index(of: message) { + return IndexPath(row: row, section: index) } + return nil } - public func log() { - print("Type: ", type) - print("sections: ", sections) - print("indexPaths: ", indexPaths) + public func message(for indexPath: IndexPath) -> Message? { + return section(indexPath.section)?.message(for: indexPath) } - public func hasChanges() -> Bool { - return hasRowChanges() || hasSectionChanges() - } - - public func hasSectionChanges() -> Bool { - return !sections.isEmpty - } - - public func hasRowChanges() -> Bool { - return !indexPaths.isEmpty - } - } diff --git a/ChatK!t/Core/Message/Model/MessagesModel.swift b/ChatK!t/Core/Message/Model/MessagesModel.swift index 4d25b66d..68de0736 100644 --- a/ChatK!t/Core/Message/Model/MessagesModel.swift +++ b/ChatK!t/Core/Message/Model/MessagesModel.swift @@ -6,26 +6,38 @@ // import Foundation +import RxSwift -public class MessagesModel: NSObject { +public protocol PMessagesModelDelegate { + // Need to be with the oldest message first + func loadMessages(with oldestMessage: Message?) -> Single<[Message]> + + // Need to be with the oldest message last + func initialMessages() -> [Message] +} + +public class MessagesModel { public let _thread: Thread + public let _delegate: PMessagesModelDelegate public let _messageTimeFormatter = DateFormatter() - - public var _selectedMessages = [Message]() - + public var _onSelectionChange: (([Message]) -> Void)? public var _sectionNib: UINib? = ChatKit.provider().sectionNib() public var _messageCellRegistrations = [String: MessageCellRegistration]() public var _view: PMessagesView? - public var _messageListAdapter = MessagesListAdapter() + public var _adapter = MessagesListAdapter() - public init(_ thread: Thread) { + public init(_ thread: Thread, delegate: PMessagesModelDelegate) { _thread = thread + _delegate = delegate _messageTimeFormatter.setLocalizedDateFormatFromTemplate(ChatKit.config().timeFormat) - super.init() + } + + public func delegate() -> PMessagesModelDelegate { + _delegate } public func messageTimeFormatter() -> DateFormatter { @@ -41,6 +53,10 @@ public class MessagesModel: NSObject { registerMessageCell(registration: registration) } } + + public func cellRegistration(_ messageType: String) -> MessageCellRegistration? { + return _messageCellRegistrations[messageType] + } public func messageCellRegistrations() -> [MessageCellRegistration] { return _messageCellRegistrations.map { $1 } @@ -55,7 +71,7 @@ public class MessagesModel: NSObject { } public func estimatedRowHeight() -> CGFloat { - return 60 + return 69 } public func avatarSize() -> CGFloat { @@ -90,135 +106,94 @@ public class MessagesModel: NSObject { _onSelectionChange?(selectedMessages()) } - public func setSelectionChangeListener(_ listener: @escaping (([Message]) -> Void)) { - _onSelectionChange = listener - } - - public func selectedIndexPaths() -> [IndexPath] { - return _messageListAdapter.indexPaths(for: _selectedMessages) - } - - public func isSelected(_ message: Message) -> Bool { - return _selectedMessages.contains { - return $0.messageId() == message.messageId() - } - } - - public func isSelected(_ indexPath: IndexPath) -> Bool { - if let message = _messageListAdapter.message(for: indexPath) { - return isSelected(message) - } - return false - } - - public func select(indexPaths: [IndexPath], updateView: Bool = false) { - select(messages: _messageListAdapter.messages(for: indexPaths), updateView: updateView) - } - - public func select(messages: [Message], updateView: Bool = false) { - for message in messages { - if !isSelected(message) { - _selectedMessages.append(message) - } - } + public func toggleSelection(_ message: Message) { + message.toggleSelected() notifySelectionChanged() - - // Get the index paths - if(updateView) { - _view?.updateTable(TableUpdate(.update, indexPaths: _messageListAdapter.indexPaths(for: messages)), completion: nil) - } } - public func deselect(indexPaths: [IndexPath], updateView: Bool = false) { - deselect(messages: _messageListAdapter.messages(for: indexPaths), updateView: updateView) - } - - public func deselect(messages: [Message], updateView: Bool = false) { - for message in messages { - if let index = _selectedMessages.firstIndex(where: { $0.messageId() == message.messageId() }) { - _selectedMessages.remove(at: index) - } - } - notifySelectionChanged() - if(updateView) { - _view?.updateTable(TableUpdate(.update, indexPaths: _messageListAdapter.indexPaths(for: messages)), completion: nil) - } + public func setSelectionChangeListener(_ listener: @escaping (([Message]) -> Void)) { + _onSelectionChange = listener } public func section(for index: Int) -> Section? { - return _messageListAdapter.section(index) + return _adapter.section(index) } public func setView(_ view: PMessagesView) { _view = view } - public func loadMessages() { - let messages = _thread.threadMessages() - addMessages(toEnd: messages, updateView: false, completion: nil) - _view?.reloadData() - _view?.scrollToBottom(animated: false, force: true) + public func loadInitialMessages() { + let messages = _delegate.initialMessages() + _ = addMessages(toEnd: messages, updateView: true, animated: false)?.subscribe(onCompleted: { [weak self] in + self?._view?.scrollToBottom(animated: false, force: true) + }) } public func messageExists(_ message: Message) -> Bool { - _messageListAdapter.message(exists: message) + _adapter.message(exists: message) } public func message(for id: String) -> Message? { - _messageListAdapter.message(for: id) + _adapter.message(for: id) } -} - -extension MessagesModel: UITableViewDataSource { - public func numberOfSections(in tableView: UITableView) -> Int { - return _messageListAdapter.sectionCount() + public func adapter() -> MessagesListAdapter { + _adapter } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return _messageListAdapter.section(section)?.messageCount() ?? 0 - } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if let message = _messageListAdapter.message(for: indexPath) { - var cell: MessageCell? - // Get the registration so we know which cell identifier to use - if let registration = _messageCellRegistrations[message.messageType()] { - let identifier = registration.identifier(direction: message.messageDirection()) - - cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? MessageCell - cell?.setContent(content: registration.content(direction: message.messageDirection())) - cell?.bind(message: message, model: self, selected: isSelected(message)) - return cell! - } - } - return UITableViewCell() - } } extension MessagesModel { - public func addMessage(toStart message: Message, updateView: Bool = true) { - addMessages(toStart: [message], updateView: updateView) + public func addMessage(toStart message: Message, updateView: Bool = true, animated: Bool = true) -> Completable? { + _adapter.addMessage(toStart: message) + if updateView { + return synchronize(animated) + } + return nil } - public func addMessage(toEnd message: Message, updateView: Bool = true) { - addMessages(toEnd: [message], updateView: updateView, completion: nil) - _view?.scrollToBottom(animated: true, force: message.messageSender().userIsMe()) + public func addMessage(toEnd message: Message, updateView: Bool = true, animated: Bool = true, scrollToBottom: Bool = false) -> Completable? { + _adapter.addMessage(toEnd: message) + if updateView { + return synchronize(animated)?.do(onCompleted: { [weak self] in + if scrollToBottom { + self?._view?.scrollToBottom(animated: true, force: message.messageSender().userIsMe()) + } + }) + } + return nil } - public func addMessages(toStart messages: [Message], updateView: Bool = true) { - let update = _messageListAdapter.addMessages(toStart: messages) + public func addMessages(toStart messages: [Message], updateView: Bool = true, animated: Bool = true) -> Completable? { + _adapter.addMessages(toStart: messages) if updateView { - _view?.updateTable(update, completion: nil) + return synchronize(animated) } + return nil } - public func addMessages(toEnd messages: [Message], updateView: Bool = true, completion: ((Bool) -> Void)?) { - let update = _messageListAdapter.addMessages(toEnd: messages) + public func addMessages(toEnd messages: [Message], updateView: Bool = true, animated: Bool = true) -> Completable? { + _adapter.addMessages(toEnd: messages) if updateView { - _view?.updateTable(update, completion: completion) + return synchronize(animated) } + return nil + } + + public func synchronize(_ animated: Bool) -> Completable? { + var snapshot = NSDiffableDataSourceSnapshot() + + snapshot.appendSections(_adapter.sections()) + for section in _adapter._sections { + snapshot.appendItems(section.messages(), toSection: section) + } + return _view?.apply(snapshot: snapshot, animated: animated) + } + + public func loadMessages() -> Single<[Message]> { + return _delegate.loadMessages(with: _adapter.oldestMessage()) } // public func nextItem(_ message: Message) -> NSObject? { @@ -240,39 +215,52 @@ extension MessagesModel { // } - public func removeMessage(_ message: Message, completion: ((Bool) -> Void)?) { - removeMessages([message], completion: completion) + public func removeMessage(_ message: Message, animated: Bool = true) -> Completable? { + _adapter.removeMessages([message]) + notifySelectionChanged() + return synchronize(animated) } - public func removeMessages(_ messages: [Message], completion: ((Bool) -> Void)?) { - let update = _messageListAdapter.removeMessages(messages) - update.allowConcurrent = true - deselect(messages: messages) - _view?.updateTable(update, completion: completion) + public func removeMessages(_ messages: [Message], animated: Bool = true) -> Completable? { + _adapter.removeMessages(messages) + notifySelectionChanged() + return synchronize(animated) } - public func updateMessage(_ message: Message) { - if let indexPath = _messageListAdapter.indexPath(for: message) { - let update = TableUpdate(.update) - update.add(indexPath: indexPath) - _view?.updateTable(update, completion: nil) + public func updateMessage(id: String, animated: Bool = false) -> Completable? { + // Get the messages + if let message = adapter().message(for: id) { + return _view?.reload(messages: [message], animated: animated) } + return nil } + } extension MessagesModel: ChatToolbarDelegate { - public func clearSelection(_ updateView: Bool?) { - let update = TableUpdate(.update, indexPaths: selectedIndexPaths()) - _selectedMessages.removeAll() + public func clearSelection(_ updateView: Bool?, animated: Bool) { + let selected = selectedMessages() + for message in selected { + message.setSelected(false) + } notifySelectionChanged() - if(updateView ?? false) { - _view?.updateTable(update, completion: nil) + if updateView ?? false { + _ = _view?.reload(messages: selected, animated: animated).subscribe() } + } public func selectedMessages() -> [Message] { - _selectedMessages + var messages = [Message]() + for section in _adapter.sections() { + for message in section.messages() { + if message.isSelected() { + messages.append(message) + } + } + } + return messages } } diff --git a/ChatK!t/Core/Message/Model/Section.swift b/ChatK!t/Core/Message/Model/Section.swift index 0ee3fd9e..668a9f49 100644 --- a/ChatK!t/Core/Message/Model/Section.swift +++ b/ChatK!t/Core/Message/Model/Section.swift @@ -7,7 +7,7 @@ import Foundation -public class Section: NSObject { +public class Section { let _date: Date @@ -43,54 +43,44 @@ public class Section: NSObject { _messagesIndex[message.messageId()] != nil } - public func addMessage(toIndex message: Message) { + public func addToIndex(_ message: Message) { _messagesIndex[message.messageId()] = message } - public func addMessage(toStart message: Message) -> Int? { + public func addMessage(toStart message: Message) { if !exists(message) { _messages.insert(message, at: 0) - addMessage(toIndex: message) - return _messages.count - 1 + addToIndex(message) } - return nil } - public func removeMessage(_ message: Message) -> Int? { - if let i = index(for: message) { + public func removeMessage(_ message: Message) { + if let i = index(of: message) { _messages.remove(at: i) - return i } - return nil } - public func index(for message: Message) -> Int? { - return _messages.firstIndex { - $0.messageId() == message.messageId() - } + public func index(of message: Message) -> Int? { + return _messages.firstIndex(of: message) } - public func addMessage(toEnd message: Message) -> Int? { + public func addMessage(toEnd message: Message) { if !exists(message) { _messages.append(message) - addMessage(toIndex: message) - return _messages.count - 1 + addToIndex(message) } - return nil } public func isEmpty() -> Bool { return _messages.isEmpty } - public func addMessage(message: Message) -> Int? { + public func addMessage(message: Message) { if !exists(message) { _messages.append(message) - addMessage(toIndex: message) + addToIndex(message) sort() - return index(for: message) } - return nil } public func sort() { @@ -106,7 +96,21 @@ public class Section: NSObject { public func messageCount() -> Int { return _messages.count } + + public func messages() -> [Message] { + return _messages + } + +} +extension Section: Hashable { + public static func == (lhs: Section, rhs: Section) -> Bool { + return lhs.date() == rhs.date() + } + public func hash(into hasher: inout Hasher) { + hasher.combine(date().hashValue) + } + } diff --git a/ChatK!t/Core/Message/View/MessagesView.swift b/ChatK!t/Core/Message/View/MessagesView.swift index 30e557a4..74f30c09 100644 --- a/ChatK!t/Core/Message/View/MessagesView.swift +++ b/ChatK!t/Core/Message/View/MessagesView.swift @@ -8,22 +8,34 @@ import Foundation import UIKit import KeepLayout +import RxSwift public class MessagesView: UIView { + public enum RefreshState { + case none + case willLoad + case loading + case loaded + } + public var _tableView = UITableView() public var _model: MessagesModel? + public var _lazyReloadManager: LazyReloadManager? public var _longPressRecognizer: UIGestureRecognizer? public var _tapRecognizer: UIGestureRecognizer? + public var _refreshControl: UIRefreshControl? + + public var _refreshState: RefreshState = .none + public var _loadedMessages: [Message]? public var _hideKeyboardListener: (() -> Void)? // This is used to preseve the Y position when we change the table insets public var _bottomYClearance: CGFloat = 0 - public var tableUpdateQueue = [TableUpdate]() - public var tableUpdating = false + public var datasource: UITableViewDiffableDataSource? public override init(frame: CGRect) { super.init(frame: frame) @@ -58,19 +70,66 @@ public class MessagesView: UIView { _tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap(_:))) _tableView.addGestureRecognizer(_tapRecognizer!) + + _refreshControl = UIRefreshControl() + _refreshControl?.addTarget(self, action: #selector(startLoading), for: .valueChanged) + _tableView.addSubview(_refreshControl!) + } + @objc public func startLoading() { + _refreshState = .willLoad + _tableView.isUserInteractionEnabled = false + } + + @objc public func loadMoreMessages() { + _refreshState = .loading + _ = _model?.loadMessages().subscribe(onSuccess: { [weak self] messages in + self?._loadedMessages = messages + self?._refreshState = .loaded + self?._refreshControl?.endRefreshing() + }, onFailure: { [weak self] error in + self?._tableView.isUserInteractionEnabled = true + self?._refreshState = .none + self?._refreshControl?.endRefreshing() + }) + } + + public func addLoadedMessages() { + _tableView.isUserInteractionEnabled = true + _refreshState = .none + if let messages = _loadedMessages, !messages.isEmpty { + if let indexPath = _tableView.indexPathsForVisibleRows?.first, let message = _model?.adapter().message(for: indexPath) { + _ = _model?.addMessages(toStart: messages, animated: false)?.subscribe(onCompleted: { [weak self] in + if let newIndexPath = self?._model?.adapter().indexPath(for: message) { + self?._tableView.scrollToRow(at: newIndexPath, at: .top, animated: false) + } + }) + } + } + } + public func setModel(model: MessagesModel) { assert(_model == nil, "The model can't be set more than once") _model = model model.setView(self) + +// _lazyReloadManager = LazyReloadManager(model, messageAdder: { messages -> Completable? in +// return model.addMessages(toStart: messages, updateView: true, animated: false) +// }) - _tableView.dataSource = model - _tableView.estimatedRowHeight = model.estimatedRowHeight() - _tableView.estimatedSectionHeaderHeight = 20 + datasource = UITableViewDiffableDataSource(tableView: _tableView) { tableView, indexPath, message in + let registration = model.cellRegistration(message.messageType())! + let identifier = registration.identifier(direction: message.messageDirection()) + let cell = tableView.dequeueReusableCell(withIdentifier: identifier) as! MessageCell + cell.setContent(content: registration.content(direction: message.messageDirection())) + cell.bind(message: message, model: model) + return cell + } + datasource?.defaultRowAnimation = .fade for registration in model.messageCellRegistrations() { _tableView.register(registration.nib(direction: .incoming), forCellReuseIdentifier: registration.identifier(direction: .incoming)) @@ -78,6 +137,10 @@ public class MessagesView: UIView { } _tableView.register(model.sectionNib(), forCellReuseIdentifier: model.sectionIdentifier()) + _tableView.dataSource = datasource! + _tableView.estimatedRowHeight = model.estimatedRowHeight() + _tableView.estimatedSectionHeaderHeight = ChatKit.config().messagesViewSectionHeight + } public func willSetBottomInset() { @@ -88,6 +151,7 @@ public class MessagesView: UIView { } public func setBottomInset(height: CGFloat) { + var insets = _tableView.contentInset insets.bottom = height _tableView.contentInset = insets @@ -96,23 +160,21 @@ public class MessagesView: UIView { let tableViewHeight = _tableView.frame.height - _tableView.contentInset.bottom - _tableView.contentInset.top let contentHeight = _tableView.contentSize.height + print("Height: \(contentHeight), Y clearance: \(_bottomYClearance), tableViewHeight: \(tableViewHeight)") + // Apply the bottom clearance _tableView.contentOffset.y = contentHeight - _bottomYClearance - tableViewHeight } - - public func popTableUpdateQueue() { - if let update = tableUpdateQueue.first { - tableUpdateQueue.remove(at: 0) - updateTable(update) - } - } - + @objc public func onLongPress(_ sender: UILongPressGestureRecognizer) { if ChatKit.config().messageSelectionEnabled && !isSelectionModeEnabled() { let point = sender.location(in: _tableView) - if let path = _tableView.indexPathForRow(at: point) { - _model?.select(indexPaths: [path], updateView: true) + if let indexPath = _tableView.indexPathForRow(at: point) { + if let message = datasource?.itemIdentifier(for: indexPath) { + _model?.toggleSelection(message) + _ = reload(messages: [message], animated: true).subscribe() + } } } } @@ -120,24 +182,19 @@ public class MessagesView: UIView { @objc public func onTap(_ sender: UITapGestureRecognizer) { if ChatKit.config().messageSelectionEnabled && isSelectionModeEnabled() { let point = sender.location(in: _tableView) - if let path = _tableView.indexPathForRow(at: point) { - if isSelected(path) { - _model?.deselect(indexPaths: [path], updateView: true) - } else { - _model?.select(indexPaths: [path], updateView: true) + if let indexPath = _tableView.indexPathForRow(at: point) { + if let message = datasource?.itemIdentifier(for: indexPath) { + _model?.toggleSelection(message) + _ = reload(messages: [message], animated: true).subscribe() } } } else { _hideKeyboardListener?() } } - - public func isSelected(_ path: IndexPath) -> Bool { - return _model?.isSelected(path) ?? false - } public func isSelectionModeEnabled() -> Bool { - return !(_model?.selectedIndexPaths().isEmpty ?? false) + return !(_model?.selectedMessages().isEmpty ?? false) } } @@ -149,6 +206,11 @@ extension MessagesView: UITableViewDelegate { } return nil } + + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return ChatKit.config().messagesViewSectionHeight + } + } extension MessagesView: PMessagesView { @@ -168,43 +230,53 @@ extension MessagesView: PMessagesView { } } } - - public func updateTable(_ update: TableUpdate, completion: ((Bool) -> Void)? = nil) { - if !update.hasChanges() { - return + + public func apply(snapshot: NSDiffableDataSourceSnapshot, animated: Bool) -> Completable { + return Completable.create { [weak self] completable in + DispatchQueue.main.async { + self?.datasource?.apply(snapshot, animatingDifferences: animated, completion: { + completable(.completed) + }) + } + return Disposables.create {} } - update.log() - - if tableUpdating && !update.allowConcurrent { - tableUpdateQueue.append(update) - } else { - tableUpdating = true - _tableView.performBatchUpdates({ [weak self] in - if update.type == .add { - self?._tableView.insertSections(IndexSet(update.sections), with: update.animation) - self?._tableView.insertRows(at: update.indexPaths, with: update.animation) - } - if update.type == .remove { - if !update.sections.isEmpty { - self?._tableView.deleteSections(IndexSet(update.sections), with: update.animation) - } else { - self?._tableView.deleteRows(at: update.indexPaths, with: update.animation) - } - } - if update.type == .update { - self?._tableView.reloadSections(IndexSet(update.sections), with: update.animation) - self?._tableView.reloadRows(at: update.indexPaths, with: update.animation) + } + + public func reload(messages: [Message], animated: Bool) -> Completable { + return Completable.create { [weak self] completable in + DispatchQueue.main.async { + if var snapshot = self?.datasource?.snapshot() { + snapshot.reloadItems(messages) + self?.datasource?.apply(snapshot, animatingDifferences: animated, completion: { + completable(.completed) + }) } - }, completion: { [weak self] success in - self?.tableUpdating = false - self?.popTableUpdateQueue() - completion?(success) - }) + } + return Disposables.create {} } } +} + +extension MessagesView: UIScrollViewDelegate { - public func reloadData() { - _tableView.reloadData() + public func scrollViewDidScroll(_ scrollView: UIScrollView) { +// _lazyReloadManager?.tableViewDidScroll(_tableView) + + let offset = scrollView.contentOffset.y + scrollView.adjustedContentInset.top + if abs(offset) < 5 && offset != 0 && _refreshState == .willLoad { + loadMoreMessages() + } + if abs(offset) < 1 && offset != 0 && _refreshState == .loaded { + addLoadedMessages() + } } +// public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { +// _lazyReloadManager?.scrollViewWillBeginDragging(scrollView: scrollView) +// } +// +// public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { +// _lazyReloadManager?.scrollViewDidEndDecelerating(scrollView: scrollView) +// } + } diff --git a/ChatK!t/Core/Message/View/PMessagesView.swift b/ChatK!t/Core/Message/View/PMessagesView.swift index d8b92edf..ef2e4346 100644 --- a/ChatK!t/Core/Message/View/PMessagesView.swift +++ b/ChatK!t/Core/Message/View/PMessagesView.swift @@ -6,11 +6,10 @@ // import Foundation +import RxSwift public protocol PMessagesView { - func scrollToBottom(animated: Bool, force: Bool) - func updateTable(_ update: TableUpdate, completion: ((Bool) -> Void)?) - func reloadData() - + func apply(snapshot: NSDiffableDataSourceSnapshot, animated: Bool) -> Completable + func reload(messages: [Message], animated: Bool) -> Completable } diff --git a/ChatK!t/Core/Toolbar/ChatToolbar.swift b/ChatK!t/Core/Toolbar/ChatToolbar.swift index 918d2dd0..51cd6ca2 100644 --- a/ChatK!t/Core/Toolbar/ChatToolbar.swift +++ b/ChatK!t/Core/Toolbar/ChatToolbar.swift @@ -9,7 +9,7 @@ import Foundation public protocol ChatToolbarDelegate { func selectedMessages() -> [Message] - func clearSelection(_ updateView: Bool?) + func clearSelection(_ updateView: Bool?, animated: Bool) } public protocol ChatToolbarActionsDelegate { @@ -56,7 +56,7 @@ public class ChatToolbar: UIToolbar { action.implOnClick = { if action.notify(delegate.selectedMessages()) { - delegate.clearSelection(true) + delegate.clearSelection(true, animated: true) } }