From e67058223f006b83c78c6dbcf0e30674d5336406 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 7 Aug 2018 17:14:21 +0300 Subject: [PATCH 01/10] Minor refactoring --- .../Message/View/MessageVC+CellDelegate.swift | 6 +++--- Nynja/Modules/Message/View/MessageVC.swift | 7 +++---- .../Views/TableView/MessageTableViewDelegate.swift | 14 ++++++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift index 78ff22af8..afe12aea7 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 9d8d3312b..eecd473e6 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1125,11 +1125,10 @@ 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 = cells.first(where: { $0.id == messageId }) else { return } - let model = cells[index] model.starID = starID reloadIfVisible(models: [model]) } @@ -1488,7 +1487,7 @@ 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.cells.index(where: { $0.id == messageId }) else { return } tableView.performUpdates { diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift index cad450d33..be3cb8e63 100644 --- a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift +++ b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift @@ -122,10 +122,11 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg } 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) + 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] @@ -143,8 +144,9 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg } 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 } + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } view?.progressDictionary.removeValue(forKey: url) } -- GitLab From bd4725428eb0703728a1ce8cdea5a08edd7b814e Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 7 Aug 2018 19:24:55 +0300 Subject: [PATCH 02/10] [NY-2285] Refactored MessageDS in order to be able to handle reversed datasource. --- Nynja/Modules/Message/View/MessageVC.swift | 153 +++++++++--------- .../View/Views/TableView/MessageDS.swift | 108 ++++++++++++- .../TableView/MessageTableViewDelegate.swift | 2 +- 3 files changed, 183 insertions(+), 80 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index eecd473e6..199766c22 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -670,9 +670,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[.. cells.count { - let indexPath = IndexPath(row: cells.count, section: 0) + if messageDS.count > history.count { + let indexPath = IndexPath(row: history.count, section: 0) let rect = tableView.rectForRow(at: indexPath) tableView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell } @@ -951,7 +949,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw 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 } } @@ -961,12 +959,12 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw tableView.performUpdates(animated: false) { if let unrIndex = unreadCellIndex { - messageDS.cells.remove(at: unrIndex) + messageDS.remove(at: unrIndex) tableView.deleteRows(at: [IndexPath(row: unrIndex, section: 0)], with: .none) } - messageDS.cells.append(cell) - tableView.insertRows(at: [IndexPath(row: messageDS.cells.count - 1, section: 0)], with: .none) + messageDS.addNewMessage(cell) + tableView.insertRows(at: [IndexPath(row: messageDS.bottomIndex, section: 0)], with: .none) } adjustTableTopInset() @@ -992,30 +990,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 index in messageDS.indices { + let model = messageDS.cellModel(at: index) + + switch model.messageType { + case let .reply(replyModel, _): + if replyModel.id == messageId && replyModel.type.rawValue == "text" { + replyModel.text = model.text ?? "" + shouldReload.append(model) } + default: + break } - reloadIfVisible(models: shouldReload) } + reloadIfVisible(models: shouldReload) } func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel? = nil, targetView: UIView? = nil) { @@ -1100,18 +1104,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,9 +1125,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateStar(starID: String?, messageId: String) { - let cells = self.messageDS.cells - - guard let model = cells.first(where: { $0.id == messageId }) else { + guard let model = messageDS.first(where: { $0.id == messageId }) else { return } model.starID = starID @@ -1134,24 +1133,23 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateUnreadTitle(_ count: Int) { - let index = max(messageDS.cells.count - count, 0) + let index = messageDS.unreadIndex(count: count) tableView.performUpdates { - messageDS.cells.insert(BaseChatCellModel.unreadModel(), at: index) + messageDS.insert(.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]) + messageDS.bottomModel.map { readMessage($0) } } // 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() } } @@ -1173,14 +1171,18 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func scrollToUnreadMessage(with index: Int) { - let prevInd = index - 1 - let cellModel = messageDS.cells[safe: prevInd] - guard cellModel?.unread == true else { return } + 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) @@ -1382,25 +1384,31 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw 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() { @@ -1408,7 +1416,7 @@ 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) } @@ -1487,22 +1495,19 @@ 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) + messageDS.remove(at: index) tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .none) } adjustTableTopInset() } func getNextPage() { - for i in 0.. Int { + + // MARK: - Data + + 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] + let model = cellModel(at: indexPath) if model.unread != nil { if let cell = tableView.dequeueReusableCell(withIdentifier: "unread", for: indexPath) as? Unread { diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift index be3cb8e63..9c0c825af 100644 --- a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift +++ b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift @@ -47,7 +47,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 -- GitLab From 9424c9a6ff6826e97850768f938de4e1c53b0afb Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 8 Aug 2018 12:00:25 +0300 Subject: [PATCH 03/10] MInor fix in datasource handling --- Nynja/Modules/Message/View/MessageVC.swift | 81 +++++++++---------- .../View/Views/TableView/MessageDS.swift | 2 + .../TableView/MessageTableViewDelegate.swift | 38 ++++----- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 199766c22..c9c1acf71 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -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 @@ -703,14 +709,12 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw toggleMentionsCounter(false) } - private func lastMessagePosition() -> 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 lastMessagePosition = CGPoint( + x: tableView.bounds.width / 2, + y: tableView.contentOffset.y + tableView.bounds.height - tableView.contentInset.bottom + ) + return tableView.indexPathForRow(at: lastMessagePosition)?.row } @@ -823,8 +827,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateData(with configuration: DisplayChatConfiguration) { - let isLastMessageVisible = self.isLastMessageVisible - messageDS.update(configuration.cells) tableView.reloadData() adjustTableTopInset() @@ -837,7 +839,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateNewData(_ history: [BaseChatCellModel]) { - var lastTimeShowed: Date? for i in (0.. history.count { + if !messageDS.isReversed, messageDS.count > history.count { let indexPath = IndexPath(row: history.count, section: 0) let rect = tableView.rectForRow(at: indexPath) tableView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell @@ -942,7 +939,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func newMessage(_ cell: BaseChatCellModel) { - let isLastMessageVisible = self.isLastMessageVisible if let messageDS = self.messageDS { var unreadCellIndex: Int? @@ -969,9 +965,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw adjustTableTopInset() if isLastMessageVisible || cell.isOwner { - dispatchAsyncMainAfter(0.01, block: { + dispatchAsyncMainAfter(0.01) { self.scrollToBottom(animated: false) - }) + } } else { buttonState = .newMessages } @@ -1362,20 +1358,23 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw tableView.performUpdates { 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)!) + guard let cell = cell as? BaseChatCell, let model = cell.model else { + return + } + if let cellModel = models.first(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.updateData(cellModel) + if let indexPath = tableView.indexPath(for: cell) { + indexPaths.append(indexPath) } } } diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift b/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift index 87312a2a8..df9e8540d 100644 --- a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift +++ b/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift @@ -23,6 +23,8 @@ class MessageDS: NSObject, UITableViewDataSource { // MARK: - Data + let isReversed = false + var isEmpty: Bool { return cells.isEmpty } diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift index 9c0c825af..d0515e6c6 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 { @@ -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,8 +134,8 @@ class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDeleg lastVisibleIndex = indexPath.row view?.setupUnreadMentions(after: indexPath.row) } - - func setupProgress(cell: UITableViewCell) { + + private func setupProgress(cell: UITableViewCell) { guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { return } @@ -134,24 +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 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() - } } -- GitLab From 9f725234d1c5805d8fa89cfa4fe0e72e8555173f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 8 Aug 2018 12:35:53 +0300 Subject: [PATCH 04/10] Update scrolling behavior --- Nynja/Modules/Message/View/MessageVC.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index c9c1acf71..ae60d8f77 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -710,9 +710,13 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func lastVisibleCellIndex() -> Int? { + let offset = messageDS.isReversed + ? -tableView.contentInset.top + : tableView.bounds.height - tableView.contentInset.bottom + let lastMessagePosition = CGPoint( x: tableView.bounds.width / 2, - y: tableView.contentOffset.y + tableView.bounds.height - tableView.contentInset.bottom + y: tableView.contentOffset.y + offset ) return tableView.indexPathForRow(at: lastMessagePosition)?.row } @@ -1163,7 +1167,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func scrollToMessage(with index: Int) { let indexPath = IndexPath(row: index, section: 0) - scroll(to: indexPath, at: .top, animated: false) + scroll(to: indexPath, at: .bottom, animated: false) } func scrollToUnreadMessage(with index: Int) { @@ -1416,7 +1420,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func scrollToBottom(animated: Bool) { let indexPath = IndexPath(row: messageDS.bottomIndex, section: 0) - scroll(to: indexPath, at: .bottom, animated: animated) + scroll(to: indexPath, at: .top, animated: animated) } private func scroll(to indexPath: IndexPath, at position: UITableViewScrollPosition, animated: Bool) { @@ -1466,7 +1470,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 + ? tableView.contentOffset.y + : tableView.contentSize.height - (tableView.contentOffset.y + tableView.bounds.height) } private var isLastMessageVisible: Bool { -- GitLab From 90f4e1c51f06dbd3b55659972936bd542578d82b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 8 Aug 2018 16:55:57 +0300 Subject: [PATCH 05/10] Restore scroll positions --- Nynja/Modules/Message/Protocols/MessageProtocols.swift | 3 +-- Nynja/Modules/Message/View/MessageVC.swift | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 4ae1ebb15..4e8660a49 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.swift b/Nynja/Modules/Message/View/MessageVC.swift index ae60d8f77..338730f49 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1167,10 +1167,10 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func scrollToMessage(with index: Int) { let indexPath = IndexPath(row: index, section: 0) - scroll(to: indexPath, at: .bottom, animated: false) + scroll(to: indexPath, at: .top, animated: false) } - func scrollToUnreadMessage(with index: Int) { + private func scrollToUnreadMessage(with index: Int) { guard case let prevInd = index - 1, messageDS.indices.contains(prevInd) else { return } @@ -1420,7 +1420,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func scrollToBottom(animated: Bool) { let indexPath = IndexPath(row: messageDS.bottomIndex, section: 0) - scroll(to: indexPath, at: .top, animated: animated) + scroll(to: indexPath, at: .bottom, animated: animated) } private func scroll(to indexPath: IndexPath, at position: UITableViewScrollPosition, animated: Bool) { -- GitLab From f45ffaaee72ab50a8fbf3a3feb93ded6f2523a01 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 8 Aug 2018 19:14:57 +0300 Subject: [PATCH 06/10] Replace tableView with collectionView --- Nynja.xcodeproj/project.pbxproj | 20 ++ Nynja/Modules/Message/View/MessageVC.swift | 264 +++++++++--------- .../MessageCollectionViewDataSource.swift | 168 +++++++++++ .../MessageCollectionViewDelegate.swift | 137 +++++++++ .../MessageCollectionViewLayout.swift | 102 +++++++ .../ChatCells/BaseChatCell/BaseChatCell.swift | 9 +- .../Views/TableView/Cells/SystemCell.swift | 5 +- .../View/Views/TableView/Cells/TimeCell.swift | 3 +- .../View/Views/TableView/Cells/Unread.swift | 5 +- .../View/Views/TableView/MessageDS.swift | 74 ++--- Nynja/Modules/Replies/View/RepliesDS.swift | 34 +-- 11 files changed, 628 insertions(+), 193 deletions(-) create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c8c6681a0..82a372f43 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 */, @@ -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/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 338730f49..405d45a2d 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 @@ -100,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: MessageCollectionViewLayout = { + let layout = MessageCollectionViewLayout() + return layout + }() + + private(set) lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: MessageCollectionViewLayout()) + 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 @@ -173,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 @@ -235,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 @@ -300,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) @@ -352,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 @@ -387,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 @@ -430,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() } @@ -465,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), @@ -486,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 @@ -502,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) @@ -711,14 +731,14 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func lastVisibleCellIndex() -> Int? { let offset = messageDS.isReversed - ? -tableView.contentInset.top - : tableView.bounds.height - tableView.contentInset.bottom + ? -collectionView.contentInset.top + : collectionView.bounds.height - collectionView.contentInset.bottom let lastMessagePosition = CGPoint( - x: tableView.bounds.width / 2, - y: tableView.contentOffset.y + offset + x: collectionView.bounds.width / 2, + y: collectionView.contentOffset.y + offset ) - return tableView.indexPathForRow(at: lastMessagePosition)?.row + return collectionView.indexPathForItem(at: lastMessagePosition)?.item } @@ -832,7 +852,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func updateData(with configuration: DisplayChatConfiguration) { messageDS.update(configuration.cells) - tableView.reloadData() + collectionView.reloadData() adjustTableTopInset() hasUnread = configuration.isUnreadShown @@ -861,8 +881,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw continue } let indexPath = IndexPath(row: i, section: 0) - if let cell = tableView.cellForRow(at: indexPath) { - offsetForDeletedTimeCell = cell.bounds.height + if let attributes = collectionView.layoutAttributesForItem(at: indexPath) { + offsetForDeletedTimeCell = attributes.bounds.height } messageDS.remove(at: i) break @@ -870,15 +890,18 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } messageDS.insertNewHistory(history) - let initialOffset = tableView.contentOffset + let initialOffset = collectionView.contentOffset - tableView.reloadData() + collectionView.reloadData() adjustTableTopInset() if !messageDS.isReversed, messageDS.count > history.count { let indexPath = IndexPath(row: history.count, section: 0) - let rect = tableView.rectForRow(at: indexPath) - tableView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell + guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { + return + } + let rect = attributes.frame + collectionView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell } loadingStatus = false @@ -954,27 +977,27 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } } } - + readMessage(cell) - tableView.performUpdates(animated: false) { + collectionView.performBatchUpdates({ if let unrIndex = unreadCellIndex { messageDS.remove(at: unrIndex) - tableView.deleteRows(at: [IndexPath(row: unrIndex, section: 0)], with: .none) + collectionView.deleteItems(at: [IndexPath(row: unrIndex, section: 0)]) } - messageDS.addNewMessage(cell) - tableView.insertRows(at: [IndexPath(row: messageDS.bottomIndex, section: 0)], with: .none) - } - adjustTableTopInset() - - if isLastMessageVisible || cell.isOwner { - dispatchAsyncMainAfter(0.01) { - self.scrollToBottom(animated: false) + 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 } - } else { - buttonState = .newMessages - } + }) + adjustTableTopInset() } } @@ -1045,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() @@ -1135,14 +1158,16 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func updateUnreadTitle(_ count: Int) { let index = messageDS.unreadIndex(count: count) - tableView.performUpdates { + collectionView.performBatchUpdates({ messageDS.insert(.unreadModel(), at: index) - tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .none) - } - adjustTableTopInset() - scrollToUnreadMessage(with: index + 1) + collectionView.insertItems(at: [IndexPath(row: index, section: 0)]) + }, completion: { _ in + self.scrollToUnreadMessage(with: index + 1) + self.messageDS.bottomModel.map { self.readMessage($0) } + }) - messageDS.bottomModel.map { readMessage($0) } + adjustTableTopInset() + } // MARK: Scroll to message @@ -1158,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) } } } @@ -1244,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() } @@ -1254,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 @@ -1346,22 +1370,22 @@ 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 + collectionView.visibleCells.forEach { (cell) in guard let cell = cell as? BaseChatCell, let model = cell.model else { return } @@ -1377,13 +1401,14 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw return false }) { cell.updateData(cellModel) - if let indexPath = tableView.indexPath(for: cell) { + if let indexPath = collectionView.indexPath(for: cell) { indexPaths.append(indexPath) } } } - tableView.reloadRows(at: indexPaths, with: .none) - } + collectionView.reloadItems(at: indexPaths) + }, completion: nil) + adjustTableTopInset() } @@ -1423,31 +1448,31 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw 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)) } @@ -1455,7 +1480,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 @@ -1471,8 +1496,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw private var bottomGap: CGFloat { return messageDS.isReversed - ? tableView.contentOffset.y - : tableView.contentSize.height - (tableView.contentOffset.y + tableView.bounds.height) + ? collectionView.contentOffset.y + : collectionView.contentSize.height - (collectionView.contentOffset.y + collectionView.bounds.height) } private var isLastMessageVisible: Bool { @@ -1503,10 +1528,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw guard let messageDS = self.messageDS, let index = messageDS.index(where: { $0.id == messageId }) else { return } - tableView.performUpdates { + collectionView.performBatchUpdates({ messageDS.remove(at: index) - tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .none) - } + collectionView.deleteItems(at: [IndexPath(row: index, section: 0)]) + }, completion: nil) + adjustTableTopInset() } @@ -1539,10 +1565,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw guard let prevIndex = messageDS.index(where: { $0.unread == true }) else { return } - tableView.performUpdates { + collectionView.performBatchUpdates({ messageDS.remove(at: prevIndex) - tableView.deleteRows(at: [IndexPath(row: prevIndex, section: 0)], with: .none) - } + collectionView.deleteItems(at: [IndexPath(row: prevIndex, section: 0)]) + }, completion: nil) + adjustTableTopInset() } @@ -1571,25 +1598,10 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw // MARK: - Input View private func adjustTableTopInset(updateOffset: Bool = true) { - adjustTableTopInset(for: tableView.bounds, updateOffset: updateOffset) + adjustTableTopInset(for: collectionView.bounds, updateOffset: updateOffset) } private func adjustTableTopInset(for bounds: CGRect, updateOffset: Bool = true) { - let minTop = Constraints.tableView.defaultVerticalInset - var topInset = max(minTop, bounds.height - tableView.contentSize.height) - let canScroll = self.canScroll(with: bounds) - - if !canScroll { - topInset -= gradientHeight - tableView.contentInset.bottom - } - - guard tableView.contentInset.top != topInset else { return } - - tableView.contentInset.top = topInset - - if updateOffset, !canScroll { - tableView.contentOffset.y = -topInset - } } //MARK: - Rejoin bar @@ -1610,7 +1622,7 @@ extension MessageVC { make.height.equalTo(0) } - tableView.snp.remakeConstraints { make in + collectionView.snp.remakeConstraints { make in makeDefaultTableConstraint(make) make.bottom.equalTo(staticFooterView.snp.top) } @@ -1623,7 +1635,7 @@ extension MessageVC { staticFooterView.snp.updateConstraints { make in make.height.equalTo(gradientHeight) } - tableView.snp.remakeConstraints { make in + collectionView.snp.remakeConstraints { make in makeDefaultTableConstraint(make) make.bottom.equalTo(staticFooterView.snp.top).offset(-FooterViewLayout.collapsedHeight) } diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift new file mode 100644 index 000000000..468232cec --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift @@ -0,0 +1,168 @@ +// +// MessageCollectionViewDataSource.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MessageCollectionViewDataSource: NSObject { + + unowned var view: MessageVC + + private var cells = [BaseChatCellModel]() + + init(view: MessageVC) { + self.view = view + super.init() + } + + + // 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: - 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 000000000..60855bd90 --- /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 000000000..9842d055d --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift @@ -0,0 +1,102 @@ +// +// MessageCollectionViewLayout.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MessageCollectionViewLayout: UICollectionViewFlowLayout { + + private var minContentHeight: CGFloat { + return max(collectionView?.bounds.height ?? 0, collectionViewContentSize.height) + } + + override init() { + super.init() + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() { +// sectionFootersPinToVisibleBounds = true + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + guard let attributes = super.layoutAttributesForItem(at: indexPath) else { return nil } + return modifiedAttributes(attributes) + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil } + return attributes.map { modifiedAttributes($0) } + } + + override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) +// guard let attributes = superAttributes?.copy() as? UICollectionViewLayoutAttributes else { +// return superAttributes +// } + + attributes?.alpha = 1 + return attributes + } + + override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) +// guard let attributes = superAttributes?.copy() as? UICollectionViewLayoutAttributes else { +// return superAttributes +// } + + attributes?.alpha = 1 + return attributes + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if let currentBounds = collectionView?.bounds { + return currentBounds.width != newBounds.width + } else { + return false + } + } + + + // MARK: - Utils + + private func modifiedAttributes(_ attr: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { +// guard let copy = attr.copy() as? UICollectionViewLayoutAttributes else { fatalError() } + let copy = attr + copy.center = shiftedPoint(for: attr.center) + return copy + } + + private func shiftedPoint(for point: CGPoint) -> CGPoint { + return CGPoint(x: point.x, y: shiftedY(for: point.y)) + } + + private func shiftedY(for y: CGFloat) -> CGFloat { + let visibleHeight = (collectionView?.bounds.size ?? collectionViewContentSize).height + let contentHeight = collectionViewContentSize.height + + guard contentHeight < visibleHeight else { + return y + } + + return y + visibleHeight - contentHeight + } + +// private func shiftedRect(for rect: CGRect) -> CGRect { +// let size = rect.size +// let normalTopLeft = rect.origin +// let reversedBottomLeft = shiftedPoint(for: normalTopLeft) +// let reversedTopLeft = CGPoint(x: reversedBottomLeft.x, y: reversedBottomLeft.y - size.height) +// let reversedRect = CGRect(origin: reversedTopLeft, size: size) +// return reversedRect +// } +} 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 137438bfd..6fff3a309 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 57e3daf63..b0b0ec9c2 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 640d4bdb0..19cc21e05 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 c93c62d37..4ddaeb466 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 df9e8540d..0ec2afe83 100644 --- a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift +++ b/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift @@ -124,43 +124,43 @@ class MessageDS: NSObject, UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - 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 - } - } +// 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/Replies/View/RepliesDS.swift b/Nynja/Modules/Replies/View/RepliesDS.swift index 28ccc1d02..8f621c7e6 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() } -- GitLab From e73605be147d6d8183e6ba40d38600c94ba7b1c7 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 8 Aug 2018 22:51:48 +0300 Subject: [PATCH 07/10] Simplify collection view layout --- Nynja.xcodeproj/project.pbxproj | 4 +- .../MessageCollectionViewLayout.swift | 73 +++---------------- 2 files changed, 13 insertions(+), 64 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 82a372f43..16f197af0 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -12576,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", @@ -12603,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", diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift index 9842d055d..9cabc525d 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift @@ -10,10 +10,6 @@ import UIKit final class MessageCollectionViewLayout: UICollectionViewFlowLayout { - private var minContentHeight: CGFloat { - return max(collectionView?.bounds.height ?? 0, collectionViewContentSize.height) - } - override init() { super.init() setup() @@ -25,36 +21,23 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { } private func setup() { -// sectionFootersPinToVisibleBounds = true - } - - override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - guard let attributes = super.layoutAttributesForItem(at: indexPath) else { return nil } - return modifiedAttributes(attributes) - } - - override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil } - return attributes.map { modifiedAttributes($0) } } - + override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) -// guard let attributes = superAttributes?.copy() as? UICollectionViewLayoutAttributes else { -// return superAttributes -// } - - attributes?.alpha = 1 + let superAttributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) + guard let attributes = superAttributes else { + return superAttributes + } + attributes.alpha = 1 return attributes } override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - let attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) -// guard let attributes = superAttributes?.copy() as? UICollectionViewLayoutAttributes else { -// return superAttributes -// } - - attributes?.alpha = 1 + let superAttributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) + guard let attributes = superAttributes else { + return superAttributes + } + attributes.alpha = 1 return attributes } @@ -65,38 +48,4 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { return false } } - - - // MARK: - Utils - - private func modifiedAttributes(_ attr: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { -// guard let copy = attr.copy() as? UICollectionViewLayoutAttributes else { fatalError() } - let copy = attr - copy.center = shiftedPoint(for: attr.center) - return copy - } - - private func shiftedPoint(for point: CGPoint) -> CGPoint { - return CGPoint(x: point.x, y: shiftedY(for: point.y)) - } - - private func shiftedY(for y: CGFloat) -> CGFloat { - let visibleHeight = (collectionView?.bounds.size ?? collectionViewContentSize).height - let contentHeight = collectionViewContentSize.height - - guard contentHeight < visibleHeight else { - return y - } - - return y + visibleHeight - contentHeight - } - -// private func shiftedRect(for rect: CGRect) -> CGRect { -// let size = rect.size -// let normalTopLeft = rect.origin -// let reversedBottomLeft = shiftedPoint(for: normalTopLeft) -// let reversedTopLeft = CGPoint(x: reversedBottomLeft.x, y: reversedBottomLeft.y - size.height) -// let reversedRect = CGRect(origin: reversedTopLeft, size: size) -// return reversedRect -// } } -- GitLab From 2ef4e2574b513960157f6ba684862f6c91509f96 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 8 Aug 2018 23:42:06 +0300 Subject: [PATCH 08/10] Fixed layout reloading --- Nynja/Modules/Message/View/MessageVC.swift | 4 +-- .../MessageCollectionViewLayout.swift | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 405d45a2d..6ed15235a 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -101,13 +101,13 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } } - private let collectionViewLayout: MessageCollectionViewLayout = { + private let collectionViewLayout: UICollectionViewFlowLayout = { let layout = MessageCollectionViewLayout() return layout }() private(set) lazy var collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: MessageCollectionViewLayout()) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) collectionView.backgroundColor = .clear collectionView.alwaysBounceVertical = true diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift index 9cabc525d..0b4b93c54 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift @@ -22,6 +22,14 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { private func setup() { } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if let currentBounds = collectionView?.bounds { + return currentBounds.width != newBounds.width + } else { + return false + } + } override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let superAttributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) @@ -37,15 +45,16 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { guard let attributes = superAttributes else { return superAttributes } - attributes.alpha = 1 + attributes.alpha = 0 return attributes } - override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - if let currentBounds = collectionView?.bounds { - return currentBounds.width != newBounds.width - } else { - return false - } - } + // TODO: - + + // This method is called when there is an update with deletes/inserts to the collection view. + // It will be called prior to calling the initial/final layout attribute methods below to give the layout an opportunity to do batch computations for the insertion and deletion layout attributes. + // The updateItems parameter is an array of UICollectionViewUpdateItem instances for each element that is moving to a new index path. + // open func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) + + // open func finalizeCollectionViewUpdates() // called inside an animation block after the update } -- GitLab From 45ebc6c31a6a92f495f3da14579d5ec3acb454bd Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 9 Aug 2018 00:15:05 +0300 Subject: [PATCH 09/10] Fixed 'reloadIfVisible' --- Nynja/Modules/Message/View/MessageVC.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 6ed15235a..616b5c5a8 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1389,7 +1389,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw guard let cell = cell as? BaseChatCell, let model = cell.model else { return } - if let cellModel = models.first(where: { (mod) -> Bool in + if models.contains(where: { (mod) -> Bool in if mod.id != nil { return mod.id == model.id } @@ -1400,7 +1400,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw return false }) { - cell.updateData(cellModel) if let indexPath = collectionView.indexPath(for: cell) { indexPaths.append(indexPath) } -- GitLab From 7019ecdb68751177215a823868262b64c79dab06 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 9 Aug 2018 10:20:19 +0300 Subject: [PATCH 10/10] Remove unused layout code. Fixed updateMessage --- Nynja/Modules/Message/View/MessageVC.swift | 8 ++--- .../MessageCollectionViewLayout.swift | 30 +------------------ 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 616b5c5a8..ec993888c 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1029,14 +1029,14 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw //TODO: I think we need to do it outside this class var shouldReload = [model] - for index in messageDS.indices { - let model = messageDS.cellModel(at: index) + for idx in messageDS.indices { + let cellModel = messageDS.cellModel(at: idx) - switch model.messageType { + switch cellModel.messageType { case let .reply(replyModel, _): if replyModel.id == messageId && replyModel.type.rawValue == "text" { replyModel.text = model.text ?? "" - shouldReload.append(model) + shouldReload.append(cellModel) } default: break diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift index 0b4b93c54..c22152e6a 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift @@ -20,8 +20,7 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { setup() } - private func setup() { - } + private func setup() {} override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { if let currentBounds = collectionView?.bounds { @@ -30,31 +29,4 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { return false } } - - override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - let superAttributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) - guard let attributes = superAttributes else { - return superAttributes - } - attributes.alpha = 1 - return attributes - } - - override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - let superAttributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) - guard let attributes = superAttributes else { - return superAttributes - } - attributes.alpha = 0 - return attributes - } - - // TODO: - - - // This method is called when there is an update with deletes/inserts to the collection view. - // It will be called prior to calling the initial/final layout attribute methods below to give the layout an opportunity to do batch computations for the insertion and deletion layout attributes. - // The updateItems parameter is an array of UICollectionViewUpdateItem instances for each element that is moving to a new index path. - // open func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) - - // open func finalizeCollectionViewUpdates() // called inside an animation block after the update } -- GitLab