ConversationViewController.swift 30.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*
 *  Copyright (C) 2017 Savoir-faire Linux Inc.
 *
 *  Author: Silbino Gonçalves Matado <silbino.gmatado@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 RxSwift
23
import Reusable
24 25
import SwiftyBeaver

26 27 28 29 30 31 32 33 34
extension UITextField {
    func setPadding(_ left: CGFloat, _ right: CGFloat) {
        self.leftView = UIView(frame: CGRect(x: 0, y: 0, width: left, height: self.frame.size.height))
        self.rightView = UIView(frame: CGRect(x: 0, y: 0, width: right, height: self.frame.size.height))
        self.leftViewMode = .always
        self.rightViewMode = .always
    }
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
35
// swiftlint:disable type_body_length
36 37
class ConversationViewController: UIViewController, UITextFieldDelegate, StoryboardBased, ViewModelBased {

38 39
    let log = SwiftyBeaver.self

40 41
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var spinnerView: UIView!
42 43 44

    let disposeBag = DisposeBag()

45
    var viewModel: ConversationViewModel!
Thibault Wittemberg's avatar
Thibault Wittemberg committed
46
    var messageViewModels: [MessageViewModel]?
47
    var textFieldShouldEndEditing = false
48
    var bottomOffset: CGFloat = 0
49
    let scrollOffsetThreshold: CGFloat = 600
50

51
    fileprivate var fallbackBGColorObservable: Observable<UIColor>!
52

53 54 55 56 57 58 59 60 61
    override func viewDidLoad() {
        super.viewDidLoad()

        self.setupUI()
        self.setupTableView()
        self.setupBindings()

        self.messageAccessoryView.messageTextField.delegate = self

62 63
        self.messageAccessoryView.messageTextField.setPadding(8.0, 8.0)

64 65 66 67
        /*
         Register to keyboard notifications to adjust tableView insets when the keybaord appears
         or disappears
         */
68
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(withNotification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
69 70 71
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(withNotification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }

72
    @objc func keyboardWillShow(withNotification notification: Notification) {
73 74

        let userInfo: Dictionary = notification.userInfo!
75 76
        guard let keyboardFrame: NSValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }

77 78 79 80 81 82
        let keyboardRectangle = keyboardFrame.cgRectValue
        let keyboardHeight = keyboardRectangle.height

        self.tableView.contentInset.bottom = keyboardHeight
        self.tableView.scrollIndicatorInsets.bottom = keyboardHeight

83
        self.scrollToBottom(animated: false)
84 85 86
        self.updateBottomOffset()
    }

87
    @objc func keyboardWillHide(withNotification notification: Notification) {
88 89 90 91 92
        self.tableView.contentInset.bottom = 0
        self.tableView.scrollIndicatorInsets.bottom = 0
        self.updateBottomOffset()
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
93
    func setupNavTitle(profileImageData: Data?, displayName: String? = nil, username: String?) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
        let imageSize       = CGFloat(36.0)
        let imageOffsetY    = CGFloat(5.0)
        let infoPadding     = CGFloat(8.0)
        let maxNameLength   = CGFloat(128.0)
        var userNameYOffset = CGFloat(9.0)
        var nameSize        = CGFloat(18.0)
        let navbarFrame     = self.navigationController?.navigationBar.frame
        let totalHeight     = ((navbarFrame?.size.height ?? 0) + (navbarFrame?.origin.y ?? 0)) / 2

        // Replace "< Home" with a back arrow while we are crunching everything to the left side of the bar for now.
        self.navigationController?.navigationBar.backIndicatorImage = UIImage(named: "back_button")
        self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "back_button")
        self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil)

        let titleView: UIView = UIView.init(frame: CGRect(x: 0, y: 0, width: view.frame.width - 32, height: totalHeight))

        let profileImageView = UIImageView(frame: CGRect(x: 0, y: imageOffsetY, width: imageSize, height: imageSize))
        profileImageView.frame = CGRect.init(x: 0, y: 0, width: imageSize, height: imageSize)
        profileImageView.center = CGPoint.init(x: imageSize / 2, y: titleView.center.y)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
113 114

