Created
June 5, 2020 20:56
-
-
Save alaxicsmith/8e1c419ac464deeb7241a7b8736bcebc to your computer and use it in GitHub Desktop.
Sendbird
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ViewMessagesViewController.swift | |
// Socialchair | |
// | |
// Created by Alaxic Smith on 10/18/19. | |
// Copyright © 2019 Socialchair. All rights reserved. | |
// | |
// swiftlint:disable empty_count | |
import KeyboardLayoutGuide | |
import NotificationBannerSwift | |
import SendBirdSDK | |
import Spring | |
import UIKit | |
class ViewMessagesViewController: UIViewController { | |
var channel: SBDGroupChannel! | |
var username: String! | |
var messages: [SBDBaseMessage] = [] | |
private let tableView = UITableView() | |
private let messageInputView = UIView() | |
private let messageField = PaddedTextField() | |
private let sendButton = UIButton() | |
private let typingView = SpringView() | |
var initialLoading: Bool = true | |
var hasPrevious: Bool? | |
var minMessageTimestamp: Int64 = Int64.max | |
var isLoading: Bool = false | |
var firstLoad: Bool = true | |
var trypingIndicatorTimer: [String: Timer] = [:] | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
self.navigationItem.title = "" | |
view.backgroundColor = UIColor(named: "Gray Background") | |
self.navigationController?.navigationBar.tintColor = UIColor.white | |
self.setupView() | |
self.setContraints() | |
self.getMessages() | |
SBDMain.add(self as SBDChannelDelegate, identifier: self.description) | |
SBDMain.add(self as SBDConnectionDelegate, identifier: self.description) | |
} | |
override func viewWillDisappear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshMessages"), object: nil) | |
} | |
func setContraints() { | |
let offset = self.tabBarController?.getHeight() | |
messageInputView.snp.makeConstraints { make in | |
make.leading.trailing.equalToSuperview() | |
make.height.equalTo(70) | |
make.bottom.equalTo(self.view.keyboardLayoutGuide.snp.top).offset(offset!) | |
} | |
sendButton.snp.makeConstraints { make in | |
make.width.height.equalTo(40) | |
make.trailing.equalTo(-15) | |
make.centerY.equalToSuperview() | |
} | |
messageField.snp.makeConstraints { make in | |
make.centerY.equalToSuperview() | |
make.leading.equalTo(15) | |
make.height.equalTo(40) | |
make.right.equalTo(sendButton.snp.left).offset(-10) | |
} | |
tableView.snp.makeConstraints { make in | |
make.leading.trailing.top.equalToSuperview() | |
make.bottom.equalTo(messageInputView.snp.top) | |
} | |
typingView.snp.makeConstraints { make in | |
make.leading.equalTo(15) | |
make.width.equalTo(15) | |
make.bottom.equalTo(messageInputView.snp.top).offset(-10) | |
} | |
} | |
func setupView() { | |
sendButton.setImage(UIImage(named: "Send Icon"), for: .normal) | |
sendButton.tintColor = UIColor.white | |
sendButton.backgroundColor = UIColor(named: "Blue") | |
sendButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40) | |
sendButton.layer.cornerRadius = 0.5 * 40 | |
sendButton.clipsToBounds = true | |
sendButton.addTarget(self, action: #selector(self.sendMessageButtonTapped(sender:)), for: .touchUpInside) | |
messageInputView.addSubview(sendButton) | |
messageInputView.layer.applySketchShadow(color: UIColor.black, alpha: 0.09, xCoordinate: 0, yCoordinate: 1, blur: 4, spread: 0) | |
messageInputView.backgroundColor = UIColor.white | |
view.addSubview(messageInputView) | |
messageInputView.addSubview(messageField) | |
messageField.becomeFirstResponder() | |
messageField.textColor = UIColor(named: "Gray Text") | |
messageField.font = R.font.markOTMedium(size: 15) | |
messageField.placeholder = "Send message..." | |
messageField.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 30, height: 40) | |
messageField.backgroundColor = UIColor.white | |
messageField.layer.borderColor = UIColor(named: "Gray Border")?.cgColor | |
messageField.layer.borderWidth = 1.5 | |
messageField.layer.cornerRadius = 0.5 * 40 | |
messageField.clipsToBounds = true | |
messageField.addTarget(self, action: #selector(self.inputMessageTextFieldChanged(_:)), for: .editingChanged) | |
view.addSubview(typingView) | |
typingView.backgroundColor = UIColor.black | |
typingView.alpha = 0.0 | |
typingView.animation = "slideUp" | |
typingView.duration = 1.5 | |
view.addSubview(tableView) | |
tableView.dataSource = self | |
tableView.delegate = self | |
tableView.tableFooterView = UIView() | |
tableView.backgroundColor = UIColor.clear | |
tableView.separatorStyle = .none | |
tableView.showsVerticalScrollIndicator = false | |
tableView.showsHorizontalScrollIndicator = false | |
tableView.estimatedRowHeight = 75.0 | |
tableView.contentInset = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0) | |
tableView.register(SentMessageTableViewCell.self, forCellReuseIdentifier: "sentCell") | |
tableView.register(OutgoingMessageTableViewCell.self, forCellReuseIdentifier: "outgoingCell") | |
} | |
func buildTypingIndicatorLabel(channel: SBDGroupChannel) -> String { | |
let typingMembers = channel.getTypingMembers() | |
if typingMembers == nil || typingMembers?.count == 0 { | |
return "" | |
} | |
else { | |
if typingMembers?.count == 1 { | |
return String(format: "%@ is typing.", typingMembers![0].nickname!) | |
} | |
else if typingMembers?.count == 2 { | |
return String(format: "%@ and %@ are typing.", typingMembers![0].nickname!, typingMembers![1].nickname!) | |
} | |
else { | |
return "Several people are typing." | |
} | |
} | |
} | |
func loadPreviousMessages(initial: Bool) { | |
if self.isLoading { | |
return | |
} | |
self.isLoading = true | |
var timestamp: Int64 = 0 | |
if initial { | |
self.hasPrevious = true | |
timestamp = Int64.max | |
} else { | |
timestamp = self.minMessageTimestamp | |
} | |
if self.hasPrevious == false { | |
return | |
} | |
guard let channel = self.channel else { | |
return | |
} | |
channel.getPreviousMessages(byTimestamp: timestamp, limit: 30, reverse: !initial, messageType: .all, customType: nil) { msgs, error in | |
if error != nil { | |
self.isLoading = false | |
return | |
} | |
guard let messages = msgs else { | |
return | |
} | |
if messages.isEmpty { | |
self.hasPrevious = false | |
} | |
if initial { | |
channel.markAsRead() | |
} else { | |
if self.firstLoad == false { | |
if !messages.isEmpty { | |
DispatchQueue.main.async { | |
var messageIndexPaths: [IndexPath] = [] | |
var row: Int = 0 | |
for message in messages { | |
self.messages.insert(message, at: 0) | |
if self.minMessageTimestamp > message.createdAt { | |
self.minMessageTimestamp = message.createdAt | |
} | |
messageIndexPaths.append(IndexPath(row: row, section: 0)) | |
row += 1 | |
} | |
self.tableView.reloadData() | |
self.tableView.layoutIfNeeded() | |
self.tableView.scrollToRow(at: IndexPath(row: messages.count - 1, section: 0), at: .top, animated: false) | |
self.isLoading = false | |
} | |
} | |
} | |
} | |
} | |
} | |
@objc func typingIndicatorTimeout(_ timer: Timer) { | |
if let channelUrl = timer.userInfo as? String { | |
self.trypingIndicatorTimer[channelUrl]?.invalidate() | |
self.trypingIndicatorTimer.removeValue(forKey: channelUrl) | |
DispatchQueue.main.async { | |
self.tableView.reloadData() | |
} | |
} | |
} | |
func getMessages() { | |
self.initialLoading = true | |
self.isLoading = true | |
self.firstLoad = true | |
guard let messageChannel = self.channel else { | |
return | |
} | |
// Load the messages from the channel in question | |
let previousMessageQuery = messageChannel.createPreviousMessageListQuery() | |
previousMessageQuery?.load(completionHandler: { messages, _ in | |
guard let channelMessages = messages else { | |
return | |
} | |
self.channel.markAsRead() | |
self.messages = channelMessages | |
self.tableView.reloadData() | |
if !self.messages.isEmpty { | |
self.tableView.scrollToBottom() | |
} | |
self.initialLoading = false | |
self.isLoading = false | |
}) | |
} | |
} | |
extension ViewMessagesViewController { | |
func showAlert(title: String, message: String) { | |
let alert = UIAlertController(title: title, | |
message: message, | |
preferredStyle: .alert) | |
alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) | |
self.present(alert, animated: true, completion: nil) | |
} | |
func sendMessage(message: String, completion: @escaping (Bool) -> Void) { | |
self.channel.sendUserMessage(message) { message, _ in | |
guard let _ = message else { | |
completion(false) | |
return | |
} | |
if let channel = self.channel { | |
channel.endTyping() | |
} | |
completion(true) | |
} | |
} | |
@objc func sendMessageButtonTapped(sender: UIButton) { | |
let message = self.messageField.text ?? "" | |
switch message.isEmpty { | |
case true: | |
self.showAlert(title: "Excuse Me", message: "Message cannot be blank.") | |
case false: | |
self.sendMessage(message: message) { result in | |
switch result { | |
case true: | |
self.messageField.text = "" | |
self.messages.removeAll() | |
self.getMessages() | |
case false: | |
break | |
} | |
} | |
} | |
} | |
@objc func inputMessageTextFieldChanged(_ sender: Any) { | |
guard let channel = self.channel else { | |
return | |
} | |
guard let textField = sender as? UITextField else { | |
return | |
} | |
if textField.text!.count > 0 { | |
channel.startTyping() | |
} else { | |
channel.endTyping() | |
} | |
} | |
} | |
extension ViewMessagesViewController: UITableViewDataSource { | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
var cell: UITableViewCell = UITableViewCell() | |
let currentMessage = self.messages[indexPath.row] | |
if currentMessage is SBDUserMessage { | |
guard let userMessage = currentMessage as? SBDUserMessage else { | |
return cell | |
} | |
guard let sender = userMessage.sender else { | |
return cell | |
} | |
if sender.userId == SBDMain.getCurrentUser()!.userId { | |
// Outgoing message | |
guard let incomingCell: OutgoingMessageTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "outgoingCell", for: indexPath) as? OutgoingMessageTableViewCell else { | |
return cell | |
} | |
incomingCell.message = currentMessage | |
return incomingCell | |
} else { | |
// Incoming message | |
guard let outgoingCell: SentMessageTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "sentCell", for: indexPath) as? SentMessageTableViewCell else { | |
return cell | |
} | |
outgoingCell.message = currentMessage | |
return outgoingCell | |
} | |
} | |
let typingIndicatorText = self.buildTypingIndicatorLabel(channel: channel) | |
let timer = self.trypingIndicatorTimer[channel.channelUrl] | |
var showTypingIndicator = false | |
if timer != nil && typingIndicatorText.count > 0 { | |
showTypingIndicator = true | |
} | |
if showTypingIndicator { | |
print("what is good") | |
} | |
return cell | |
} | |
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { | |
if indexPath.row == 10 && self.initialLoading == false && self.isLoading == false && self.firstLoad == false { | |
self.loadPreviousMessages(initial: false) | |
} | |
} | |
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { | |
return UITableView.automaticDimension | |
} | |
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
let messageCount = self.messages.count | |
return messageCount | |
} | |
} | |
extension ViewMessagesViewController: UITableViewDelegate { | |
} | |
extension ViewMessagesViewController: SBDChannelDelegate, SBDConnectionDelegate { | |
func channel(_ sender: SBDBaseChannel, didReceive message: SBDBaseMessage) { | |
if sender == self.channel { | |
guard let channel = self.channel else { | |
return | |
} | |
channel.markAsRead() | |
DispatchQueue.main.async { | |
UIView.setAnimationsEnabled(false) | |
self.messages.append(message) | |
self.tableView.reloadData() | |
self.tableView.layoutIfNeeded() | |
self.tableView.scrollToBottom() | |
UIView.setAnimationsEnabled(true) | |
} | |
} | |
} | |
func channelDidUpdateTypingStatus(_ sender: SBDGroupChannel) { | |
if let timer = self.trypingIndicatorTimer[sender.channelUrl] { | |
timer.invalidate() | |
} | |
let timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(ViewMessagesViewController.typingIndicatorTimeout(_ :)), userInfo: sender.channelUrl, repeats: false) | |
self.trypingIndicatorTimer[sender.channelUrl] = timer | |
DispatchQueue.main.async { | |
self.tableView.reloadData() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment