diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c8c6681a09992a4f440c342d56c619a074eee002..16f197af088f1b00c2527ce6dfd2727490b6f31f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -790,6 +790,8 @@ 853FB0702049B396000996C5 /* SupportItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB06F2049B396000996C5 /* SupportItemsFactory.swift */; }; 853FB0752049B4FF000996C5 /* TextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB0742049B4FF000996C5 /* TextTableViewCell.swift */; }; 853FB0772049B7CA000996C5 /* TextCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB0762049B7CA000996C5 /* TextCellViewModel.swift */; }; + 8540A331211B34B4007F65AF /* MessageCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */; }; + 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; @@ -809,6 +811,7 @@ 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; 854A4B312080D6C400759152 /* CellWithImageCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2F2080D6C400759152 /* CellWithImageCellModel.swift */; }; 854CFB08210704AE00FBC133 /* CGRectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */; }; + 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */; }; 854FC1CB204468FC00B12BE5 /* CarouselFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */; }; 8557987F2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557987D2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift */; }; 855798802093200D007050B8 /* StickerMenuActionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557987E2093200D007050B8 /* StickerMenuActionCellModel.swift */; }; @@ -2773,6 +2776,8 @@ 853FB06F2049B396000996C5 /* SupportItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportItemsFactory.swift; sourceTree = ""; }; 853FB0742049B4FF000996C5 /* TextTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextTableViewCell.swift; sourceTree = ""; }; 853FB0762049B7CA000996C5 /* TextCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCellViewModel.swift; sourceTree = ""; }; + 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDataSource.swift; sourceTree = ""; }; + 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; @@ -2792,6 +2797,7 @@ 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; 854A4B2F2080D6C400759152 /* CellWithImageCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageCellModel.swift; sourceTree = ""; }; 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectExtensions.swift; sourceTree = ""; }; + 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewLayout.swift; sourceTree = ""; }; 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselFlowLayout.swift; sourceTree = ""; }; 8557987D2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerMenuActionCollectionViewCell.swift; sourceTree = ""; }; 8557987E2093200D007050B8 /* StickerMenuActionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerMenuActionCellModel.swift; sourceTree = ""; }; @@ -7427,6 +7433,16 @@ path = Image; sourceTree = ""; }; + 854D13D6211B2E6200E139FC /* CollectionView */ = { + isa = PBXGroup; + children = ( + 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */, + 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */, + 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */, + ); + path = CollectionView; + sourceTree = ""; + }; 8557987C20931FE8007050B8 /* Menu */ = { isa = PBXGroup; children = ( @@ -9234,6 +9250,7 @@ A45F10C820B4218D00F45004 /* Views */ = { isa = PBXGroup; children = ( + 854D13D6211B2E6200E139FC /* CollectionView */, 5BC1D37E20D3B54B002A44B3 /* CallInfoView */, A45F10C920B4218D00F45004 /* TableView */, A45F10F420B4218D00F45004 /* ReplyPreview */, @@ -12559,7 +12576,7 @@ "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", @@ -12586,7 +12603,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", @@ -13375,6 +13392,7 @@ FBCE83D520E52397003B7558 /* MQTTServiceWallet.swift in Sources */, 853D55B820CE72C60080659F /* StickerContentDataSource.swift in Sources */, 85CB25DC20D723D300D5E565 /* StickerPackDAOProtocol.swift in Sources */, + 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */, 4B06D3152028A537003B275B /* ContactsItemsFactory.swift in Sources */, A48C154420EF7A15002DA994 /* LinkHandler.swift in Sources */, A45F113320B4218D00F45004 /* MessageCellFactory.swift in Sources */, @@ -14333,6 +14351,7 @@ 54FFFD58388E2B660C1E5A05 /* MapPresenter.swift in Sources */, A42D52D6206A53AB00EEB952 /* serviceTask_Spec.swift in Sources */, 0AB08BA89A51118248FA3233 /* MapInteractor.swift in Sources */, + 8540A331211B34B4007F65AF /* MessageCollectionViewDataSource.swift in Sources */, A48C154220EF76EE002DA994 /* LinkExtension.swift in Sources */, A42D52AD206A53AA00EEB952 /* log_Spec.swift in Sources */, F6150A15F8A3E399EEB2C724 /* MapWireframe.swift in Sources */, @@ -14508,6 +14527,7 @@ 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */, A4569873060C49904EF8C555 /* EditGroupPhotoViewController.swift in Sources */, 8502DB502061030000613C8C /* WheelPositionPickerWireFrame.swift in Sources */, + 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */, 0062D9452062EC4100B915AC /* InviteFriendsDS.swift in Sources */, F117872620ACF2DB007A9A1B /* VideoQuality.swift in Sources */, FBDA34E920921079009F4FB6 /* KeyboardLayoutGuide.swift in Sources */, diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 4ae1ebb15b659f5ca3c79688876178ba6ba40499..4e8660a49d7c68b760f452e6725a9fde7914299d 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -263,10 +263,9 @@ protocol MessageViewProtocol: class { func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel?, targetView: UIView?) - func scrollToBottomIfNeeded() func scrollToMessage(with localId: String?) - func scrollToUnreadMessage(with index: Int) func scrollToMessage(serverId: Int64) + func scrollToBottomIfNeeded() func scrollToBottom() func updateHeaderStatus(_ status: String) diff --git a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift index 78ff22af8b59185932c27fc75917fdb76704eb69..afe12aea7de80dd323beb5b4f33d47537f202bf0 100644 --- a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift +++ b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift @@ -12,11 +12,11 @@ extension MessageVC: BaseChatCellDelegate, AudioManagerDelegate, ProximitySensor // MARK: - BaseChatCellDelegate func didCellTapped(_ cell: BaseChatCell) { - guard let model = cell.model else { + guard let model = cell.model, let type = model.type else { return } - - switch model.type! { + + switch type { case .image: if let url = model.fileUrl { let contentImageView = cell.transitionImageView diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 9d8d3312b4cf9277f66cc9bf8099d11f4eef000c..ec993888cd875d79a46b04416795c8d04be44420 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -32,8 +32,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw var isHideKeyboard = true var verticalOffset: CGFloat = 0.0 - var messageDS: MessageDS! - var messageTVDelegate: MessageTableViewDelegate! + var messageDS: MessageCollectionViewDataSource! + var messageTVDelegate: MessageCollectionViewDelegate! var contextMenuPresented = false @@ -67,6 +67,12 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw static let convertionModel = "convertionModel" static let starId = "starID" } + + private let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY-MM-dd" + return dateFormatter + }() // MARK: - Views private var bottomInset = Constraints.replyPreview.bottomInset @@ -94,35 +100,63 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw make.left.right.equalToSuperview() } } - - private(set) lazy var tableView: UITableView = { - let tv = UITableView() - - tv.separatorStyle = .none - tv.backgroundColor = UIColor.clear - - registerCells(for: tv) - - tv.estimatedRowHeight = 0 // NOTE: It is important for scrolling behavior. - tv.estimatedSectionHeaderHeight = 0 + + private let collectionViewLayout: UICollectionViewFlowLayout = { + let layout = MessageCollectionViewLayout() + return layout + }() + + private(set) lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.backgroundColor = .clear + collectionView.alwaysBounceVertical = true - - tv.contentInset = UIEdgeInsets( + collectionView.contentInset = UIEdgeInsets( top: Constraints.tableView.defaultVerticalInset, left: 0, bottom: Constraints.tableView.bottomInset, right: 0 ) - - self.view.addSubview(tv) - tv.snp.makeConstraints { make in + + view.addSubview(collectionView) + collectionView.snp.makeConstraints { make in makeDefaultTableConstraint(make) make.bottom.equalTo(staticFooterView.snp.top) } - - return tv + + registerCells(for: collectionView) + + return collectionView }() +// private(set) lazy var tableView: UITableView = { +// let tv = UITableView() +// +// tv.separatorStyle = .none +// tv.backgroundColor = UIColor.clear +// +// registerCells(for: tv) +// +// tv.estimatedRowHeight = 0 // NOTE: It is important for scrolling behavior. +// tv.estimatedSectionHeaderHeight = 0 +// +// +// tv.contentInset = UIEdgeInsets( +// top: Constraints.tableView.defaultVerticalInset, +// left: 0, +// bottom: Constraints.tableView.bottomInset, +// right: 0 +// ) +// +// self.view.addSubview(tv) +// tv.snp.makeConstraints { make in +// makeDefaultTableConstraint(make) +// make.bottom.equalTo(staticFooterView.snp.top) +// } +// +// return tv +// }() + private(set) lazy var inputBar: InputBar = { let inputBar = InputBar() inputBar.displayMode = .new @@ -167,8 +201,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw inputBar.changesHeightHandler = { [weak self] diff in guard let `self` = self else { return } - self.tableView.contentInset.top -= diff - self.tableView.contentOffset.y += diff + self.collectionView.contentInset.top -= diff + self.collectionView.contentOffset.y += diff } inputBar.inputTypeChangeHandler = { [weak self] inputType in self?.stickerInputState.performUpdates { state in @@ -229,7 +263,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw view.snp.makeConstraints({ (make) in make.height.equalTo(gradientHeight) make.left.right.equalToSuperview() - make.bottom.equalTo(tableView.snp.bottom) + make.bottom.equalTo(collectionView.snp.bottom) }) return view @@ -294,7 +328,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw view.addSubview(searchView) searchView.snp.makeConstraints { maker in maker.left.right.equalToSuperview() - maker.bottom.equalTo(tableView.snp.bottom) + maker.bottom.equalTo(collectionView.snp.bottom) } searchView.setupContentInset(to: gradientView) @@ -346,7 +380,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw view.addSubview(mentionPanelView) mentionPanelView.snp.makeConstraints { maker in - maker.edges.equalTo(tableView) + maker.edges.equalTo(collectionView) } return mentionPanelView @@ -381,11 +415,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw audioManager.delegate = self proximityManager.delegate = self - messageDS = MessageDS(view: self) - tableView.dataSource = messageDS + messageDS = MessageCollectionViewDataSource(view: self) + collectionView.dataSource = messageDS - messageTVDelegate = MessageTableViewDelegate(view: self) - tableView.delegate = messageTVDelegate + messageTVDelegate = MessageCollectionViewDelegate(view: self) + collectionView.delegate = messageTVDelegate replyPreview.delegate = self gradientView.isHidden = false @@ -424,7 +458,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw NotificationCenter.default.addObserver(self, selector: #selector(self.willResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) - tableView.layoutIfNeeded() + collectionView.layoutIfNeeded() presenter.viewDidAppear() } @@ -459,7 +493,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - let expandedHeight = self.tableView.bounds.height + CGFloat(FooterViewLayout.collapsedHeight) + let expandedHeight = self.collectionView.bounds.height + CGFloat(FooterViewLayout.collapsedHeight) footerView?.update(constraintLayout: CollapsedView.ConstraintLayout(availableHeight: [CGFloat(FooterViewLayout.collapsedHeight), CGFloat(FooterViewLayout.middleHeight), @@ -480,14 +514,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } override func keyboardNotified(endFrame: CGRect) { - let currentTableBounds = tableView.bounds - if endFrame.origin.y >= UIScreen.main.bounds.size.height { if !isHideKeyboard { - let newTableBounds = currentTableBounds.updating(height: currentTableBounds.height + verticalOffset) - adjustTableTopInset(for: newTableBounds) - - tableView.contentOffset = CGPoint(x: 0, y: tableView.contentOffset.y - verticalOffset) + collectionView.contentOffset = CGPoint(x: 0, y: collectionView.contentOffset.y - verticalOffset) verticalOffset = 0 } isHideKeyboard = true @@ -496,10 +525,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } else { if isHideKeyboard { verticalOffset = CGFloat(endFrame.height) - safeAreaBottomInset() - let newTableBounds = currentTableBounds.updating(height: currentTableBounds.height - verticalOffset) - adjustTableTopInset(for: newTableBounds) - - tableView.contentOffset = CGPoint(x: 0, y: tableView.contentOffset.y + verticalOffset) + collectionView.contentOffset = CGPoint(x: 0, y: collectionView.contentOffset.y + verticalOffset) } isHideKeyboard = false updateToShow(endFrame: endFrame) @@ -670,9 +696,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw // MARK: - Mentions func setupUnreadMentions(after index: Int) { - var serverId = messageDS.cells[index].serverID + var serverId = messageDS.cellModel(at: index).serverID - if serverId == nil, let previousModel = messageDS.cells[.. CGPoint { - return CGPoint(x: tableView.bounds.width / 2, - y: tableView.contentOffset.y + tableView.bounds.height - tableView.contentInset.bottom) - } - func lastVisibleCellIndex() -> Int? { - let point = lastMessagePosition() - return tableView.indexPathForRow(at: point)?.row + let offset = messageDS.isReversed + ? -collectionView.contentInset.top + : collectionView.bounds.height - collectionView.contentInset.bottom + + let lastMessagePosition = CGPoint( + x: collectionView.bounds.width / 2, + y: collectionView.contentOffset.y + offset + ) + return collectionView.indexPathForItem(at: lastMessagePosition)?.item } @@ -823,10 +851,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateData(with configuration: DisplayChatConfiguration) { - let isLastMessageVisible = self.isLastMessageVisible - - messageDS.cells = configuration.cells - tableView.reloadData() + messageDS.update(configuration.cells) + collectionView.reloadData() adjustTableTopInset() hasUnread = configuration.isUnreadShown @@ -836,13 +862,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw updateUnreadMentions() } - func updateNewData(_ cells: [BaseChatCellModel]) { - var result = cells - + func updateNewData(_ history: [BaseChatCellModel]) { var lastTimeShowed: Date? - for i in (0.. cells.count { - let indexPath = IndexPath(row: cells.count, section: 0) - let rect = tableView.rectForRow(at: indexPath) - tableView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell + if !messageDS.isReversed, messageDS.count > history.count { + let indexPath = IndexPath(row: history.count, section: 0) + guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { + return + } + let rect = attributes.frame + collectionView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell } loadingStatus = false @@ -944,39 +966,38 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func newMessage(_ cell: BaseChatCellModel) { - let isLastMessageVisible = self.isLastMessageVisible if let messageDS = self.messageDS { var unreadCellIndex: Int? if cell.isOwner { if hasUnread { hasUnread = false - if let index = messageDS.cells.index(where: { $0.unread == true }) { + if let index = messageDS.index(where: { $0.unread == true }) { unreadCellIndex = index } } } - + readMessage(cell) - tableView.performUpdates(animated: false) { + collectionView.performBatchUpdates({ if let unrIndex = unreadCellIndex { - messageDS.cells.remove(at: unrIndex) - tableView.deleteRows(at: [IndexPath(row: unrIndex, section: 0)], with: .none) + messageDS.remove(at: unrIndex) + collectionView.deleteItems(at: [IndexPath(row: unrIndex, section: 0)]) } - - messageDS.cells.append(cell) - tableView.insertRows(at: [IndexPath(row: messageDS.cells.count - 1, section: 0)], with: .none) - } + messageDS.addNewMessage(cell) + collectionView.insertItems(at: [IndexPath(row: messageDS.bottomIndex, section: 0)]) + + }, completion: { _ in + if self.isLastMessageVisible || cell.isOwner { + dispatchAsyncMainAfter(0.01) { + self.scrollToBottom(animated: false) + } + } else { + self.buttonState = .newMessages + } + }) adjustTableTopInset() - - if isLastMessageVisible || cell.isOwner { - dispatchAsyncMainAfter(0.01, block: { - self.scrollToBottom(animated: false) - }) - } else { - buttonState = .newMessages - } } } @@ -992,30 +1013,36 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func updateTranslationProgress(progressModel: ConvertionProgressModel?) { DispatchQueue.main.async { let id = progressModel?.id - let list = self.messageDS.cells.filter { $0.id == id } + let list = self.messageDS.filter { $0.id == id } list.forEach { $0.translationProgressModel = progressModel } self.reloadIfVisible(models: list) } } func updateMessage(_ model: BaseChatCellModel) { - if let messageId = model.id, let index = messageDS.cells.index(where: { $0.id == messageId }) { - model.deliveryStatus = messageDS.cells[index].deliveryStatus // TODO: castil - messageDS.cells[index] = model - //TODO: I think we need to do it outside this class - var shouldReload = [model] - messageDS.cells.forEach { (mod) in - switch mod.messageType { - case .reply(let replyModel, _): - if replyModel.id == messageId && replyModel.type.rawValue == "text" { - replyModel.text = model.text ?? "" - shouldReload.append(mod) - } - default: break + guard let messageId = model.id, let index = messageDS.index(where: { $0.id == messageId }) else { + return + } + model.deliveryStatus = messageDS.cellModel(at: index).deliveryStatus // TODO: castil + messageDS.set(model, at: index) + + //TODO: I think we need to do it outside this class + var shouldReload = [model] + + for idx in messageDS.indices { + let cellModel = messageDS.cellModel(at: idx) + + switch cellModel.messageType { + case let .reply(replyModel, _): + if replyModel.id == messageId && replyModel.type.rawValue == "text" { + replyModel.text = model.text ?? "" + shouldReload.append(cellModel) } + default: + break } - reloadIfVisible(models: shouldReload) } + reloadIfVisible(models: shouldReload) } func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel? = nil, targetView: UIView? = nil) { @@ -1041,7 +1068,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw design: NynjaContextMenuItemsFactory.defaultDesign, userInfo: itemsFactory.userInfo, targetView: targetView, - targetMask: tableView, + targetMask: collectionView, containerViewMaskRect: menuMask) let menu = NynjaContextMenu() @@ -1100,18 +1127,15 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateDeliveryStatus(_ status: DeliveryStatus, messageId: String) { - let cells = self.messageDS.cells - - guard let index = cells.index(where: { $0.id == messageId }) else { + guard let index = messageDS.index(where: { $0.id == messageId }) else { return } - if status != .read { - let model = cells[index] + let model = messageDS.cellModel(at: index) model.deliveryStatus = status reloadIfVisible(models: [model]) } else { - markMesssagesAsRead(from: index, cells: cells) + markMesssagesAsRead(from: index) } } @@ -1124,35 +1148,33 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateStar(starID: String?, messageId: String) { - let cells = self.messageDS.cells - - guard let index = cells.index(where: { $0.id == messageId }) else { + guard let model = messageDS.first(where: { $0.id == messageId }) else { return } - let model = cells[index] model.starID = starID reloadIfVisible(models: [model]) } func updateUnreadTitle(_ count: Int) { - let index = max(messageDS.cells.count - count, 0) + let index = messageDS.unreadIndex(count: count) + + collectionView.performBatchUpdates({ + messageDS.insert(.unreadModel(), at: index) + collectionView.insertItems(at: [IndexPath(row: index, section: 0)]) + }, completion: { _ in + self.scrollToUnreadMessage(with: index + 1) + self.messageDS.bottomModel.map { self.readMessage($0) } + }) - tableView.performUpdates { - messageDS.cells.insert(BaseChatCellModel.unreadModel(), at: index) - tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .none) - } adjustTableTopInset() - scrollToUnreadMessage(with: index + 1) - - let lastIndex = messageDS.cells.count - 1 - readMessage(messageDS.cells[lastIndex]) + } // MARK: Scroll to message func scrollToMessage(with localId: String?) { - if let id = localId, let index = messageDS.cells.index(where: { $0.id == id }) { + if let id = localId, let index = messageDS.index(where: { $0.id == id }) { scrollToMessage(with: index) - } else if !messageDS.cells.isEmpty { + } else if !messageDS.isEmpty { scrollToBottom() } } @@ -1161,9 +1183,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw if canScroll { scrollToMessage(with: checkpoint.localId) if checkpoint.topOffset > 0 { - var contentOffset = tableView.contentOffset.y + CGFloat(checkpoint.topOffset) - contentOffset = min(contentOffset, tableView.contentSize.height - tableView.bounds.height) - tableView.contentOffset.y = contentOffset + (gradientHeight - tableView.contentInset.bottom) + var contentOffset = collectionView.contentOffset.y + CGFloat(checkpoint.topOffset) + contentOffset = min(contentOffset, collectionView.contentSize.height - collectionView.bounds.height) + collectionView.contentOffset.y = contentOffset + (gradientHeight - collectionView.contentInset.bottom) } } } @@ -1173,15 +1195,19 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw scroll(to: indexPath, at: .top, animated: false) } - func scrollToUnreadMessage(with index: Int) { - let prevInd = index - 1 - let cellModel = messageDS.cells[safe: prevInd] - guard cellModel?.unread == true else { return } + private func scrollToUnreadMessage(with index: Int) { + guard case let prevInd = index - 1, messageDS.indices.contains(prevInd) else { + return + } + let cellModel = messageDS.cellModel(at: prevInd) + guard cellModel.unread == true else { + return + } scrollToMessage(with: prevInd) } func scrollToMessage(serverId: Int64) { - guard let index = messageDS.cells.index(where: { $0.serverID == serverId }) else { + guard let index = messageDS.index(where: { $0.serverID == serverId }) else { return } scrollToMessage(with: index) @@ -1243,7 +1269,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func displayRejoinBanner(display: Bool, count: Int) { self.messageTVDelegate.rejoinBannerDisplayed = display self.messageTVDelegate.membersCount = count - self.tableView.reloadData() + self.collectionView.reloadData() adjustTableTopInset() } @@ -1253,10 +1279,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw let replyPreviewHeight = CGFloat(Constraints.replyPreview.height) if shouldShow { - tableView.contentOffset.y += replyPreviewHeight - tableView.contentInset.top -= replyPreviewHeight + collectionView.contentOffset.y += replyPreviewHeight } else { - tableView.contentInset.top += replyPreviewHeight + collectionView.contentOffset.y -= replyPreviewHeight } replyPreview.isHidden = !shouldShow @@ -1345,63 +1370,72 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw }) } - private func registerCells(for tableView: UITableView) { + private func registerCells(for collectionView: UICollectionView) { MessageCellFactory.selfIdentifiers.forEach { - tableView.register(BaseChatCell.self, forCellReuseIdentifier: $0) + collectionView.register(BaseChatCell.self, forCellWithReuseIdentifier: $0) } MessageCellFactory.opponenetIdentifiers.forEach { - tableView.register(OponentChatCell.self, forCellReuseIdentifier: $0) + collectionView.register(OponentChatCell.self, forCellWithReuseIdentifier: $0) } - tableView.register(SystemCell.self, forCellReuseIdentifier: "system") - tableView.register(TimeCell.self, forCellReuseIdentifier: "time") - tableView.register(Unread.self, forCellReuseIdentifier: "unread") + collectionView.register(SystemCell.self, forCellWithReuseIdentifier: "system") + collectionView.register(TimeCell.self, forCellWithReuseIdentifier: "time") + collectionView.register(Unread.self, forCellWithReuseIdentifier: "unread") } func reloadIfVisible(models: [BaseChatCellModel]) { - tableView.performUpdates { + collectionView.performBatchUpdates({ var indexPaths: [IndexPath] = [] - tableView.visibleCells.forEach { (cell) in - if let model = (cell as? BaseChatCell)?.model { - if let index = models.index(where: { (mod) -> Bool in - if mod.id != nil { - return mod.id == model.id - } - - if mod.text != nil { - return mod.text == model.text - } - - return false - }) { - (cell as? BaseChatCell)?.updateData(models[index]) - indexPaths.append(tableView.indexPath(for: cell)!) + collectionView.visibleCells.forEach { (cell) in + guard let cell = cell as? BaseChatCell, let model = cell.model else { + return + } + if models.contains(where: { (mod) -> Bool in + if mod.id != nil { + return mod.id == model.id + } + + if mod.text != nil { + return mod.text == model.text + } + + return false + }) { + if let indexPath = collectionView.indexPath(for: cell) { + indexPaths.append(indexPath) } } } - tableView.reloadRows(at: indexPaths, with: .none) - } + collectionView.reloadItems(at: indexPaths) + }, completion: nil) + adjustTableTopInset() } - private func markMesssagesAsRead(from index: Int, cells: [BaseChatCellModel]) { + private func markMesssagesAsRead(from index: Int) { var updatedCells: [BaseChatCellModel] = [] - for i in (0...index).reversed() where cells[i].isOwner { - let model = cells[i] - if model.deliveryStatus == .read { + + for i in (0...index).reversed() { + guard case let model = messageDS.cellModel(at: i), model.isOwner else { + continue + } + + guard model.deliveryStatus != .read else { break - } else { - model.deliveryStatus = .read - updatedCells.append(model) } - reloadIfVisible(models: updatedCells) + + model.deliveryStatus = .read + updatedCells.append(model) } + reloadIfVisible(models: updatedCells) } func scrollToBottomIfNeeded() { - guard let model = messageDS.cells.last, bottomGap < messageTVDelegate.height(for: model) else { return } - dispatchAsyncMainAfter(0.01, block: { + guard let model = messageDS.bottomModel, bottomGap < messageTVDelegate.height(for: model) else { + return + } + dispatchAsyncMainAfter(0.01) { self.scrollToBottom(animated: true) - }) + } } func scrollToBottom() { @@ -1409,35 +1443,35 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func scrollToBottom(animated: Bool) { - let indexPath = IndexPath(row: messageDS.cells.count - 1, section: 0) + let indexPath = IndexPath(row: messageDS.bottomIndex, section: 0) scroll(to: indexPath, at: .bottom, animated: animated) } - private func scroll(to indexPath: IndexPath, at position: UITableViewScrollPosition, animated: Bool) { + private func scroll(to indexPath: IndexPath, at position: UICollectionViewScrollPosition, animated: Bool) { guard !contextMenuPresented, canScroll else { return } - tableView.scrollToRow(at: indexPath, at: position, animated: animated) + collectionView.scrollToItem(at: indexPath, at: position, animated: animated) } private var canScroll: Bool { - return canScroll(with: tableView.bounds) + return canScroll(with: collectionView.bounds) } private func canScroll(with bounds: CGRect) -> Bool { - return tableView.contentSize.height > bounds.height + return collectionView.contentSize.height > bounds.height } private var checkpoint: ChatCheckpoint? { - let visibleCells = tableView.visibleCells + let visibleCells = collectionView.visibleCells guard let index = visibleCells.index(where: { $0 is BaseChatCell }), let cell = visibleCells[index] as? BaseChatCell, let model = cell.model, let localId = model.id else { return nil } - let rect = tableView.convert(cell.frame, to: tableView.superview) - let offset = tableView.frame.origin.y - rect.origin.y + let rect = collectionView.convert(cell.frame, to: collectionView.superview) + let offset = collectionView.frame.origin.y - rect.origin.y return ChatCheckpoint(localId: localId, topOffset: Double(offset)) } @@ -1445,7 +1479,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw private var localIdOfFirstVisibleMessage: String? { var messageCell: BaseChatCell? - for cell in tableView.visibleCells { + for cell in collectionView.visibleCells { if let chatCell = cell as? BaseChatCell { messageCell = chatCell break @@ -1460,7 +1494,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } private var bottomGap: CGFloat { - return tableView.contentSize.height - (tableView.contentOffset.y + tableView.bounds.height) + return messageDS.isReversed + ? collectionView.contentOffset.y + : collectionView.contentSize.height - (collectionView.contentOffset.y + collectionView.bounds.height) } private var isLastMessageVisible: Bool { @@ -1488,22 +1524,20 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func removeMessage(_ messageId: String) { - guard let messageDS = self.messageDS, let index = messageDS.cells.index(where: { $0.id == "\(messageId)" }) else { + guard let messageDS = self.messageDS, let index = messageDS.index(where: { $0.id == messageId }) else { return } - tableView.performUpdates { - messageDS.cells.remove(at: index) - tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .none) - } + collectionView.performBatchUpdates({ + messageDS.remove(at: index) + collectionView.deleteItems(at: [IndexPath(row: index, section: 0)]) + }, completion: nil) + adjustTableTopInset() } func getNextPage() { - for i in 0.. { + return 0.. Int { + return unreadIndices(count: count).upperBound + } + + private func unreadIndices(count: Int) -> CountableRange { + return 0.. CountableRange { + return index.. CountableRange { + return 0.. Bool) -> Int? { + return cells.index(where: predicate) + } + + func first(where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { + return cells.first(where: predicate) + } + + func filter(where predicate: (BaseChatCellModel) -> Bool) -> [BaseChatCellModel] { + return cells.filter(predicate) + } + + func cellModel(at indexPath: IndexPath) -> BaseChatCellModel { + return cells[indexPath.row] + } + + func cellModel(at index: Int) -> BaseChatCellModel { + return cells[index] + } + + func cellModel(before index: Int, where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { + // don't reverse if table will be reversed + return cells[.. Int64? { + return cells.first { $0.serverID != nil }.flatMap { $0.serverID } + } +} + +// MARK: - UICollectionViewDataSource + +extension MessageCollectionViewDataSource: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let model = cellModel(at: indexPath) + + if model.unread != nil { + if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "unread", for: indexPath) as? Unread { + cell.setup() + cell.accessibilityIdentifier = "chat_cell_unread_\(indexPath.section)" + return cell + } + } + + if model.messageType == .system { + if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "system", for: indexPath) as? SystemCell { + cell.setup(message: model.text!) + cell.accessibilityIdentifier = "system_cell_unread_\(indexPath.section)" + return cell + } + } else if model.time == nil { + var cell: BaseChatCell! + + if let identifier = MessageCellFactory.identifier(for: model) { + cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as? BaseChatCell + cell.accessibilityIdentifier = "message_cell_\(identifier)_\(indexPath.section)" + } + + if view.messageTVDelegate != nil { + cell.delegate = view + } + cell.setup(model: model) + return cell + } else { + if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "time", for: indexPath) as? TimeCell { + cell.setup(date: model.time!) + cell.accessibilityIdentifier = "time_cell_unread_\(indexPath.section)" + + return cell + } + } + + return UICollectionViewCell() + } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift new file mode 100644 index 0000000000000000000000000000000000000000..60855bd90e10c578ae3da058d1b7660a0540d906 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift @@ -0,0 +1,137 @@ +// +// MessageCollectionViewDelegate.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { + + unowned var view: MessageVC + + var rejoinBannerDisplayed: Bool = false + var membersCount: Int = 0 + + init(view: MessageVC) { + self.view = view + super.init() + } + + + // MARK: - Collection View + + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + setupProgress(cell: cell) + setupMentions(cell: cell, indexPath: indexPath) + } + + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } + view.progressDictionary.removeValue(forKey: url) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: view.view.bounds.width, height: heightForItem(at: indexPath)) + } + + private func heightForItem(at indexPath: IndexPath) -> CGFloat { + let model = view.messageDS.cellModel(at: indexPath) + + if model.messageType == .system { + return 40 + } + + if let _ = model.type { + return height(for: model) + } + + if model.unread != nil { + return 30 + } + + if model.time == nil { + return height(for: model) + } else { + return 30 + } + } + + func height(for model: BaseChatCellModel) -> CGFloat { + return model.isOwner ? BaseChatCell.size(for: model).height : OponentChatCell.size(for: model).height + } + + + // MARK: - Scroll View + + var scrollOffset: CGFloat = 0 + var lastVisibleIndex: Int? + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let offset = scrollView.contentOffset.y + + let absoluteTop: CGFloat = 0 + let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height + + let isTop = scrollOffset - offset > 0 && offset > absoluteTop + let isBottom = scrollOffset - offset < 0 && offset < absoluteBottom + + if isTop { + view.isScrollingTo(.top) + } else if isBottom { + view.isScrollingTo(.bottom) + } + + if offset >= absoluteBottom { + view.didReachBottom() + } + + let contentHeight = scrollView.contentSize.height + + if offset < 0 && scrollView.frame.size.height < contentHeight { + loadData() + } + scrollOffset = offset + } + + + // MARK: - Private + + private func setupMentions(cell: UICollectionViewCell, indexPath: IndexPath) { + guard let index = lastVisibleIndex ?? view.lastVisibleCellIndex() else { + lastVisibleIndex = indexPath.row + view.setupUnreadMentions(after: indexPath.row) + return + } + guard indexPath.row > index else { + return + } + lastVisibleIndex = indexPath.row + view.setupUnreadMentions(after: indexPath.row) + } + + private func setupProgress(cell: UICollectionViewCell) { + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } + let block: (ProgressModel) -> Void = { model in + cell.updateProgressClosure(model: model) + } + if view.progressDictionary[url] == nil { + view.progressDictionary[url] = [block] + } else { + view.progressDictionary[url]?.append(block) + } + } + + private func loadData() { + if !view.loadingStatus { + view.loadingStatus = true + view.getNextPage() + } + } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift new file mode 100644 index 0000000000000000000000000000000000000000..c22152e6abec8f88e2ceee2ad0d3ae18da8768c7 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift @@ -0,0 +1,32 @@ +// +// MessageCollectionViewLayout.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MessageCollectionViewLayout: UICollectionViewFlowLayout { + + override init() { + super.init() + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() {} + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if let currentBounds = collectionView?.bounds { + return currentBounds.width != newBounds.width + } else { + return false + } + } +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift index 137438bfde888bf21bdc94fbd5d25eacb59a7d90..6fff3a30970a2200a158eb523412cc805be60972 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift @@ -9,7 +9,7 @@ import Foundation import SnapKit -class BaseChatCell: UITableViewCell, MessageBaseImageViewDataSource, MessageContentAppearance, MessageVoiceViewDelegate, ReplyCounterDelegate, MessageRepliedViewDelegate, MessageTextViewDelegate, MessageForwardViewDelegate, MessageStickerRepliedViewDelegate, MessageConvertionViewDelegate { +class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, MessageContentAppearance, MessageVoiceViewDelegate, ReplyCounterDelegate, MessageRepliedViewDelegate, MessageTextViewDelegate, MessageForwardViewDelegate, MessageStickerRepliedViewDelegate, MessageConvertionViewDelegate { private static let senderFont = UIFont(fontName: Constants.fonts.medium, height: CGFloat(22.0.adjustedByWidth)) private static let translatingFont = UIFont(name: Constants.fonts.regular, size: 12) @@ -237,9 +237,11 @@ class BaseChatCell: UITableViewCell, MessageBaseImageViewDataSource, MessageCont model?.resetHandlers() } + // MARK: - Init - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) + + override init(frame: CGRect) { + super.init(frame: frame) initialSetup() } @@ -250,7 +252,6 @@ class BaseChatCell: UITableViewCell, MessageBaseImageViewDataSource, MessageCont // MARK: - Setup func initialSetup() { - selectionStyle = .none backgroundColor = .clear setupRecognizers() } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift index 57e3daf634fed5d86748f565471892d2c41c63e3..b0b0ec9c28f9a67c1f4bf3526cfc5232d46d0952 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -final class SystemCell: UITableViewCell { +final class SystemCell: UICollectionViewCell { lazy var systemLabel: UILabel = { let label = UILabel(height: Constraints.Label.height, @@ -31,11 +31,9 @@ final class SystemCell: UITableViewCell { // MARK: - Setup func setup(message: String) { - self.selectionStyle = .none self.backgroundColor = UIColor.clear systemLabel.text = message } - } @@ -49,5 +47,4 @@ extension SystemCell { static let horizontalInset = 16.0.adjustedByWidth } } - } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift index 640d4bdb052af29f8e60f4e30471389febddb304..19cc21e0512626fb149b9bfc173ad18b61af7259 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift @@ -8,7 +8,7 @@ import Foundation -class TimeCell: UITableViewCell { +final class TimeCell: UICollectionViewCell { lazy var timeStamp: UILabel = { let v = UILabel() @@ -22,7 +22,6 @@ class TimeCell: UITableViewCell { }() func setup(date: Date) { - self.selectionStyle = .none self.backgroundColor = UIColor.clear timeStamp.text = self.getText(fromDate: date).uppercased() } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Unread.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Unread.swift index c93c62d379f0095b9093e781ed5abda7018b58e4..4ddaeb466242a988ee11ee5391a2c69b2b2561c2 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Unread.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Unread.swift @@ -8,7 +8,7 @@ import Foundation -class Unread: UITableViewCell { +final class Unread: UICollectionViewCell { lazy var timeStamp: UILabel = { let v = UILabel() @@ -49,8 +49,7 @@ class Unread: UITableViewCell { }() func setup() { - self.selectionStyle = .none - self.backgroundColor = UIColor.clear + backgroundColor = UIColor.clear timeStamp.isHidden = false line1.isHidden = false line2.isHidden = false diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift b/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift index 76130bd7e13cd19966dfcfc8356284ee890df207..0ec2afe83db368ac47b57abad6fe5c74f9ed2636 100644 --- a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift +++ b/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift @@ -11,56 +11,156 @@ class MessageDS: NSObject, UITableViewDataSource { weak var view: MessageVC! - var cells = [BaseChatCellModel]() + private var cells = [BaseChatCellModel]() + + + // MARK: - Init - // MARK: Init init(view: MessageVC) { self.view = view } - // MARK: UITableViewDataSource - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + // MARK: - Data + + let isReversed = false + + var isEmpty: Bool { + return cells.isEmpty + } + + var count: Int { return cells.count } + var indices: CountableRange { + return 0.. Int { + return unreadIndices(count: count).upperBound + } + + private func unreadIndices(count: Int) -> CountableRange { + return 0.. CountableRange { + return index.. CountableRange { + return 0.. Bool) -> Int? { + return cells.index(where: predicate) + } + + func first(where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { + return cells.first(where: predicate) + } + + func filter(where predicate: (BaseChatCellModel) -> Bool) -> [BaseChatCellModel] { + return cells.filter(predicate) + } + + func cellModel(at indexPath: IndexPath) -> BaseChatCellModel { + return cells[indexPath.row] + } + + func cellModel(at index: Int) -> BaseChatCellModel { + return cells[index] + } + + func cellModel(before index: Int, where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { + // don't reverse if table will be reversed + return cells[.. Int64? { + return cells.first { $0.serverID != nil }.flatMap { $0.serverID } + } + + + // MARK: - UITableViewDataSource + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return count + } + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = cells[indexPath.row] - - if model.unread != nil { - if let cell = tableView.dequeueReusableCell(withIdentifier: "unread", for: indexPath) as? Unread { - cell.setup() - cell.accessibilityIdentifier = "chat_cell_unread_\(indexPath.section)" - return cell - } - } - - if model.messageType == .system { - if let cell = tableView.dequeueReusableCell(withIdentifier: "system", for: indexPath) as? SystemCell { - cell.setup(message: model.text!) - cell.accessibilityIdentifier = "system_cell_unread_\(indexPath.section)" - return cell - } - } else if model.time == nil { - var cell: BaseChatCell! - - if let identifier = MessageCellFactory.identifier(for: model) { - cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? BaseChatCell - cell.accessibilityIdentifier = "message_cell_\(identifier)_\(indexPath.section)" - } - - if view.messageTVDelegate != nil { - cell.delegate = view - } - cell.setup(model: model) - return cell - } else { - if let cell = tableView.dequeueReusableCell(withIdentifier: "time", for: indexPath) as? TimeCell { - cell.setup(date: model.time!) - cell.accessibilityIdentifier = "time_cell_unread_\(indexPath.section)" - - return cell - } - } +// let model = cellModel(at: indexPath) +// +// if model.unread != nil { +// if let cell = tableView.dequeueReusableCell(withIdentifier: "unread", for: indexPath) as? Unread { +// cell.setup() +// cell.accessibilityIdentifier = "chat_cell_unread_\(indexPath.section)" +// return cell +// } +// } +// +// if model.messageType == .system { +// if let cell = tableView.dequeueReusableCell(withIdentifier: "system", for: indexPath) as? SystemCell { +// cell.setup(message: model.text!) +// cell.accessibilityIdentifier = "system_cell_unread_\(indexPath.section)" +// return cell +// } +// } else if model.time == nil { +// var cell: BaseChatCell! +// +// if let identifier = MessageCellFactory.identifier(for: model) { +// cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? BaseChatCell +// cell.accessibilityIdentifier = "message_cell_\(identifier)_\(indexPath.section)" +// } +// +// if view.messageTVDelegate != nil { +// cell.delegate = view +// } +// cell.setup(model: model) +// return cell +// } else { +// if let cell = tableView.dequeueReusableCell(withIdentifier: "time", for: indexPath) as? TimeCell { +// cell.setup(date: model.time!) +// cell.accessibilityIdentifier = "time_cell_unread_\(indexPath.section)" +// +// return cell +// } +// } return UITableViewCell() } diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift index cad450d335e90059a544a7a844029b148d5d036b..d0515e6c6b367f3c917cf5463c18d47cba536ba4 100644 --- a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift +++ b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift @@ -26,7 +26,6 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg // MARK: UITableViewDelegate func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - var callInfoView:CallInfoView? = nil if rejoinBannerDisplayed { @@ -47,7 +46,7 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let model = view?.messageDS.cells[indexPath.row] else { return 0 } + guard let model = view?.messageDS.cellModel(at: indexPath) else { return 0 } if model.messageType == .system { return 40 @@ -108,7 +107,22 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg setupMentions(cell: cell, indexPath: indexPath) } - func setupMentions(cell: UITableViewCell, indexPath: IndexPath) { + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } + view?.progressDictionary.removeValue(forKey: url) + } + + // MARK: CallInfoVIewDelegate + func didPressButtonJoinIn(callInfoView: CallInfoView) { + self.view?.rejoinRunningCall() + } + + + // MARK: - Private + + private func setupMentions(cell: UITableViewCell, indexPath: IndexPath) { guard let index = lastVisibleIndex ?? view?.lastVisibleCellIndex() else { lastVisibleIndex = indexPath.row view?.setupUnreadMentions(after: indexPath.row) @@ -120,12 +134,13 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg lastVisibleIndex = indexPath.row view?.setupUnreadMentions(after: indexPath.row) } - - func setupProgress(cell: UITableViewCell) { - guard let bcc = cell as? BaseChatCell else { return } - guard let url = bcc.model?.progressModel?.url.absoluteString else { return } - let block : (ProgressModel)-> Void = { model in - (cell as? BaseChatCell)?.updateProgressClosure(model: model) + + private func setupProgress(cell: UITableViewCell) { + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } + let block: (ProgressModel) -> Void = { model in + cell.updateProgressClosure(model: model) } if view?.progressDictionary[url] == nil { view?.progressDictionary[url] = [block] @@ -133,23 +148,12 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg view?.progressDictionary[url]?.append(block) } } - - func loadData() { + + private func loadData() { guard let v = self.view else { return } if !v.loadingStatus{ v.loadingStatus = true v.getNextPage() } } - - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let bcc = cell as? BaseChatCell else { return } - guard let url = bcc.model?.fileUrl?.absoluteString else { return } - view?.progressDictionary.removeValue(forKey: url) - } - - // MARK: CallInfoVIewDelegate - func didPressButtonJoinIn(callInfoView: CallInfoView) { - self.view?.rejoinRunningCall() - } } diff --git a/Nynja/Modules/Replies/View/RepliesDS.swift b/Nynja/Modules/Replies/View/RepliesDS.swift index 28ccc1d02314f87ba466f1d45124a459a1205be5..8f621c7e6935ea8b5ba10015d6db2cb65f6866cc 100644 --- a/Nynja/Modules/Replies/View/RepliesDS.swift +++ b/Nynja/Modules/Replies/View/RepliesDS.swift @@ -25,23 +25,23 @@ class RepliesDS: NSObject, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = cells[indexPath.row] - if model.time == nil { - var cell: BaseChatCell! - - if let identifier = MessageCellFactory.identifier(for: model) { - cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? BaseChatCell - } - - if view.tableViewDelegate != nil { - cell.delegate = view - } - - cell.setup(model: model) - -// view.cellDisplayed(at: indexPath) - - return cell - } +// if model.time == nil { +// var cell: BaseChatCell! +// +// if let identifier = MessageCellFactory.identifier(for: model) { +// cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? BaseChatCell +// } +// +// if view.tableViewDelegate != nil { +// cell.delegate = view +// } +// +// cell.setup(model: model) +// +//// view.cellDisplayed(at: indexPath) +// +// return cell +// } return UITableViewCell() }