        if let imageData = profileImageData, let image = UIImage(data: imageData) {
Andreas Traczyk's avatar
Andreas Traczyk committed
115 116 117 118 119
            self.log.debug("standard avatar")
            (profileImageView as UIImageView).image = image.circleMasked
            titleView.addSubview(profileImageView)
        } else {
            // use fallback avatars
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
120
            let name = self.viewModel.userName.value
Andreas Traczyk's avatar
Andreas Traczyk committed
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
            let scanner = Scanner(string: name.toMD5HexString().prefixString())
            var index: UInt64 = 0
            if scanner.scanHexInt64(&index) {
                let fbaBGColor = avatarColors[Int(index)]
                let circle = UIView(frame: CGRect(x: 0.0, y: imageOffsetY, width: imageSize, height: imageSize))
                circle.center = CGPoint.init(x: imageSize / 2, y: titleView.center.y)
                circle.layer.cornerRadius = imageSize / 2
                circle.backgroundColor = fbaBGColor
                circle.clipsToBounds = true
                titleView.addSubview(circle)
                if self.viewModel.conversation.value.recipientRingId != name {
                    // use g-style fallback avatar
                    self.log.debug("fallback avatar")
                    let initialLabel: UILabel = UILabel.init(frame: CGRect.init(x: 0, y: imageOffsetY - 1, width: imageSize, height: imageSize))
                    initialLabel.center = circle.center
                    initialLabel.text = name.prefixString().capitalized
                    initialLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
                    initialLabel.textColor = UIColor.white
                    initialLabel.textAlignment = .center
                    titleView.addSubview(initialLabel)
                } else {
                    // ringId only, so fallback fallback avatar
                    self.log.debug("fallback fallback avatar")
                    if let image = UIImage(named: "fallback_avatar") {
                        (profileImageView as UIImageView).image = image.circleMasked
                        titleView.addSubview(profileImageView)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
147 148 149 150 151
                    }
                }
            }
        }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
152
        if let name = displayName, !name.isEmpty {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
153
            let dnlabel: UILabel = UILabel.init(frame: CGRect.init(x: imageSize + infoPadding, y: 4, width: maxNameLength, height: 20))
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
154
            dnlabel.text = name
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
            dnlabel.font = UIFont.systemFont(ofSize: nameSize)
            dnlabel.textColor = UIColor.white
            dnlabel.textAlignment = .left
            titleView.addSubview(dnlabel)
            userNameYOffset = 20.0
            nameSize = 14.0
        }

        let unlabel: UILabel = UILabel.init(frame: CGRect.init(x: imageSize + infoPadding, y: userNameYOffset, width: maxNameLength, height: 24))
        unlabel.text = username
        unlabel.font = UIFont.systemFont(ofSize: nameSize)
        unlabel.textColor = UIColor.white
        unlabel.textAlignment = .left
        titleView.addSubview(unlabel)

        self.navigationItem.titleView = titleView
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
171

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
172 173
    }

174
    func setupUI() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
175 176 177
        if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad {
            self.viewModel.userName.asObservable().bind(to: self.navigationItem.rx.title).disposed(by: disposeBag)
        } else {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
178 179 180 181 182 183 184 185
            self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value,
                               displayName: self.viewModel.displayName.value,
                               username: self.viewModel.userName.value)

            Observable<(Data?, String?, String)>.combineLatest(self.viewModel.profileImageData.asObservable(),
                                                               self.viewModel.displayName.asObservable(),
                                                               self.viewModel.userName.asObservable()) { profileImage, displayName, username in
                                                            return (profileImage, displayName, username)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
186 187
                }
                .observeOn(MainScheduler.instance)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
188 189 190 191
                .subscribe({ [weak self] profileData -> Void in
                    self?.setupNavTitle(profileImageData: profileData.element?.0,
                                        displayName: profileData.element?.1,
                                        username: profileData.element?.2)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
192 193 194 195
                    return
                })
                .disposed(by: self.disposeBag)
        }
196

197
        // UIColor that observes "best Id" prefix
198
        self.fallbackBGColorObservable = viewModel.userName.asObservable()
