diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift new file mode 100644 index 000000000..874be05e3 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift @@ -0,0 +1,293 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +@objcMembers +class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdaterProtocol { + + // MARK: - Constants + + private enum Constants { + static let bubbleBackgroundViewCornerRadius: CGFloat = 12.0 + } + + // MARK: - Properties + + private var incomingColor: UIColor { + return ThemeService.shared().theme.colors.system + } + + private var outgoingColor: UIColor { + return ThemeService.shared().theme.colors.accent.withAlphaComponent(0.10) + } + + // MARK: - Public + + func updateLayoutIfNeeded(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { + + var isMessageSenderCurrentUser: Bool = false + + if let senderId = cellData.senderId, let currentUserId = cellData.mxSession.myUserId, senderId == currentUserId { + isMessageSenderCurrentUser = true + } + + if isMessageSenderCurrentUser { + self.updateLayout(forOutgoingTextMessageCell: cell, andCellData: cellData) + } else { + self.updateLayout(forIncomingTextMessageCell: cell, andCellData: cellData) + } + + } + + func updateLayout(forIncomingTextMessageCell cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { + + if let messageBubbleBackgroundView = self.getMessageBubbleBackgroundView(from: cell) { + + if self.canUseBubbleBackground(forCell: cell, withCellData: cellData) { + + messageBubbleBackgroundView.isHidden = false + + self.updateMessageBubbleBackgroundView(messageBubbleBackgroundView, withCell: cell, andCellData: cellData) + } else { + messageBubbleBackgroundView.isHidden = true + } + } + } + + func updateLayout(forOutgoingTextMessageCell cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { + + if let messageBubbleBackgroundView = self.getMessageBubbleBackgroundView(from: cell) { + + if self.canUseBubbleBackground(forCell: cell, withCellData: cellData) { + + messageBubbleBackgroundView.isHidden = false + + self.updateMessageBubbleBackgroundView(messageBubbleBackgroundView, withCell: cell, andCellData: cellData) + } else { + messageBubbleBackgroundView.isHidden = true + } + } + } + + func setupLayout(forIncomingTextMessageCell cell: MXKRoomBubbleTableViewCell) { + + self.setupIncomingMessageTextViewMargins(for: cell) + + self.addBubbleBackgroundViewToCell(cell, backgroundColor: self.incomingColor) + + cell.setNeedsUpdateConstraints() + } + + func setupLayout(forOutgoingTextMessageCell cell: MXKRoomBubbleTableViewCell) { + + self.setupOutgoingMessageTextViewMargins(for: cell) + + // Hide avatar view + cell.pictureView?.isHidden = true + + self.addBubbleBackgroundViewToCell(cell, backgroundColor: self.outgoingColor) + + cell.setNeedsUpdateConstraints() + } + + // MARK: - Private + + // MARK: Bubble background view + + private func createBubbleBackgroundView(with backgroundColor: UIColor) -> RoomMessageBubbleBackgroundView { + + let bubbleBackgroundView = RoomMessageBubbleBackgroundView() + bubbleBackgroundView.backgroundColor = backgroundColor + + return bubbleBackgroundView + } + + func getMessageBubbleBackgroundView(from cell: MXKRoomBubbleTableViewCell) -> RoomMessageBubbleBackgroundView? { + + let foundView = cell.contentView.subviews.first { view in + return view is RoomMessageBubbleBackgroundView + } + + return foundView as? RoomMessageBubbleBackgroundView + } + + private func addBubbleBackgroundViewToCell(_ bubbleCell: MXKRoomBubbleTableViewCell, backgroundColor: UIColor) { + + guard let messageTextView = bubbleCell.messageTextView else { + return + } + + let topMargin: CGFloat = 0.0 + let leftMargin: CGFloat = -5.0 + let rightMargin: CGFloat = 5.0 + + let bubbleBackgroundView = self.createBubbleBackgroundView(with: backgroundColor) + + bubbleCell.contentView.insertSubview(bubbleBackgroundView, at: 0) + + let topAnchor = messageTextView.topAnchor + let leadingAnchor = messageTextView.leadingAnchor + let trailingAnchor = messageTextView.trailingAnchor + + bubbleBackgroundView.updateHeight(messageTextView.frame.height) + + NSLayoutConstraint.activate([ + bubbleBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: topMargin), + bubbleBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: leftMargin), + bubbleBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightMargin) + ]) + } + + private func canUseBubbleBackground(forCell cell: MXKRoomBubbleTableViewCell, withCellData cellData: MXKRoomBubbleCellData) -> Bool { + + guard let firstComponent = cellData.getFirstBubbleComponentWithDisplay(), let firstEvent = firstComponent.event else { + return false + } + + switch firstEvent.eventType { + case .roomMessage: + if let messageTypeString = firstEvent.content["msgtype"] as? String { + + let messageType = MXMessageType(identifier: messageTypeString) + + switch messageType { + case .text : + return true + default: + break + } + } + default: + break + } + + return false + } + + private func getTextMessageHeight(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) -> CGFloat? { + + guard let roomBubbleCellData = cellData as? RoomBubbleCellData, + let lastBubbleComponent = cellData.getLastBubbleComponentWithDisplay(), + let firstComponent = roomBubbleCellData.getFirstBubbleComponentWithDisplay() else { + return nil + } + + + let bubbleHeight: CGFloat + let bottomMargin: CGFloat = 4.0 + + let lastEventId = lastBubbleComponent.event.eventId + let lastMessageBottomPosition = cell.bottomPosition(ofEvent: lastEventId) + + let firstEventId = firstComponent.event.eventId + let firstMessageTopPosition = cell.topPosition(ofEvent: firstEventId) + + let additionalContentHeight = roomBubbleCellData.additionalHeight(forEvent: lastEventId) + + bubbleHeight = lastMessageBottomPosition - firstMessageTopPosition - additionalContentHeight + bottomMargin + + guard bubbleHeight >= 0 else { + return nil + } + + return bubbleHeight + } + + @discardableResult + private func updateMessageBubbleBackgroundView(_ roomMessageBubbleBackgroundView: RoomMessageBubbleBackgroundView, withCell cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) -> Bool { + + var finalBubbleHeight: CGFloat? + + if let bubbleHeight = self.getTextMessageHeight(for: cell, andCellData: cellData) { + finalBubbleHeight = bubbleHeight + + } else if let messageTextViewHeight = cell.messageTextView?.frame.height { + + finalBubbleHeight = messageTextViewHeight + } + + if let finalBubbleHeight = finalBubbleHeight { + return roomMessageBubbleBackgroundView.updateHeight(finalBubbleHeight) + } else { + return false + } + } + + private func getIncomingMessageTextViewInsets(from bubbleCell: MXKRoomBubbleTableViewCell) -> UIEdgeInsets { + + let messageViewMarginTop: CGFloat + let messageViewMarginBottom: CGFloat = -2.0 + let messageViewMarginLeft: CGFloat = 3.0 + let messageViewMarginRight: CGFloat = 0.0 + + if bubbleCell.userNameLabel != nil { + messageViewMarginTop = 10.0 + } else { + messageViewMarginTop = 0.0 + } + + let messageViewInsets = UIEdgeInsets(top: messageViewMarginTop, left: messageViewMarginLeft, bottom: messageViewMarginBottom, right: messageViewMarginRight) + + return messageViewInsets + } + + // MARK: Text message + + private func setupIncomingMessageTextViewMargins(for cell: MXKRoomBubbleTableViewCell) { + + guard cell.messageTextView != nil else { + return + } + + let messageViewInsets = self.getIncomingMessageTextViewInsets(from: cell) + + cell.msgTextViewBottomConstraint.constant += messageViewInsets.bottom + cell.msgTextViewTopConstraint.constant += messageViewInsets.top + cell.msgTextViewLeadingConstraint.constant += messageViewInsets.left + cell.msgTextViewTrailingConstraint.constant += messageViewInsets.right + } + + private func setupOutgoingMessageTextViewMargins(for cell: MXKRoomBubbleTableViewCell) { + + guard let messageTextView = cell.messageTextView else { + return + } + + let contentView = cell.contentView + + let leftMargin: CGFloat = 80.0 + let rightMargin: CGFloat = 38.0 + let bottomMargin: CGFloat = -2.0 + + cell.msgTextViewLeadingConstraint.isActive = false + cell.msgTextViewTrailingConstraint.isActive = false + + let leftConstraint = messageTextView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: leftMargin) + + let rightConstraint = messageTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -rightMargin) + + NSLayoutConstraint.activate([ + leftConstraint, + rightConstraint + ]) + + cell.msgTextViewLeadingConstraint = leftConstraint + cell.msgTextViewTrailingConstraint = rightConstraint + + cell.msgTextViewBottomConstraint.constant += bottomMargin + } +}