| /* |
| * Copyright (C) 2017 Savoir-faire Linux Inc. |
| * |
| * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> |
| * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| import UIKit |
| import Reusable |
| import RxSwift |
| import ActiveLabel |
| import SwiftyBeaver |
| |
| class MessageCell: UITableViewCell, NibReusable { |
| |
| let log = SwiftyBeaver.self |
| |
| @IBOutlet weak var avatarView: UIView! |
| @IBOutlet weak var bubble: MessageBubble! |
| @IBOutlet weak var bubbleBottomConstraint: NSLayoutConstraint! |
| @IBOutlet weak var bubbleTopConstraint: NSLayoutConstraint! |
| @IBOutlet weak var messageLabelMarginConstraint: NSLayoutConstraint! |
| @IBOutlet weak var avatarBotomAlignConstraint: NSLayoutConstraint! |
| @IBOutlet weak var messageLabel: ActiveLabel! |
| @IBOutlet weak var sizeLabel: UILabel! |
| @IBOutlet weak var statusLabel: UILabel! |
| @IBOutlet weak var progressBar: UIProgressView! |
| @IBOutlet weak var acceptButton: UIButton? |
| @IBOutlet weak var cancelButton: UIButton! |
| @IBOutlet weak var buttonsHeightConstraint: NSLayoutConstraint? |
| @IBOutlet weak var bottomCorner: UIView! |
| @IBOutlet weak var topCorner: UIView! |
| @IBOutlet weak var timeLabel: UILabel! |
| @IBOutlet weak var leftDivider: UIView! |
| @IBOutlet weak var rightDivider: UIView! |
| @IBOutlet weak var sendingIndicator: UIActivityIndicatorView! |
| @IBOutlet weak var failedStatusLabel: UILabel! |
| @IBOutlet weak var bubbleViewMask: UIView? |
| |
| private var transferImageView = UIImageView() |
| |
| var dataTransferProgressUpdater: Timer? |
| |
| var disposeBag = DisposeBag() |
| |
| |
| override func prepareForReuse() { |
| super.prepareForReuse() |
| self.stopProgressMonitor() |
| self.disposeBag = DisposeBag() |
| } |
| |
| func startProgressMonitor(_ item: MessageViewModel, |
| _ conversationViewModel: ConversationViewModel) { |
| if self.dataTransferProgressUpdater != nil { |
| self.stopProgressMonitor() |
| return |
| } |
| guard let transferId = item.daemonId else { return } |
| self.dataTransferProgressUpdater = Timer.scheduledTimer(timeInterval: 0.5, |
| target: self, |
| selector: #selector(self.updateProgressBar), |
| userInfo: ["transferId": transferId, |
| "conversationViewModel": conversationViewModel], |
| repeats: true) |
| } |
| |
| func stopProgressMonitor() { |
| guard let updater = self.dataTransferProgressUpdater else { return } |
| updater.invalidate() |
| self.dataTransferProgressUpdater = nil |
| } |
| |
| @objc func updateProgressBar(timer: Timer) { |
| guard let userInfoDict = timer.userInfo as? NSDictionary else { return } |
| guard let transferId = userInfoDict["transferId"] as? UInt64 else { return } |
| guard let viewModel = userInfoDict["conversationViewModel"] as? ConversationViewModel else { return } |
| if let progress = viewModel.getTransferProgress(transferId: transferId) { |
| DispatchQueue.main.async { |
| self.progressBar.progress = progress |
| } |
| } |
| } |
| |
| func showCopyMenu() { |
| becomeFirstResponder() |
| let menu = UIMenuController.shared |
| if !menu.isMenuVisible { |
| menu.setTargetRect(self.bubble.frame, in: self) |
| menu.setMenuVisible(true, animated: true) |
| } |
| } |
| |
| func setup() { |
| let longGestureRecognizer = UILongPressGestureRecognizer() |
| self.messageLabel.isUserInteractionEnabled = true |
| self.messageLabel.addGestureRecognizer(longGestureRecognizer) |
| longGestureRecognizer.rx.event.bind(onNext: { [weak self] _ in |
| self?.showCopyMenu() |
| }).disposed(by: self.disposeBag) |
| } |
| |
| override func copy(_ sender: Any?) { |
| UIPasteboard.general.string = self.messageLabel.text |
| UIMenuController.shared.setMenuVisible(false, animated: true) |
| } |
| |
| override var canBecomeFirstResponder: Bool { |
| return true |
| } |
| |
| override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { |
| if action == #selector(UIResponderStandardEditActions.copy) { |
| return true |
| } |
| return false |
| } |
| |
| func formatCellTimeLabel(_ item: MessageViewModel) { |
| // hide for potentially reused cell |
| self.timeLabel.isHidden = true |
| self.leftDivider.isHidden = true |
| self.rightDivider.isHidden = true |
| |
| if item.timeStringShown == nil { |
| return |
| } |
| |
| // setup the label |
| self.timeLabel.text = item.timeStringShown |
| self.timeLabel.textColor = UIColor.ringMsgCellTimeText |
| self.timeLabel.font = UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.medium) |
| |
| // show the time |
| self.timeLabel.isHidden = false |
| self.leftDivider.isHidden = false |
| self.rightDivider.isHidden = false |
| } |
| |
| // swiftlint:disable cyclomatic_complexity |
| func applyBubbleStyleToCell(_ items: [MessageViewModel]?, cellForRowAt indexPath: IndexPath) { |
| guard let item = items?[indexPath.row] else { |
| return |
| } |
| |
| let type = item.bubblePosition() |
| var bubbleColor: UIColor |
| if item.isTransfer { |
| if item.content.containsOnlyEmoji { |
| bubbleColor = UIColor.ringMsgCellEmoji |
| } else { |
| bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor(hex: 0xcfebf5, alpha: 1.0) |
| } |
| } else { |
| if item.content.containsOnlyEmoji { |
| bubbleColor = UIColor.ringMsgCellEmoji |
| } else { |
| bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent |
| } |
| } |
| |
| if item.isTransfer { |
| self.messageLabel.enabledTypes = [] |
| let contentArr = item.content.components(separatedBy: "\n") |
| if contentArr.count > 1 { |
| self.messageLabel.text = contentArr[0] |
| self.sizeLabel.text = contentArr[1] |
| } else { |
| self.messageLabel.text = item.content |
| } |
| } else { |
| self.messageLabel.enabledTypes = [.url] |
| self.setup() |
| self.messageLabel.setTextWithLineSpacing(withText: item.content, withLineSpacing: 2) |
| self.messageLabel.handleURLTap { url in |
| let urlString = url.absoluteString |
| if let prefixedUrl = URL(string: urlString.contains("http") ? urlString : "http://\(urlString)") { |
| UIApplication.shared.openURL(prefixedUrl) |
| } |
| } |
| } |
| |
| self.topCorner.isHidden = true |
| self.topCorner.backgroundColor = bubbleColor |
| self.bottomCorner.isHidden = true |
| self.bottomCorner.backgroundColor = bubbleColor |
| self.bubbleBottomConstraint.constant = 8 |
| self.bubbleTopConstraint.constant = 8 |
| |
| var adjustedSequencing = item.sequencing |
| |
| if item.timeStringShown != nil { |
| self.bubbleTopConstraint.constant = 32 |
| adjustedSequencing = indexPath.row == (items?.count)! - 1 ? |
| .singleMessage : adjustedSequencing != .singleMessage && adjustedSequencing != .lastOfSequence ? |
| .firstOfSequence : .singleMessage |
| } |
| |
| if indexPath.row + 1 < (items?.count)! { |
| if items?[indexPath.row + 1].timeStringShown != nil { |
| switch adjustedSequencing { |
| case .firstOfSequence: |
| adjustedSequencing = .singleMessage |
| case .middleOfSequence: |
| adjustedSequencing = .lastOfSequence |
| default: break |
| } |
| } |
| } |
| |
| item.sequencing = adjustedSequencing |
| |
| switch item.sequencing { |
| case .middleOfSequence: |
| self.topCorner.isHidden = item.isTransfer |
| self.bottomCorner.isHidden = item.isTransfer |
| self.bubbleBottomConstraint.constant = 1 |
| self.bubbleTopConstraint.constant = item.timeStringShown != nil ? 32 : 1 |
| case .firstOfSequence: |
| self.bottomCorner.isHidden = item.isTransfer |
| self.bubbleBottomConstraint.constant = 1 |
| self.bubbleTopConstraint.constant = item.timeStringShown != nil ? 32 : 8 |
| case .lastOfSequence: |
| self.topCorner.isHidden = item.isTransfer |
| self.bubbleTopConstraint.constant = item.timeStringShown != nil ? 32 : 1 |
| default: break |
| } |
| if item.content.containsOnlyEmoji { |
| self.messageLabel.font = UIFont.systemFont(ofSize: 40.0, weight: UIFont.Weight.medium) |
| } else { |
| self.messageLabel.font = UIFont.systemFont(ofSize: 16.0, weight: UIFont.Weight.medium) |
| } |
| } |
| |
| /// swiftlint:disable function_body_length |
| func configureFromItem(_ conversationViewModel: ConversationViewModel, |
| _ items: [MessageViewModel]?, |
| cellForRowAt indexPath: IndexPath) { |
| |
| self.backgroundColor = UIColor.clear |
| self.bubbleViewMask?.backgroundColor = UIColor.ringMsgBackground |
| self.transferImageView.backgroundColor = UIColor.ringMsgBackground |
| buttonsHeightConstraint?.priority = UILayoutPriority(rawValue: 999.0) |
| guard let item = items?[indexPath.row] else { |
| return |
| } |
| |
| self.transferImageView.removeFromSuperview() |
| self.bubbleViewMask?.isHidden = true |
| |
| // hide/show time label |
| self.formatCellTimeLabel(item) |
| |
| if item.bubblePosition() == .generated { |
| self.bubble.backgroundColor = UIColor.ringMsgCellReceived |
| self.messageLabel.setTextWithLineSpacing(withText: item.content, withLineSpacing: 2) |
| if indexPath.row == 0 { |
| self.messageLabelMarginConstraint.constant = 4 |
| self.bubbleTopConstraint.constant = 36 |
| } else { |
| self.messageLabelMarginConstraint.constant = -2 |
| self.bubbleTopConstraint.constant = 32 |
| } |
| return |
| } else if item.isTransfer { |
| self.messageLabel.lineBreakMode = .byTruncatingMiddle |
| let type = item.bubblePosition() |
| self.bubble.backgroundColor = type == .received ? UIColor.ringMsgCellReceived : UIColor(hex: 0xcfebf5, alpha: 1.0) |
| if indexPath.row == 0 { |
| self.messageLabelMarginConstraint.constant = 4 |
| self.bubbleTopConstraint.constant = 36 |
| } else { |
| self.messageLabelMarginConstraint.constant = -2 |
| self.bubbleTopConstraint.constant = 32 |
| } |
| if item.bubblePosition() == .received { |
| self.acceptButton?.tintColor = UIColor(hex: 0x00b20b, alpha: 1.0) |
| self.cancelButton.tintColor = UIColor(hex: 0xf00000, alpha: 1.0) |
| self.progressBar.tintColor = UIColor.ringMain |
| } else if item.bubblePosition() == .sent { |
| self.cancelButton.tintColor = UIColor(hex: 0xf00000, alpha: 1.0) |
| self.progressBar.tintColor = UIColor.ringMain.lighten(byPercentage: 0.2) |
| } |
| |
| if item.shouldDisplayTransferedImage { |
| self.displayTransferedImage(message: item, conversationID: conversationViewModel.conversation.value.conversationId) |
| } |
| } |
| |
| // bubble grouping for cell |
| self.applyBubbleStyleToCell(items, cellForRowAt: indexPath) |
| |
| // special cases where top/bottom margins should be larger |
| if indexPath.row == 0 { |
| self.messageLabelMarginConstraint.constant = 4 |
| self.bubbleTopConstraint.constant = 36 |
| } else if items?.count == indexPath.row + 1 { |
| self.bubbleBottomConstraint.constant = 16 |
| } |
| |
| if item.bubblePosition() == .sent { |
| // When the message contains only emoji |
| if item.content.containsOnlyEmoji { |
| self.bubble.backgroundColor = UIColor.ringMsgCellEmoji |
| } else { |
| self.bubble.backgroundColor = UIColor.ringMsgCellSent |
| } |
| if item.isTransfer { |
| // outgoing transfer |
| } else { |
| // sent message status |
| item.status.asObservable() |
| .observeOn(MainScheduler.instance) |
| .map { value in value == MessageStatus.sending ? true : false } |
| .bind(to: self.sendingIndicator.rx.isAnimating) |
| .disposed(by: self.disposeBag) |
| item.status.asObservable() |
| .observeOn(MainScheduler.instance) |
| .map { value in value == MessageStatus.failure ? false : true } |
| .bind(to: self.failedStatusLabel.rx.isHidden) |
| .disposed(by: self.disposeBag) |
| } |
| } else if item.bubblePosition() == .received { |
| // When the message contains only emoji |
| if item.content.containsOnlyEmoji { |
| self.bubble.backgroundColor = UIColor.ringMsgCellEmoji |
| if (self.avatarBotomAlignConstraint != nil) { |
| self.avatarBotomAlignConstraint.constant = -14 |
| } |
| } else { |
| self.bubble.backgroundColor = UIColor.ringMsgCellReceived |
| if (self.avatarBotomAlignConstraint != nil) { |
| self.avatarBotomAlignConstraint.constant = -1 |
| } |
| } |
| // received message avatar |
| Observable<(Data?, String)>.combineLatest(conversationViewModel.profileImageData.asObservable(), |
| conversationViewModel.userName.asObservable(), |
| conversationViewModel.displayName.asObservable()) { profileImage, username, displayName in |
| if let displayName = displayName, !displayName.isEmpty { |
| return (profileImage, displayName) |
| } |
| return (profileImage, username) |
| } |
| .observeOn(MainScheduler.instance) |
| .startWith((conversationViewModel.profileImageData.value, conversationViewModel.userName.value)) |
| .subscribe({ [weak self] profileData -> Void in |
| self?.avatarView.subviews.forEach({ $0.removeFromSuperview() }) |
| self?.avatarView.addSubview(AvatarView(profileImageData: profileData.element?.0, |
| username: (profileData.element?.1)!, |
| size: 32)) |
| self?.avatarView.isHidden = !(item.sequencing == .lastOfSequence || item.sequencing == .singleMessage) |
| return |
| }) |
| .disposed(by: self.disposeBag) |
| } |
| } |
| |
| // swiftlint:enable function_body_length |
| |
| func displayTransferedImage(message: MessageViewModel, conversationID: String) { |
| let screenWidth = UIScreen.main.bounds.width |
| var maxDimsion: CGFloat = 250 |
| //iPhone 5 width |
| if screenWidth <= 320 { |
| maxDimsion = 200 |
| //iPhone 6, iPhone 6 Plus and iPhone XR width |
| } else if screenWidth > 320 && screenWidth <= 414 { |
| maxDimsion = 250 |
| //iPad width |
| } else if screenWidth > 414 { |
| maxDimsion = 300 |
| } |
| let defaultSize = CGSize(width: maxDimsion, height: maxDimsion) |
| if let image = message.getTransferedImage(maxSize: maxDimsion, conversationID: conversationID) { |
| self.transferImageView.image = image |
| let newSize = self.transferImageView.image?.getNewSize(of: defaultSize) |
| let xOriginImageSend = self.bubble.bounds.size.width - (newSize?.width)! |
| if message.bubblePosition() == .sent { |
| self.transferImageView.frame = CGRect(x: xOriginImageSend, y: 0, width: ((newSize?.width ?? 200)), height: ((newSize?.height ?? 200))) |
| } else if message.bubblePosition() == .received { |
| self.transferImageView.frame = CGRect(x: 0, y: 0, width: ((newSize?.width ?? 200)), height: ((newSize?.height ?? 200))) |
| } |
| self.transferImageView.layer.cornerRadius = 20 |
| self.transferImageView.layer.masksToBounds = true |
| self.transferImageView.contentMode = .scaleAspectFill |
| buttonsHeightConstraint?.priority = UILayoutPriority(rawValue: 250.0) |
| self.bubble.addSubview(self.transferImageView) |
| self.bubbleViewMask?.isHidden = false |
| self.bottomCorner.isHidden = true |
| self.topCorner.isHidden = true |
| self.transferImageView.translatesAutoresizingMaskIntoConstraints = true |
| self.transferImageView.topAnchor.constraint(equalTo: self.bubble.topAnchor, constant: 0).isActive = true |
| self.transferImageView.bottomAnchor.constraint(equalTo: self.bubble.bottomAnchor, constant: 0).isActive = true |
| } |
| } |
| // swiftlint:enable cyclomatic_complexity |
| |
| } |