199 200 201 202 203 204 205 206 207 208
            .observeOn(MainScheduler.instance)
            .map { name in
                let scanner = Scanner(string: name.toMD5HexString().prefixString())
                var index: UInt64 = 0
                if scanner.scanHexInt64(&index) {
                    return avatarColors[Int(index)]
                }
                return defaultAvatarColor
            }

209 210
        self.tableView.contentInset.bottom = messageAccessoryView.frame.size.height
        self.tableView.scrollIndicatorInsets.bottom = messageAccessoryView.frame.size.height
211

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
212
        //set navigation buttons - call and send contact request
213 214
        let inviteItem = UIBarButtonItem()
        inviteItem.image = UIImage(named: "add_person")
215 216
        inviteItem.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
            .subscribe(onNext: { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
                self.inviteItemTapped()
            })
            .disposed(by: self.disposeBag)

        self.viewModel.inviteButtonIsAvailable.asObservable()
            .bind(to: inviteItem.rx.isEnabled)
            .disposed(by: disposeBag)

        // call button
        let audioCallItem = UIBarButtonItem()
        audioCallItem.image = UIImage(asset: Asset.callButton)
        audioCallItem.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
            .subscribe(onNext: { [unowned self] in
                self.placeAudioOnlyCall()
            })
            .disposed(by: self.disposeBag)

        let videoCallItem = UIBarButtonItem()
        videoCallItem.image = UIImage(asset: Asset.videoRunning)
        videoCallItem.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
237 238 239
            .subscribe(onNext: { [unowned self] in
                self.placeCall()
            }).disposed(by: self.disposeBag)
240 241 242 243 244 245 246 247 248

        //block contact button
        let blockItem = UIBarButtonItem()
        blockItem.image = UIImage(named: "block_icon")
        blockItem.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
            .subscribe(onNext: { [unowned self] in
                self.blockItemTapped()
            }).disposed(by: self.disposeBag)

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
249 250
        // Items are from right to left
        self.navigationItem.rightBarButtonItems = [blockItem, videoCallItem, audioCallItem, inviteItem]
251 252 253 254 255 256 257 258 259

        Observable<[UIBarButtonItem]>
            .combineLatest(self.viewModel.inviteButtonIsAvailable.asObservable(),
                           self.viewModel.blockButtonIsAvailable.asObservable(),
                           resultSelector: { inviteButton, blockButton in
                            var buttons = [UIBarButtonItem]()
                            if blockButton {
                                buttons.append(blockItem)
                            }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
260 261
                            buttons.append(videoCallItem)
                            buttons.append(audioCallItem)
262 263 264 265 266 267 268 269 270
                            if inviteButton {
                                buttons.append(inviteItem)
                            }
                            return buttons
            })
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] buttons in
                self?.navigationItem.rightBarButtonItems = buttons
            }).disposed(by: self.disposeBag)
271 272 273 274
    }

    func inviteItemTapped() {
       self.viewModel?.sendContactRequest()
275 276
    }

