Skip to content

Instantly share code, notes, and snippets.

@alaxicsmith
Created June 5, 2020 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alaxicsmith/8e1c419ac464deeb7241a7b8736bcebc to your computer and use it in GitHub Desktop.
Save alaxicsmith/8e1c419ac464deeb7241a7b8736bcebc to your computer and use it in GitHub Desktop.
Sendbird
//
// 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