277 278 279 280 281 282 283 284 285 286 287
    func blockItemTapped() {
        let alert = UIAlertController(title: L10n.Alerts.confirmBlockContactTitle, message: L10n.Alerts.confirmBlockContact, preferredStyle: .alert)
        let blockAction = UIAlertAction(title: L10n.Actions.blockAction, style: .destructive) { (_: UIAlertAction!) -> Void in
            self.viewModel.block()
        }
        let cancelAction = UIAlertAction(title: L10n.Actions.cancelAction, style: .default) { (_: UIAlertAction!) -> Void in }
        alert.addAction(blockAction)
        alert.addAction(cancelAction)
        self.present(alert, animated: true, completion: nil)
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
288 289 290 291
    func placeCall() {
        self.viewModel.startCall()
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
292 293 294 295
    func placeAudioOnlyCall() {
        self.viewModel.startAudioCall()
    }

296 297 298 299
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        self.scrollToBottom(animated: false)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
300
        self.textFieldShouldEndEditing = false
301 302 303 304 305 306 307
        self.messagesLoadingFinished()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        self.textFieldShouldEndEditing = true
308
        self.viewModel.setMessagesAsRead()
309 310 311
    }

    func setupTableView() {
Thibault Wittemberg's avatar
Thibault Wittemberg committed
312 313
        self.tableView.dataSource = self

314 315 316 317 318
        self.tableView.estimatedRowHeight = 50
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.separatorStyle = .none

        //Register cell
Thibault Wittemberg's avatar
Thibault Wittemberg committed
319 320
        self.tableView.register(cellType: MessageCellSent.self)
        self.tableView.register(cellType: MessageCellReceived.self)
321
        self.tableView.register(cellType: MessageCellGenerated.self)
322 323

        //Bind the TableView to the ViewModel
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
324
        self.viewModel.messages.asObservable().subscribe(onNext: { [weak self] (messageViewModels) in
Thibault Wittemberg's avatar
Thibault Wittemberg committed
325
            self?.messageViewModels = messageViewModels
326
            self?.computeSequencing()
Thibault Wittemberg's avatar
Thibault Wittemberg committed
327 328
            self?.tableView.reloadData()
        }).disposed(by: self.disposeBag)
329 330

        //Scroll to bottom when reloaded
331
        self.tableView.rx.methodInvoked(#selector(UITableView.reloadData)).subscribe(onNext: { [unowned self] _ in
332 333
            self.scrollToBottomIfNeed()
            self.updateBottomOffset()
334
        }).disposed(by: disposeBag)
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    }

    fileprivate func updateBottomOffset() {
        self.bottomOffset = self.tableView.contentSize.height
            - ( self.tableView.frame.size.height
                - self.tableView.contentInset.top
                - self.tableView.contentInset.bottom )
    }

    fileprivate func messagesLoadingFinished() {
        self.spinnerView.isHidden = true
    }

    fileprivate func scrollToBottomIfNeed() {
        if self.isBottomContentOffset {
350
            self.scrollToBottom(animated: false)
351 352 353 354
        }
    }

    fileprivate func scrollToBottom(animated: Bool) {
355 356 357 358 359
        let numberOfRows = self.tableView.numberOfRows(inSection: 0)
        if  numberOfRows > 0 {
            let last = IndexPath(row: numberOfRows - 1, section: 0)
            self.tableView.scrollToRow(at: last, at: .bottom, animated: animated)
        }
360 361 362
    }

    fileprivate var isBottomContentOffset: Bool {
363 364 365
        updateBottomOffset()
        let offset = abs((self.tableView.contentOffset.y + self.tableView.contentInset.top) - bottomOffset)
        return offset <= scrollOffsetThreshold
366 367 368 369 370 371 372 373 374 375 376
    }

    override var inputAccessoryView: UIView {
        return self.messageAccessoryView
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    lazy var messageAccessoryView: MessageAccessoryView = {
377
        return MessageAccessoryView.loadFromNib()
378 379 380 381 382
    }()

    func setupBindings() {

        //Binds the keyboard Send button action to the ViewModel
383 384
        self.messageAccessoryView.messageTextField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { [unowned self] _ in
            self.viewModel.sendMessage(withContent: self.messageAccessoryView.messageTextField.text!)
385
            self.messageAccessoryView.messageTextField.text = ""
386
        }).disposed(by: disposeBag)
387 388 389 390 391 392 393
    }

    // Avoid the keyboard to be hidden when the Send button is touched
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        return textFieldShouldEndEditing
    }

394 395 396 397 398
    func computeSequencing() {
        var lastShownTime: Date?
        for (index, messageViewModel) in self.messageViewModels!.enumerated() {
            // time labels
            let time = messageViewModel.receivedDate
399
            if index == 0 ||  messageViewModel.bubblePosition() == .generated {
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
                // always show first message's time
                messageViewModel.timeStringShown = getTimeLabelString(forTime: time)
                lastShownTime = time
            } else {
                // only show time for new messages if beyond an arbitrary time frame (1 minute)
                // from the previously shown time
                let hourComp = Calendar.current.compare(lastShownTime!, to: time, toGranularity: .hour)
                let minuteComp = Calendar.current.compare(lastShownTime!, to: time, toGranularity: .minute)
                if hourComp == .orderedSame && minuteComp == .orderedSame {
                    messageViewModel.timeStringShown = nil
                } else {
                    messageViewModel.timeStringShown = getTimeLabelString(forTime: time)
                    lastShownTime = time
                }
            }
            // sequencing
            messageViewModel.sequencing = getMessageSequencing(forIndex: index)
        }
    }

    func getTimeLabelString(forTime time: Date) -> String {
        // get the current time
        let currentDateTime = Date()

        // prepare formatter
        let dateFormatter = DateFormatter()
        if Calendar.current.compare(currentDateTime, to: time, toGranularity: .year) == .orderedSame {
            if Calendar.current.compare(currentDateTime, to: time, toGranularity: .weekOfYear) == .orderedSame {
                if Calendar.current.compare(currentDateTime, to: time, toGranularity: .day) == .orderedSame {
                    // age: [0, received the previous day[
                    dateFormatter.dateFormat = "h:mma"
                } else {
                    // age: [received the previous day, received 7 days ago[
                    dateFormatter.dateFormat = "E h:mma"
                }
            } else {
                // age: [received 7 days ago, received the previous year[
437
                dateFormatter.dateFormat = "MMM d, h:mma"
438 439 440
            }
        } else {
            // age: [received the previous year, inf[
441
            dateFormatter.dateFormat = "MMM d, yyyy h:mma"
442 443 444 445
        }

        // generate the string containing the message time
        return dateFormatter.string(from: time).uppercased()
446 447
    }

448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
    func formatTimeLabel(forCell cell: MessageCell,
                         withMessageVM messageVM: MessageViewModel) {
        // hide for potentially reused cell
        cell.timeLabel.isHidden = true
        cell.leftDivider.isHidden = true
        cell.rightDivider.isHidden = true

        if messageVM.timeStringShown == nil {
            return
        }

        // setup the label
        cell.timeLabel.text = messageVM.timeStringShown
        cell.timeLabel.textColor = UIColor.ringMsgCellTimeText
        cell.timeLabel.font = UIFont.boldSystemFont(ofSize: 14.0)

        // show the time
        cell.timeLabel.isHidden = false
        cell.leftDivider.isHidden = false
        cell.rightDivider.isHidden = false
468 469
    }

470 471
    func getMessageSequencing(forIndex index: Int) -> MessageSequencing {
        if let msgViewModel = self.messageViewModels?[index] {
472
            let msgOwner = msgViewModel.bubblePosition()
473 474 475
            if self.messageViewModels?.count == 1 || index == 0 {
                if self.messageViewModels?.count == index + 1 {
                    return MessageSequencing.singleMessage
476
                }
477 478
                let nextMsgViewModel = index + 1 <= (self.messageViewModels?.count)!
                    ? self.messageViewModels?[index + 1] : nil
479 480
                if nextMsgViewModel != nil {
                    return msgOwner != nextMsgViewModel?.bubblePosition()
481
                        ? MessageSequencing.singleMessage : MessageSequencing.firstOfSequence
482
                }
483 484 485
            } else if self.messageViewModels?.count == index + 1 {
                let lastMsgViewModel = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
                    ? self.messageViewModels?[index - 1] : nil
486 487
                if lastMsgViewModel != nil {
                    return msgOwner != lastMsgViewModel?.bubblePosition()
488
                        ? MessageSequencing.singleMessage : MessageSequencing.lastOfSequence
489 490
                }
            }
491 492 493 494 495
            let lastMsgViewModel = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
                ? self.messageViewModels?[index - 1] : nil
            let nextMsgViewModel = index + 1 <= (self.messageViewModels?.count)!
                ? self.messageViewModels?[index + 1] : nil
            var sequencing = MessageSequencing.singleMessage
496 497
            if (lastMsgViewModel != nil) && (nextMsgViewModel != nil) {
                if msgOwner != lastMsgViewModel?.bubblePosition() && msgOwner == nextMsgViewModel?.bubblePosition() {
498
                    sequencing = MessageSequencing.firstOfSequence
499
                } else if msgOwner != nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
500
                    sequencing = MessageSequencing.lastOfSequence
501
                } else if msgOwner == nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
502
                    sequencing = MessageSequencing.middleOfSequence
503 504
                }
            }
505
            return sequencing
506
        }
507
        return MessageSequencing.unknown
508 509
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
510
    // swiftlint:disable cyclomatic_complexity
511
    func applyBubbleStyleToCell(toCell cell: MessageCell,
512 513 514
                                cellForRowAt indexPath: IndexPath,
                                withMessageVM messageVM: MessageViewModel) {
        let type = messageVM.bubblePosition()
515
        let bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent
516
        cell.setup()
517

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
518
        cell.messageLabel.enabledTypes = [.url]
519
        cell.messageLabel.setTextWithLineSpacing(withText: messageVM.content, withLineSpacing: 2)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
520
        cell.messageLabel.handleURLTap { url in
521 522 523 524
            let urlString = url.absoluteString
            if let prefixedUrl = URL(string: urlString.contains("http") ? urlString : "http://\(urlString)") {
                UIApplication.shared.openURL(prefixedUrl)
            }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
525
        }
526 527 528 529 530 531 532 533

        cell.topCorner.isHidden = true
        cell.topCorner.backgroundColor = bubbleColor
        cell.bottomCorner.isHidden = true
        cell.bottomCorner.backgroundColor = bubbleColor
        cell.bubbleBottomConstraint.constant = 8
        cell.bubbleTopConstraint.constant = 8

534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
        var adjustedSequencing = messageVM.sequencing

        if messageVM.timeStringShown != nil {
            cell.bubbleTopConstraint.constant = 32
            adjustedSequencing = indexPath.row == (self.messageViewModels?.count)! - 1 ?
                .singleMessage : adjustedSequencing != .singleMessage && adjustedSequencing != .lastOfSequence ?
                    .firstOfSequence : .singleMessage
        }

        if indexPath.row + 1 < (self.messageViewModels?.count)! {
            if self.messageViewModels?[indexPath.row + 1].timeStringShown != nil {
                switch adjustedSequencing {
                case .firstOfSequence:
                    adjustedSequencing = .singleMessage
                case .middleOfSequence:
                    adjustedSequencing = .lastOfSequence
                default: break
                }
            }
        }

555 556 557
        messageVM.sequencing = adjustedSequencing

        switch messageVM.sequencing {
558 559 560 561
        case .middleOfSequence:
            cell.topCorner.isHidden = false
            cell.bottomCorner.isHidden = false
            cell.bubbleBottomConstraint.constant = 1
562
            cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 1
563 564 565
        case .firstOfSequence:
            cell.bottomCorner.isHidden = false
            cell.bubbleBottomConstraint.constant = 1
566
            cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 8
567 568
        case .lastOfSequence:
            cell.topCorner.isHidden = false
569
            cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 1
570 571
        default: break
        }
572 573

    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
574
    // swiftlint:enable cyclomatic_complexity
575

576
    // swiftlint:disable cyclomatic_complexity
577 578 579
    func formatCell(withCell cell: MessageCell,
                    cellForRowAt indexPath: IndexPath,
                    withMessageVM messageVM: MessageViewModel) {
580

581 582 583
        // hide/show time label
        formatTimeLabel(forCell: cell, withMessageVM: messageVM)

584 585 586 587 588 589 590 591
        if messageVM.bubblePosition() == .generated {
            cell.bubble.backgroundColor = UIColor.ringMsgCellReceived
            cell.messageLabel.setTextWithLineSpacing(withText: messageVM.content, withLineSpacing: 2)
            // generated messages should always show the time
            cell.bubbleTopConstraint.constant = 32
            return
        }

592 593 594 595 596 597 598 599 600
        // bubble grouping for cell
        applyBubbleStyleToCell(toCell: cell, cellForRowAt: indexPath, withMessageVM: messageVM)

        // special cases where top/bottom margins should be larger
        if indexPath.row == 0 {
            cell.bubbleTopConstraint.constant = 32
        } else if self.messageViewModels?.count == indexPath.row + 1 {
            cell.bubbleBottomConstraint.constant = 16
        }
601

602
        if messageVM.bubblePosition() == .sent {
603 604 605 606
            messageVM.status.asObservable()
                .observeOn(MainScheduler.instance)
                .map { value in value == MessageStatus.sending ? true : false }
                .bind(to: cell.sendingIndicator.rx.isAnimating)
607
                .disposed(by: cell.disposeBag)
608 609 610 611
            messageVM.status.asObservable()
                .observeOn(MainScheduler.instance)
                .map { value in value == MessageStatus.failure ? false : true }
                .bind(to: cell.failedStatusLabel.rx.isHidden)
612
                .disposed(by: cell.disposeBag)
613
        } else if messageVM.bubblePosition() == .received {
614 615 616 617 618 619 620 621 622 623
            // avatar
            guard let fallbackAvatar = cell.fallbackAvatar else {
                return
            }

            fallbackAvatar.isHidden = true
            cell.profileImage?.isHidden = true
            if messageVM.sequencing == .lastOfSequence || messageVM.sequencing == .singleMessage {
                cell.profileImage?.isHidden = false

624 625
                // Set placeholder avatar
                fallbackAvatar.text = nil
626
                cell.fallbackAvatarImage.isHidden = true
627 628 629 630 631 632
                let name = viewModel.userName.value
                let scanner = Scanner(string: name.toMD5HexString().prefixString())
                var index: UInt64 = 0
                if scanner.scanHexInt64(&index) {
                    fallbackAvatar.isHidden = false
                    fallbackAvatar.backgroundColor = avatarColors[Int(index)]
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
633
                    if viewModel.conversation.value.recipientRingId != name {
634
                        fallbackAvatar.text = name.prefixString().capitalized
635 636
                    } else {
                        cell.fallbackAvatarImage.isHidden = true
637 638 639 640 641 642 643 644 645 646
                    }
                }

                // Observe in case of a lookup
                self.fallbackBGColorObservable
                    .subscribe(onNext: { [weak fallbackAvatar] backgroundColor in
                        fallbackAvatar?.backgroundColor = backgroundColor
                    })
                    .disposed(by: cell.disposeBag)

647 648 649
                // Avatar placeholder initial
                viewModel.userName.asObservable()
                    .observeOn(MainScheduler.instance)
650
                    .filter({ [weak self] userName in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
651
                        return userName != self?.viewModel.conversation.value.recipientRingId
652
                    })
653 654
                    .map { value in value.prefixString().capitalized }
                    .bind(to: fallbackAvatar.rx.text)
655
                    .disposed(by: cell.disposeBag)
656

657 658 659 660 661 662
                viewModel.userName.asObservable()
                    .observeOn(MainScheduler.instance)
                    .map { [weak self] userName in userName != self?.viewModel.conversation.value.recipientRingId }
                    .bind(to: cell.fallbackAvatarImage.rx.isHidden)
                    .disposed(by: cell.disposeBag)

663 664
                // Set image if any
                cell.profileImage?.image = nil
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
665 666 667 668 669 670 671 672 673 674 675 676 677
                self.viewModel.profileImageData.asObservable()
                    .observeOn(MainScheduler.instance)
                    .subscribe(onNext: { data in
                        if let imageData = data {
                            if let image = UIImage(data: imageData) {
                                cell.profileImage?.image = image
                                fallbackAvatar.isHidden = true
                            }
                        } else {
                            cell.profileImage?.image = nil
                            fallbackAvatar.isHidden = false
                        }
                    }).disposed(by: cell.disposeBag)
678
            }
679
        }
680
    }
681
    // swiftlint:enable cyclomatic_complexity
682
}
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
683
// swiftlint:enable type_body_length
Thibault Wittemberg's avatar
Thibault Wittemberg committed
684 685 686 687 688 689 690 691

extension ConversationViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.messageViewModels?.count ?? 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let messageViewModel = self.messageViewModels?[indexPath.row] {
692 693 694 695 696 697 698
            let type =  messageViewModel.bubblePosition() == .received ? MessageCellReceived.self :
                        messageViewModel.bubblePosition() == .sent ? MessageCellSent.self :
                        messageViewModel.bubblePosition() == .generated ? MessageCellGenerated.self :
                        MessageCellGenerated.self
            let cell = tableView.dequeueReusableCell(for: indexPath, cellType: type)
            formatCell(withCell: cell, cellForRowAt: indexPath, withMessageVM: messageViewModel)
            return cell
Thibault Wittemberg's avatar
Thibault Wittemberg committed
699 700 701 702 703 704 705
        }

        return tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self)

    }

}