1470 lines
56 KiB
Swift
1470 lines
56 KiB
Swift
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import UIKit
|
|
import CoreLocation
|
|
import SnapKit
|
|
import MobileCoreServices
|
|
import os
|
|
|
|
private struct Constants {
|
|
static let MessagesPortionSize = 50
|
|
|
|
static let InputViewTopOffset: CGFloat = 50.0
|
|
|
|
static let NewMessageViewAllowedDelta: CGFloat = 20.0
|
|
static let NewMessageViewEdgesOffset: CGFloat = 5.0
|
|
static let NewMessageViewTopOffset: CGFloat = -15.0
|
|
static let NewMessageViewAnimationDuration = 0.2
|
|
|
|
static let ResetPanAnimationDuration = 0.3
|
|
|
|
static let MaxImageSizeToShowInline: OCTToxFileSize = 20 * 1024 * 1024
|
|
|
|
static let MaxInlineImageSide: CGFloat = LoadingImageView.Constants.ImageButtonSize * UIScreen.main.scale
|
|
}
|
|
|
|
protocol ChatPrivateControllerDelegate: class {
|
|
func chatPrivateControllerWillAppear(_ controller: ChatPrivateController)
|
|
func chatPrivateControllerWillDisappear(_ controller: ChatPrivateController)
|
|
func chatPrivateControllerCallToChat(_ controller: ChatPrivateController, enableVideo: Bool)
|
|
func chatPrivateControllerShowQuickLookController(
|
|
_ controller: ChatPrivateController,
|
|
dataSource: QuickLookPreviewControllerDataSource,
|
|
selectedIndex: Int)
|
|
}
|
|
|
|
class ChatPrivateController: KeyboardNotificationController, CLLocationManagerDelegate {
|
|
let chat: OCTChat
|
|
|
|
fileprivate weak var delegate: ChatPrivateControllerDelegate?
|
|
|
|
let location_manager = CLLocationManager()
|
|
fileprivate let theme: Theme
|
|
fileprivate weak var submanagerChats: OCTSubmanagerChats!
|
|
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
|
|
fileprivate weak var submanagerFiles: OCTSubmanagerFiles!
|
|
|
|
fileprivate let messages: Results<OCTMessageAbstract>
|
|
fileprivate var messagesToken: RLMNotificationToken?
|
|
fileprivate var visibleMessages: Int
|
|
|
|
fileprivate let friend: OCTFriend?
|
|
fileprivate var friendToken: RLMNotificationToken?
|
|
|
|
fileprivate let imageCache = NSCache<AnyObject, AnyObject>()
|
|
|
|
fileprivate let timeFormatter: DateFormatter
|
|
fileprivate let dateFormatter: DateFormatter
|
|
|
|
fileprivate var audioButton: UIBarButtonItem!
|
|
fileprivate var videoButton: UIBarButtonItem!
|
|
// fileprivate var locationButton: UIBarButtonItem!
|
|
fileprivate var CallWaitingView: UIView!
|
|
fileprivate var callwaiting_running: Bool!
|
|
fileprivate var CallWaitingCancelButton: CallButton?
|
|
fileprivate let linearBar: LinearProgressBar = LinearProgressBar()
|
|
|
|
fileprivate var titleView: ChatPrivateTitleView!
|
|
fileprivate var tableView: UITableView?
|
|
fileprivate var typingHeaderView: ChatTypingHeaderView!
|
|
fileprivate var fauxOfflineHeaderView: ChatFauxOfflineHeaderView!
|
|
fileprivate var newMessagesView: UIView!
|
|
fileprivate var chatInputView: ChatInputView!
|
|
fileprivate var editMessagesToolbar: UIToolbar!
|
|
|
|
fileprivate var chatInputViewManager: ChatInputViewManager!
|
|
|
|
fileprivate var tableViewTapGestureRecognizer: UITapGestureRecognizer!
|
|
|
|
fileprivate var tableViewToChatInputConstraint: Constraint!
|
|
fileprivate var typingViewToChatInputConstraint: Constraint!
|
|
|
|
fileprivate var newMessageViewTopConstraint: Constraint?
|
|
fileprivate var chatInputViewBottomConstraint: Constraint?
|
|
|
|
fileprivate var newMessagesViewVisible = false
|
|
|
|
/// Index path for cell with UIMenu presented.
|
|
fileprivate var selectedMenuIndexPath: IndexPath?
|
|
|
|
fileprivate let showKeyboardOnAppear: Bool
|
|
fileprivate var disableNextInputViewAnimation = false
|
|
|
|
init(theme: Theme, chat: OCTChat, submanagerChats: OCTSubmanagerChats, submanagerObjects: OCTSubmanagerObjects, submanagerFiles: OCTSubmanagerFiles, delegate: ChatPrivateControllerDelegate, showKeyboardOnAppear: Bool = false) {
|
|
self.theme = theme
|
|
self.chat = chat
|
|
self.friend = chat.friends.lastObject() as? OCTFriend
|
|
self.submanagerChats = submanagerChats
|
|
self.submanagerObjects = submanagerObjects
|
|
self.submanagerFiles = submanagerFiles
|
|
self.delegate = delegate
|
|
self.showKeyboardOnAppear = showKeyboardOnAppear
|
|
self.callwaiting_running = false
|
|
|
|
let predicate = NSPredicate(format: "chatUniqueIdentifier == %@", chat.uniqueIdentifier)
|
|
self.messages = submanagerObjects.messages(predicate: predicate).sortedResultsUsingProperty("dateInterval", ascending: false)
|
|
self.visibleMessages = Constants.MessagesPortionSize
|
|
|
|
self.timeFormatter = DateFormatter(type: .time)
|
|
self.dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "MMMdd"
|
|
|
|
super.init()
|
|
|
|
edgesForExtendedLayout = UIRectEdge()
|
|
hidesBottomBarWhenPushed = true
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(ChatPrivateController.applicationDidBecomeActive),
|
|
name: NSNotification.Name.UIApplicationDidBecomeActive,
|
|
object: nil)
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(ChatPrivateController.willShowMenuNotification(_:)),
|
|
name: NSNotification.Name.UIMenuControllerWillShowMenu,
|
|
object: nil)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(ChatPrivateController.willHideMenuNotification),
|
|
name: NSNotification.Name.UIMenuControllerWillHideMenu,
|
|
object: nil)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
|
messagesToken?.invalidate()
|
|
friendToken?.invalidate()
|
|
}
|
|
|
|
required convenience init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func loadView() {
|
|
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
|
|
|
|
createTableView()
|
|
createTableHeaderViews()
|
|
createNewMessagesView()
|
|
createInputView()
|
|
createEditMessageToolbar()
|
|
installConstraints()
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
addMessagesNotification()
|
|
|
|
createNavigationViews()
|
|
addFriendNotification()
|
|
|
|
// HINT: request Location updates here
|
|
LocationManager.shared.requestAccess()
|
|
|
|
// HINT: location manager to get location on button pressed
|
|
location_manager.delegate = self
|
|
|
|
self.configureLinearProgressBar()
|
|
}
|
|
|
|
// TODO(Tha_14): Remove the following
|
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
if let location = locations.first {
|
|
let lat_str = String(format: "%.5f", location.coordinate.latitude)
|
|
let lon_str = String(format: "%.5f", location.coordinate.longitude)
|
|
let location_string = lat_str + ", " + lon_str
|
|
let zoom_level = "14"
|
|
print("location: \(location_string)")
|
|
// let location_url = "https://www.openstreetmap.org/search?query=" + lat_str + "%2C%20" + lon_str + "#map=" + zoom_level + "/" + lat_str + "/" + lon_str
|
|
let location_url = "https://www.openstreetmap.org/?mlat=" + lat_str + "&mlon=" + lon_str + "#map=" + zoom_level + "/" + lat_str + "/" + lon_str
|
|
|
|
// chatInputView.text = "my Location: " + location_string + "\n" + location_url
|
|
|
|
//DispatchQueue.main.async {
|
|
self.submanagerChats.sendMessage(to: self.chat, text: "my Location: " + location_string + "\n" + location_url, type: .normal, successBlock: nil, failureBlock: nil)
|
|
//}
|
|
}
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
// print("Failed to find user's location: \(error.localizedDescription)")
|
|
}
|
|
|
|
fileprivate func configureLinearProgressBar(){
|
|
linearBar.backgroundColor = UIColor(red:0.68, green:0.81, blue:0.72, alpha:1.0)
|
|
linearBar.progressBarColor = UIColor(red:0.26, green:0.65, blue:0.45, alpha:1.0)
|
|
linearBar.heightForLinearBar = 5
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
updateLastReadDate()
|
|
delegate?.chatPrivateControllerWillAppear(self)
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
delegate?.chatPrivateControllerWillDisappear(self)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
if showKeyboardOnAppear {
|
|
disableNextInputViewAnimation = true
|
|
_ = chatInputView.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
override func keyboardWillShowAnimated(keyboardFrame frame: CGRect) {
|
|
super.keyboardWillShowAnimated(keyboardFrame: frame)
|
|
|
|
guard let constraint = chatInputViewBottomConstraint else {
|
|
return
|
|
}
|
|
|
|
constraint.update(offset: -frame.size.height)
|
|
|
|
if disableNextInputViewAnimation {
|
|
disableNextInputViewAnimation = false
|
|
|
|
UIView.setAnimationsEnabled(false)
|
|
view.layoutIfNeeded()
|
|
UIView.setAnimationsEnabled(true)
|
|
}
|
|
else {
|
|
view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
override func keyboardWillHideAnimated(keyboardFrame frame: CGRect) {
|
|
super.keyboardWillHideAnimated(keyboardFrame: frame)
|
|
|
|
guard let constraint = chatInputViewBottomConstraint else {
|
|
return
|
|
}
|
|
|
|
// TODO: this moves the input view a bit more to the top, because the home button "line" is in the way otherwise
|
|
// please fix me properly in the future
|
|
constraint.update(offset: 0.0)
|
|
if #available(iOS 11.0, *) {
|
|
let keyWindow = UIApplication.shared.keyWindow
|
|
let b = keyWindow?.safeAreaInsets.bottom
|
|
constraint.update(offset: -(b ?? 20))
|
|
}
|
|
|
|
if disableNextInputViewAnimation {
|
|
disableNextInputViewAnimation = false
|
|
|
|
UIView.setAnimationsEnabled(false)
|
|
view.layoutIfNeeded()
|
|
UIView.setAnimationsEnabled(true)
|
|
}
|
|
else {
|
|
view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
|
|
updateInputViewMaxHeight()
|
|
}
|
|
}
|
|
|
|
// MARK: Actions
|
|
extension ChatPrivateController {
|
|
@objc func tapOnTableView() {
|
|
_ = chatInputView.resignFirstResponder()
|
|
}
|
|
|
|
@objc func panOnTableView(_ recognizer: UIPanGestureRecognizer) {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
|
|
if (UserDefaultsManager().DateonmessageMode == true) {
|
|
return
|
|
}
|
|
|
|
let translation = recognizer.translation(in: recognizer.view)
|
|
recognizer.setTranslation(CGPoint.zero, in: recognizer.view)
|
|
|
|
_ = tableView.visibleCells.filter {
|
|
$0 is ChatMovableDateCell
|
|
}.map {
|
|
$0 as! ChatMovableDateCell
|
|
}.map {
|
|
switch recognizer.state {
|
|
case .possible:
|
|
fallthrough
|
|
case .began:
|
|
// nop
|
|
break
|
|
case .changed:
|
|
$0.movableOffset += translation.x
|
|
case .ended:
|
|
fallthrough
|
|
case .cancelled:
|
|
fallthrough
|
|
case .failed:
|
|
let cell = $0
|
|
UIView.animate(withDuration: Constants.ResetPanAnimationDuration, animations: {
|
|
cell.movableOffset = 0.0
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func newMessagesViewPressed() {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
|
|
tableView.setContentOffset(CGPoint.zero, animated: true)
|
|
|
|
// iOS is broken =\
|
|
// See https://stackoverflow.com/a/30804874
|
|
let delayTime = DispatchTime.now() + Double(Int64(0.2 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
|
DispatchQueue.main.asyncAfter(deadline: delayTime) { [weak self] in
|
|
self?.tableView?.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
|
}
|
|
}
|
|
|
|
func messageBox(messageTitle: String, messageAlert: String, messageBoxStyle: UIAlertControllerStyle, alertActionStyle: UIAlertActionStyle, completionHandler: @escaping () -> Void)
|
|
{
|
|
let alert = UIAlertController(title: messageTitle, message: messageAlert, preferredStyle: messageBoxStyle)
|
|
|
|
let okAction = UIAlertAction(title: "Cancel", style: alertActionStyle) { _ in
|
|
completionHandler() // This will only get called after okay is tapped in the alert
|
|
}
|
|
|
|
alert.addAction(okAction)
|
|
present(alert, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc
|
|
func buttonCallWaitingCancel() {
|
|
callwaiting_running = false
|
|
CallWaitingView.removeFromSuperview()
|
|
self.linearBar.stopAnimation()
|
|
}
|
|
|
|
@objc func audioCallButtonPressed() {
|
|
|
|
if let friend = self.friend {
|
|
let connection_status = ConnectionStatus(connectionStatus: friend.connectionStatus)
|
|
if (connection_status != .none)
|
|
{
|
|
// HINT: friend is online, so start the call now
|
|
callwaiting_running = false
|
|
delegate?.chatPrivateControllerCallToChat(self, enableVideo: false)
|
|
}
|
|
else
|
|
{
|
|
// HINT: friend is not online, show call waiting screen
|
|
callwaiting_running = true
|
|
let window = UIApplication.shared.keyWindow!
|
|
CallWaitingView = UIView(frame: window.bounds)
|
|
window.addSubview(CallWaitingView)
|
|
CallWaitingView.backgroundColor = .black
|
|
|
|
CallWaitingCancelButton = CallButton(theme: theme, type: .decline, buttonSize: .big)
|
|
CallWaitingCancelButton!.addTarget(self,
|
|
action: #selector(buttonCallWaitingCancel),
|
|
for: .touchUpInside)
|
|
// CallWaitingCancelButton!.backgroundColor = .white
|
|
|
|
let lb1 = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 80))
|
|
lb1.text = "Call"
|
|
lb1.textAlignment = .center;
|
|
lb1.font = lb1.font.withSize(35)
|
|
lb1.textColor = .white
|
|
lb1.backgroundColor = .black
|
|
lb1.numberOfLines = 0;
|
|
lb1.sizeToFit()
|
|
|
|
let lb2 = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 80))
|
|
lb2.text = friend.nickname
|
|
lb2.textAlignment = .center;
|
|
lb2.font = lb1.font.withSize(30)
|
|
lb2.textColor = .white
|
|
lb2.backgroundColor = .black
|
|
lb2.numberOfLines = 0;
|
|
lb2.sizeToFit()
|
|
|
|
let lb3 = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 80))
|
|
lb3.text = "waiting for friend to come online ..."
|
|
lb3.textAlignment = .center;
|
|
lb3.font = lb1.font.withSize(20)
|
|
lb3.textColor = .white
|
|
lb3.backgroundColor = .black
|
|
lb3.numberOfLines = 0;
|
|
lb3.lineBreakMode = .byWordWrapping
|
|
lb3.sizeToFit()
|
|
|
|
|
|
CallWaitingView.addSubview(CallWaitingCancelButton!)
|
|
CallWaitingView.addSubview(lb1)
|
|
CallWaitingView.addSubview(lb2)
|
|
CallWaitingView.addSubview(lb3)
|
|
CallWaitingCancelButton!.center = CallWaitingView.center
|
|
CallWaitingView.bringSubview(toFront: lb1)
|
|
CallWaitingView.bringSubview(toFront: lb2)
|
|
CallWaitingView.bringSubview(toFront: lb3)
|
|
CallWaitingView.bringSubview(toFront: CallWaitingCancelButton!)
|
|
|
|
let BigButtonOffset = 30.0
|
|
|
|
CallWaitingView.snp.makeConstraints { make in
|
|
make.leading.trailing.bottom.equalToSuperview()
|
|
// make.top.equalTo(view.safeAreaLayoutGuide) // --> that leave too much see through space at the top. strange
|
|
make.top.equalToSuperview()
|
|
}
|
|
|
|
CallWaitingCancelButton!.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.top.greaterThanOrEqualToSuperview().offset(BigButtonOffset)
|
|
make.bottom.equalToSuperview().offset(-BigButtonOffset)
|
|
}
|
|
|
|
lb1.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.top.equalToSuperview().offset(60)
|
|
make.leading.trailing.equalToSuperview()
|
|
}
|
|
|
|
lb2.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.top.equalTo(lb1.snp.bottom).offset(35)
|
|
make.leading.trailing.equalToSuperview()
|
|
}
|
|
|
|
lb3.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.top.equalTo(lb2.snp.bottom).offset(35)
|
|
make.leading.trailing.equalToSuperview()
|
|
}
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
|
|
print("cc:call_waiting_bg_queue")
|
|
if self.friend != nil {
|
|
|
|
DispatchQueue.main.async {
|
|
// send a text message to trigger PUSH notification, and make friend come online (hopefully)
|
|
// HINT: call OCTSubmanagerChatsImpl.m -> sendMessageToChat()
|
|
self.submanagerChats.sendMessage(to: self.chat, text: "calling you", type: .normal, successBlock: nil, failureBlock: nil)
|
|
|
|
self.linearBar.startAnimation(viewToAddto: self.CallWaitingView, viewToAlignToBottomOf: lb3, bottom_margin: 10)
|
|
self.CallWaitingView.bringSubview(toFront: self.linearBar)
|
|
}
|
|
|
|
var connection_status2: ConnectionStatus = .none
|
|
|
|
while true {
|
|
|
|
DispatchQueue.main.async {
|
|
connection_status2 = ConnectionStatus(connectionStatus: friend.connectionStatus)
|
|
}
|
|
|
|
print("cc:while_friend_not_online, %@", connection_status2)
|
|
if (connection_status2 != .none)
|
|
{
|
|
DispatchQueue.main.async {
|
|
print("cc:main_queue")
|
|
if (self.callwaiting_running)
|
|
{
|
|
self.callwaiting_running = false
|
|
self.CallWaitingView.removeFromSuperview()
|
|
self.linearBar.stopAnimation()
|
|
self.delegate?.chatPrivateControllerCallToChat(self, enableVideo: false)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
// HINT: sleep for 1 second
|
|
if (self.callwaiting_running)
|
|
{
|
|
sleep(1)
|
|
}
|
|
else
|
|
{
|
|
DispatchQueue.main.async {
|
|
self.CallWaitingView.removeFromSuperview()
|
|
self.linearBar.stopAnimation()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
print("cc:while_loop_end")
|
|
}
|
|
}
|
|
|
|
}
|
|
} else {
|
|
print("Call_ERROR:no friend?")
|
|
}
|
|
}
|
|
// TODO(Tha_14): Remove this
|
|
// @objc func displayalert() {
|
|
// var alert = UIAlertController(title: "Location Sharing", message: "Would you like to enable location sharing with this contact?\nYou can disable this feature by clicking on the location icon after it has been enabled.", preferredStyle: UIAlertControllerStyle.alert)
|
|
// var action_title = "Enable"
|
|
//
|
|
// if (AppDelegate.location_sharing_contact_pubkey != "-1")
|
|
// {
|
|
// alert = UIAlertController(title: "Location Sharing", message: "Disable sharing with this contact " + AppDelegate.location_sharing_contact_pubkey + " ?" , preferredStyle: UIAlertControllerStyle.alert)
|
|
// action_title = "Disable"
|
|
// }
|
|
//
|
|
// alert.addAction((UIAlertAction(title: action_title, style: .default, handler: { [self] (action) -> Void in
|
|
// if (action_title == "Disable")
|
|
// {
|
|
// AppDelegate.location_sharing_contact_pubkey = "-1"
|
|
// let locationImage = UIImage(named: "location-call-medium")!.withRenderingMode(.alwaysOriginal)
|
|
// locationButton.setBackgroundImage(locationImage, for: .normal, barMetrics: .default)
|
|
// }
|
|
// else
|
|
// {
|
|
// AppDelegate.location_sharing_contact_pubkey = self.friend?.publicKey ?? "-1"
|
|
// let locationImage = UIImage(named: "location-call-activated-medium")!.withRenderingMode(.alwaysOriginal)
|
|
// locationButton.setBackgroundImage(locationImage, for: .normal, barMetrics: .default)
|
|
//
|
|
// DispatchQueue.global(qos: .userInitiated).async {
|
|
//
|
|
// print("ll:location_sharing")
|
|
// if self.friend != nil {
|
|
//
|
|
// while AppDelegate.location_sharing_contact_pubkey != "-1" {
|
|
// location_manager.requestLocation()
|
|
// // HINT: sleep for 30 seconds
|
|
// sleep(30)
|
|
// }
|
|
// print("ll:while_loop_end")
|
|
// }
|
|
// }
|
|
// }
|
|
// alert.dismiss(animated: true, completion: nil)
|
|
// })))
|
|
//
|
|
// alert.addAction(
|
|
// UIAlertAction(title: "Cancel", style: .cancel, handler: { (action) -> Void in
|
|
// alert.dismiss(animated: true, completion: nil)
|
|
// }))
|
|
//
|
|
// self.present(alert, animated: true, completion: nil)
|
|
// }
|
|
|
|
@objc func videoCallButtonPressed() {
|
|
delegate?.chatPrivateControllerCallToChat(self, enableVideo: true)
|
|
}
|
|
|
|
// @objc func locationButtonPressed() {
|
|
// displayalert()
|
|
// }
|
|
|
|
@objc func editMessagesDeleteButtonPressed(_ barButtonItem: UIBarButtonItem) {
|
|
guard let selectedRows = tableView?.indexPathsForSelectedRows else {
|
|
return
|
|
}
|
|
|
|
showMessageDeletionConfirmation(messagesCount: selectedRows.count,
|
|
showFromItem: barButtonItem,
|
|
deleteClosure: { [unowned self] in
|
|
self.toggleTableViewEditing(false, animated: true)
|
|
|
|
let toRemove = selectedRows.map {
|
|
return self.messages[$0.row]
|
|
}
|
|
|
|
self.submanagerChats.removeMessages(toRemove)
|
|
})
|
|
}
|
|
|
|
@objc func deleteAllMessagesButtonPressed(_ barButtonItem: UIBarButtonItem) {
|
|
toggleTableViewEditing(false, animated: true)
|
|
|
|
showMessageDeletionConfirmation(messagesCount: messages.count,
|
|
showFromItem: barButtonItem,
|
|
deleteClosure: { [unowned self] in
|
|
self.submanagerChats.removeAllMessages(in: self.chat, removeChat: false)
|
|
})
|
|
}
|
|
|
|
@objc func cancelEditingButtonPressed() {
|
|
toggleTableViewEditing(false, animated: true)
|
|
}
|
|
}
|
|
|
|
// MARK: Notifications
|
|
extension ChatPrivateController {
|
|
@objc func applicationDidBecomeActive() {
|
|
updateLastReadDate()
|
|
}
|
|
|
|
@objc func willShowMenuNotification(_ notification: Notification) {
|
|
guard let indexPath = selectedMenuIndexPath else {
|
|
return
|
|
}
|
|
guard let cell = tableView?.cellForRow(at: indexPath) else {
|
|
return
|
|
}
|
|
guard let editable = cell as? ChatEditable else {
|
|
return
|
|
}
|
|
guard let menu = notification.object as? UIMenuController else {
|
|
return
|
|
}
|
|
|
|
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil)
|
|
|
|
menu.setMenuVisible(false, animated: false)
|
|
|
|
let rect = cell.convert(editable.menuTargetRect(), to: view)
|
|
|
|
menu.setTargetRect(rect, in: view)
|
|
menu.setMenuVisible(true, animated: true)
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(ChatPrivateController.willShowMenuNotification(_:)),
|
|
name: NSNotification.Name.UIMenuControllerWillShowMenu,
|
|
object: nil)
|
|
}
|
|
|
|
@objc func willHideMenuNotification() {
|
|
guard let indexPath = selectedMenuIndexPath else {
|
|
return
|
|
}
|
|
selectedMenuIndexPath = nil
|
|
|
|
guard let editable = tableView?.cellForRow(at: indexPath) as? ChatEditable else {
|
|
return
|
|
}
|
|
|
|
editable.willHideMenu()
|
|
}
|
|
}
|
|
|
|
extension ChatPrivateController: UITableViewDataSource {
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let message = messages[indexPath.row]
|
|
|
|
// setting default values to avoid crash
|
|
var model: ChatMovableDateCellModel = ChatMovableDateCellModel()
|
|
var cell: ChatMovableDateCell = tableView.dequeueReusableCell(withIdentifier: ChatMovableDateCell.staticReuseIdentifier) as! ChatMovableDateCell
|
|
|
|
if message.isOutgoing() {
|
|
if let messageText = message.messageText {
|
|
let outgoingModel = ChatOutgoingTextCellModel()
|
|
|
|
|
|
outgoingModel.message = messageText.text ?? ""
|
|
|
|
outgoingModel.delivered = messageText.isDelivered
|
|
//outgoingModel.sentpush = messageText.sentPush
|
|
|
|
model = outgoingModel
|
|
|
|
cell = tableView.dequeueReusableCell(withIdentifier: ChatOutgoingTextCell.staticReuseIdentifier) as! ChatOutgoingTextCell
|
|
}
|
|
else if let messageCall = message.messageCall {
|
|
let outgoingModel = ChatOutgoingCallCellModel()
|
|
outgoingModel.callDuration = messageCall.callDuration
|
|
outgoingModel.answered = (messageCall.callEvent == .answered)
|
|
model = outgoingModel
|
|
|
|
cell = tableView.dequeueReusableCell(withIdentifier: ChatOutgoingCallCell.staticReuseIdentifier) as! ChatOutgoingCallCell
|
|
}
|
|
else if let _ = message.messageFile {
|
|
(model, cell) = imageCellWithMessage(message, incoming: false)
|
|
}
|
|
}
|
|
else {
|
|
if let messageText = message.messageText {
|
|
let incomingModel = ChatBaseTextCellModel()
|
|
|
|
|
|
incomingModel.message = messageText.text ?? ""
|
|
|
|
|
|
model = incomingModel
|
|
|
|
cell = tableView.dequeueReusableCell(withIdentifier: ChatIncomingTextCell.staticReuseIdentifier) as! ChatIncomingTextCell
|
|
}
|
|
else if let messageCall = message.messageCall {
|
|
let incomingModel = ChatIncomingCallCellModel()
|
|
incomingModel.callDuration = messageCall.callDuration
|
|
incomingModel.answered = (messageCall.callEvent == .answered)
|
|
model = incomingModel
|
|
|
|
cell = tableView.dequeueReusableCell(withIdentifier: ChatIncomingCallCell.staticReuseIdentifier) as! ChatIncomingCallCell
|
|
}
|
|
else if let _ = message.messageFile {
|
|
(model, cell) = imageCellWithMessage(message, incoming: true)
|
|
}
|
|
}
|
|
|
|
var incoming_text_message: Bool = false
|
|
if (!message.isOutgoing()) {
|
|
if let messageText = message.messageText {
|
|
incoming_text_message = true
|
|
}
|
|
}
|
|
|
|
if (incoming_text_message) {
|
|
if (message.tssent == 0) {
|
|
model.dateString = timeFormatter.string(from: message.date()) + "\n" + dateFormatter.string(from: message.date())
|
|
} else {
|
|
let real_datetime = Date(timeIntervalSince1970: TimeInterval(message.tssent))
|
|
model.dateString = timeFormatter.string(from: real_datetime) + "\n" + dateFormatter.string(from: real_datetime)
|
|
// os_log("mmm:%s", timeFormatter.string(from: real_datetime))
|
|
}
|
|
} else {
|
|
model.dateString = timeFormatter.string(from: message.date()) + "\n" + dateFormatter.string(from: message.date())
|
|
}
|
|
|
|
cell.delegate = self
|
|
cell.setupWithTheme(theme, model: model)
|
|
cell.transform = tableView.transform;
|
|
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return min(visibleMessages, messages.count)
|
|
}
|
|
}
|
|
|
|
extension ChatPrivateController: UITableViewDelegate {
|
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
if indexPath.row == 0 {
|
|
toggleNewMessageView(show: false)
|
|
}
|
|
|
|
maybeLoadImageForCellAtPath(cell, indexPath: indexPath)
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
|
|
guard !tableView.isEditing else {
|
|
return false
|
|
}
|
|
guard let editable = tableView.cellForRow(at: indexPath) as? ChatEditable else {
|
|
return false
|
|
}
|
|
|
|
if !editable.shouldShowMenu() {
|
|
return false
|
|
}
|
|
|
|
selectedMenuIndexPath = indexPath
|
|
editable.willShowMenu()
|
|
|
|
return true
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
|
|
guard let cell = tableView.cellForRow(at: indexPath) as? ChatMovableDateCell else {
|
|
return false
|
|
}
|
|
|
|
return cell.isMenuActionSupportedByCell(action)
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
|
|
// Dummy method to make tableView:shouldShowMenuForRowAtIndexPath: work.
|
|
}
|
|
}
|
|
|
|
extension ChatPrivateController: UIScrollViewDelegate {
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
guard scrollView === tableView else {
|
|
return
|
|
}
|
|
|
|
if tableView.contentOffset.y > (tableView.contentSize.height - tableView.frame.size.height) {
|
|
let previous = visibleMessages
|
|
|
|
visibleMessages = visibleMessages + Constants.MessagesPortionSize
|
|
|
|
if visibleMessages > messages.count {
|
|
visibleMessages = messages.count
|
|
}
|
|
|
|
if visibleMessages != previous {
|
|
tableView.reloadData()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ChatPrivateController: ChatMovableDateCellDelegate {
|
|
func chatMovableDateCellCopyPressed(_ cell: ChatMovableDateCell) {
|
|
guard let indexPath = tableView?.indexPath(for: cell) else {
|
|
return
|
|
}
|
|
|
|
let message = messages[indexPath.row]
|
|
|
|
if let messageText = message.messageText {
|
|
UIPasteboard.general.string = messageText.text
|
|
}
|
|
else if let _ = message.messageCall {
|
|
fatalError("Message call cannot be copied")
|
|
}
|
|
else if let messageFile = message.messageFile {
|
|
guard UTTypeConformsTo(messageFile.fileUTI as CFString? ?? "" as CFString, kUTTypeImage) else {
|
|
fatalError("Cannot copy non-image file")
|
|
}
|
|
guard let file = messageFile.filePath() else {
|
|
assertionFailure("Tried to copy non-existing file")
|
|
return
|
|
}
|
|
guard let image = UIImage(contentsOfFile: file) else {
|
|
assertionFailure("Cannot create image from file")
|
|
return
|
|
}
|
|
|
|
UIPasteboard.general.image = image
|
|
}
|
|
}
|
|
|
|
func chatMovableDateCellDeletePressed(_ cell: ChatMovableDateCell) {
|
|
guard let indexPath = tableView?.indexPath(for: cell) else {
|
|
return
|
|
}
|
|
|
|
let message = messages[indexPath.row]
|
|
|
|
submanagerChats.removeMessages([message])
|
|
}
|
|
|
|
func chatMovableDateCellMorePressed(_ cell: ChatMovableDateCell) {
|
|
toggleTableViewEditing(true, animated: true)
|
|
|
|
// TODO select row
|
|
// guard let indexPath = tableView?.indexPathForCell(cell) else {
|
|
// return
|
|
// }
|
|
|
|
// tableView?.selectRowAtIndexPath(indexPath, animated: false, scrollPosition: .None)
|
|
}
|
|
}
|
|
|
|
extension ChatPrivateController: UIGestureRecognizerDelegate {
|
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
guard let panGR = gestureRecognizer as? UIPanGestureRecognizer else {
|
|
return false
|
|
}
|
|
|
|
let translation = panGR.translation(in: panGR.view)
|
|
|
|
return fabsf(Float(translation.x)) > fabsf(Float(translation.y))
|
|
}
|
|
}
|
|
|
|
private extension ChatPrivateController {
|
|
func createNavigationViews() {
|
|
titleView = ChatPrivateTitleView(theme: theme)
|
|
navigationItem.titleView = titleView
|
|
|
|
// create correct navigation buttons
|
|
toggleTableViewEditing(false, animated: false)
|
|
}
|
|
|
|
func createTableView() {
|
|
let tableView = UITableView()
|
|
self.tableView = tableView
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
tableView.transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0)
|
|
tableView.scrollsToTop = false
|
|
tableView.allowsSelection = false
|
|
tableView.estimatedRowHeight = 44.0
|
|
tableView.backgroundColor = theme.colorForType(.NormalBackground)
|
|
tableView.allowsMultipleSelectionDuringEditing = true
|
|
tableView.separatorStyle = .none
|
|
|
|
view.addSubview(tableView)
|
|
|
|
tableView.register(ChatMovableDateCell.self, forCellReuseIdentifier: ChatMovableDateCell.staticReuseIdentifier)
|
|
tableView.register(ChatIncomingTextCell.self, forCellReuseIdentifier: ChatIncomingTextCell.staticReuseIdentifier)
|
|
tableView.register(ChatOutgoingTextCell.self, forCellReuseIdentifier: ChatOutgoingTextCell.staticReuseIdentifier)
|
|
tableView.register(ChatIncomingCallCell.self, forCellReuseIdentifier: ChatIncomingCallCell.staticReuseIdentifier)
|
|
tableView.register(ChatOutgoingCallCell.self, forCellReuseIdentifier: ChatOutgoingCallCell.staticReuseIdentifier)
|
|
tableView.register(ChatIncomingFileCell.self, forCellReuseIdentifier: ChatIncomingFileCell.staticReuseIdentifier)
|
|
tableView.register(ChatOutgoingFileCell.self, forCellReuseIdentifier: ChatOutgoingFileCell.staticReuseIdentifier)
|
|
|
|
tableViewTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ChatPrivateController.tapOnTableView))
|
|
tableView.addGestureRecognizer(tableViewTapGestureRecognizer)
|
|
|
|
let panGR = UIPanGestureRecognizer(target: self, action: #selector(ChatPrivateController.panOnTableView(_:)))
|
|
panGR.delegate = self
|
|
tableView.addGestureRecognizer(panGR)
|
|
}
|
|
|
|
func createTableHeaderViews() {
|
|
typingHeaderView = ChatTypingHeaderView(theme: theme)
|
|
typingHeaderView.transform = tableView!.transform
|
|
view.addSubview(typingHeaderView)
|
|
|
|
fauxOfflineHeaderView = ChatFauxOfflineHeaderView(theme: theme)
|
|
fauxOfflineHeaderView.transform = tableView!.transform
|
|
view.addSubview(fauxOfflineHeaderView)
|
|
}
|
|
|
|
func createNewMessagesView() {
|
|
newMessagesView = UIView()
|
|
newMessagesView.backgroundColor = theme.colorForType(.ConnectingBackground)
|
|
newMessagesView.layer.cornerRadius = 5.0
|
|
newMessagesView.layer.masksToBounds = true
|
|
newMessagesView.isHidden = true
|
|
view.addSubview(newMessagesView)
|
|
|
|
let label = UILabel()
|
|
label.text = String(localized: "chat_new_messages")
|
|
label.textColor = theme.colorForType(.ConnectingText)
|
|
label.backgroundColor = .clear
|
|
label.font = UIFont.systemFont(ofSize: 12.0)
|
|
newMessagesView.addSubview(label)
|
|
|
|
let button = UIButton()
|
|
button.addTarget(self, action: #selector(ChatPrivateController.newMessagesViewPressed), for: .touchUpInside)
|
|
newMessagesView.addSubview(button)
|
|
|
|
label.snp.makeConstraints {
|
|
$0.leading.equalTo(newMessagesView).offset(Constants.NewMessageViewEdgesOffset)
|
|
$0.trailing.equalTo(newMessagesView).offset(-Constants.NewMessageViewEdgesOffset)
|
|
$0.top.equalTo(newMessagesView).offset(Constants.NewMessageViewEdgesOffset)
|
|
$0.bottom.equalTo(newMessagesView).offset(-Constants.NewMessageViewEdgesOffset)
|
|
}
|
|
|
|
button.snp.makeConstraints {
|
|
$0.edges.equalTo(newMessagesView)
|
|
}
|
|
}
|
|
|
|
func createInputView() {
|
|
chatInputView = ChatInputView(theme: theme)
|
|
view.addSubview(chatInputView)
|
|
|
|
chatInputViewManager = ChatInputViewManager(inputView: chatInputView,
|
|
chat: chat,
|
|
submanagerChats: submanagerChats,
|
|
submanagerFiles: submanagerFiles,
|
|
submanagerObjects: submanagerObjects,
|
|
presentingViewController: self)
|
|
}
|
|
|
|
func createEditMessageToolbar() {
|
|
editMessagesToolbar = UIToolbar()
|
|
editMessagesToolbar.isHidden = true
|
|
editMessagesToolbar.tintColor = theme.colorForType(.LinkText)
|
|
editMessagesToolbar.items = [
|
|
UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(ChatPrivateController.editMessagesDeleteButtonPressed(_:)))
|
|
]
|
|
view.addSubview(editMessagesToolbar)
|
|
}
|
|
|
|
func installConstraints() {
|
|
tableView!.snp.makeConstraints {
|
|
$0.top.leading.trailing.equalTo(view)
|
|
|
|
tableViewToChatInputConstraint = $0.bottom.equalTo(chatInputView.snp.top).constraint
|
|
}
|
|
|
|
typingHeaderView.snp.makeConstraints {
|
|
$0.leading.trailing.equalTo(view)
|
|
$0.top.equalTo(tableView!.snp.bottom)
|
|
typingViewToChatInputConstraint = $0.bottom.equalTo(chatInputView.snp.top).constraint
|
|
}
|
|
|
|
typingViewToChatInputConstraint.deactivate()
|
|
|
|
newMessagesView.snp.makeConstraints {
|
|
$0.centerX.equalTo(tableView!)
|
|
newMessageViewTopConstraint = $0.top.equalTo(chatInputView.snp.top).constraint
|
|
}
|
|
|
|
chatInputView.snp.makeConstraints {
|
|
$0.leading.trailing.equalTo(view)
|
|
$0.top.greaterThanOrEqualTo(view).offset(Constants.InputViewTopOffset)
|
|
chatInputViewBottomConstraint = $0.bottom.equalTo(view).constraint
|
|
// TODO: this moves the input view a bit more to the top, because the home button "line" is in the way otherwise
|
|
// please fix me properly in the future
|
|
if #available(iOS 11.0, *) {
|
|
let keyWindow = UIApplication.shared.keyWindow
|
|
let b = keyWindow?.safeAreaInsets.bottom
|
|
chatInputViewBottomConstraint?.update(offset: -(b ?? 20))
|
|
}
|
|
}
|
|
|
|
editMessagesToolbar.snp.makeConstraints {
|
|
$0.edges.equalTo(chatInputView)
|
|
}
|
|
}
|
|
|
|
func addMessagesNotification() {
|
|
self.messagesToken = messages.addNotificationBlock { [unowned self] change in
|
|
guard let tableView = self.tableView else {
|
|
return
|
|
}
|
|
switch change {
|
|
case .initial:
|
|
break
|
|
case .update(_, let deletions, let insertions, let modifications):
|
|
|
|
// TODO: this is a very bad workaround. when more than 1 message is incoming
|
|
// those would crash.
|
|
/*
|
|
tableView.beginUpdates()
|
|
self.updateTableViewWithDeletions(deletions)
|
|
self.updateTableViewWithInsertions(insertions)
|
|
self.updateTableViewWithModifications(modifications)
|
|
|
|
self.visibleMessages = self.visibleMessages + insertions.count - deletions.count
|
|
tableView.endUpdates()
|
|
*/
|
|
// now just reload the whole table to avoid the crash, until there is a fix
|
|
tableView.reloadData()
|
|
|
|
self.updateTableHeaderView()
|
|
|
|
if insertions.contains(0) {
|
|
self.handleNewMessage()
|
|
}
|
|
case .error(let error):
|
|
fatalError("\(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateTableViewWithDeletions(_ deletions: [Int]) {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
|
|
for index in deletions {
|
|
if index >= visibleMessages {
|
|
continue
|
|
}
|
|
let indexPath = IndexPath(row: index, section: 0)
|
|
tableView.deleteRows(at: [indexPath], with: .top)
|
|
}
|
|
}
|
|
|
|
func updateTableViewWithInsertions(_ insertions: [Int]) {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
|
|
for index in insertions {
|
|
if index >= visibleMessages {
|
|
continue
|
|
}
|
|
let indexPath = IndexPath(row: index, section: 0)
|
|
tableView.insertRows(at: [indexPath], with: .top)
|
|
}
|
|
}
|
|
|
|
func updateTableViewWithModifications(_ modifications: [Int]) {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
|
|
for index in modifications {
|
|
if index >= visibleMessages {
|
|
continue
|
|
}
|
|
let message = messages[index]
|
|
let indexPath = IndexPath(row: index, section: 0)
|
|
|
|
if message.messageFile == nil {
|
|
tableView.reloadRows(at: [indexPath], with: .none)
|
|
continue
|
|
}
|
|
|
|
guard let cell = tableView.cellForRow(at: indexPath) as? ChatGenericFileCell else {
|
|
continue
|
|
}
|
|
|
|
let model = ChatIncomingFileCellModel()
|
|
prepareFileCell(cell, andModel: model, withMessage: message)
|
|
cell.setupWithTheme(theme, model: model)
|
|
|
|
maybeLoadImageForCellAtPath(cell, indexPath: indexPath)
|
|
}
|
|
}
|
|
|
|
func addFriendNotification() {
|
|
guard let friend = self.friend else {
|
|
titleView.name = String(localized: "contact_deleted")
|
|
titleView.userStatus = UserStatus(connectionStatus: .none, userStatus: .none)
|
|
titleView.connectionStatus = ConnectionStatus(connectionStatus: .none)
|
|
audioButton.isEnabled = true
|
|
videoButton.isEnabled = false
|
|
// locationButton.isEnabled = true
|
|
chatInputView.cameraButtonEnabled = false
|
|
return
|
|
}
|
|
|
|
titleView.name = friend.nickname
|
|
titleView.userStatus = UserStatus(connectionStatus: friend.connectionStatus, userStatus: friend.status)
|
|
titleView.connectionStatus = ConnectionStatus(connectionStatus: friend.connectionStatus)
|
|
|
|
let predicate = NSPredicate(format: "uniqueIdentifier == %@", friend.uniqueIdentifier)
|
|
let results = submanagerObjects.friends(predicate: predicate)
|
|
|
|
friendToken = results.addNotificationBlock { [unowned self] change in
|
|
guard let friend = self.friend else {
|
|
return
|
|
}
|
|
|
|
switch change {
|
|
case .initial:
|
|
fallthrough
|
|
case .update:
|
|
self.titleView.name = friend.nickname
|
|
self.titleView.userStatus = UserStatus(connectionStatus: friend.connectionStatus, userStatus: friend.status)
|
|
self.titleView.connectionStatus = ConnectionStatus(connectionStatus: friend.connectionStatus)
|
|
|
|
let isConnected = friend.isConnected
|
|
|
|
self.audioButton.isEnabled = true
|
|
self.videoButton.isEnabled = isConnected
|
|
//self.locationButton.isEnabled = true
|
|
self.chatInputView.cameraButtonEnabled = isConnected
|
|
|
|
self.updateTableHeaderView()
|
|
case .error(let error):
|
|
fatalError("\(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateTableHeaderView() {
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
|
|
guard let friend = friend else {
|
|
// tableView.tableHeaderView = nil
|
|
return
|
|
}
|
|
|
|
UIView.animate(withDuration: Constants.NewMessageViewAnimationDuration, animations: {
|
|
if friend.isConnected {
|
|
if friend.isTyping {
|
|
self.tableViewToChatInputConstraint.deactivate()
|
|
self.typingViewToChatInputConstraint.activate()
|
|
self.typingHeaderView.isHidden = false
|
|
self.typingHeaderView.startAnimation()
|
|
}
|
|
else {
|
|
self.tableViewToChatInputConstraint.activate()
|
|
self.typingViewToChatInputConstraint.deactivate()
|
|
self.typingHeaderView.isHidden = true
|
|
self.typingHeaderView.stopAnimation()
|
|
}
|
|
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
else {
|
|
// let predicate = NSPredicate(format: "messageText.isDelivered == NO AND senderUniqueIdentifier == nil")
|
|
// let hasUnsendMessages = self.messages.objects(with: predicate).count > 0
|
|
|
|
// tableView.tableHeaderView = hasUnsendMessages ? self.fauxOfflineHeaderView : nil
|
|
}
|
|
})
|
|
|
|
updateTableHeaderViewLayout()
|
|
}
|
|
|
|
func updateTableHeaderViewLayout() {
|
|
guard let headerView = tableView?.tableHeaderView else {
|
|
return
|
|
}
|
|
|
|
headerView.setNeedsLayout()
|
|
headerView.layoutIfNeeded()
|
|
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
|
|
headerView.frame.size.height = height
|
|
tableView?.tableHeaderView = headerView
|
|
}
|
|
|
|
func updateInputViewMaxHeight() {
|
|
chatInputView.maxHeight = chatInputView.frame.maxY - Constants.InputViewTopOffset
|
|
}
|
|
|
|
func handleNewMessage() {
|
|
if UIApplication.isActive {
|
|
updateLastReadDate()
|
|
}
|
|
|
|
guard let tableView = tableView else {
|
|
return
|
|
}
|
|
guard let visible = tableView.indexPathsForVisibleRows else {
|
|
return
|
|
}
|
|
|
|
let first = IndexPath(row: 0, section: 0)
|
|
|
|
if !visible.contains(first) {
|
|
toggleNewMessageView(show: true)
|
|
}
|
|
}
|
|
|
|
func toggleNewMessageView(show: Bool) {
|
|
guard show != newMessagesViewVisible else {
|
|
return
|
|
}
|
|
newMessagesViewVisible = show
|
|
|
|
if show {
|
|
newMessagesView.isHidden = false
|
|
}
|
|
|
|
UIView.animate(withDuration: Constants.NewMessageViewAnimationDuration, animations: {
|
|
if show {
|
|
self.newMessageViewTopConstraint?.update(offset: Constants.NewMessageViewTopOffset - self.newMessagesView.frame.size.height)
|
|
}
|
|
else {
|
|
self.newMessageViewTopConstraint?.update(offset: 0.0)
|
|
}
|
|
|
|
self.view.layoutIfNeeded()
|
|
|
|
}, completion: { finished in
|
|
if !show {
|
|
self.newMessagesView.isHidden = true
|
|
}
|
|
})
|
|
}
|
|
|
|
func updateLastReadDate() {
|
|
//TODO(Tha_14): unwrapping optional value nil error here needs to be handled
|
|
submanagerObjects.change(chat, lastReadDateInterval: Date().timeIntervalSince1970)
|
|
}
|
|
|
|
func imageCellWithMessage(_ message: OCTMessageAbstract, incoming: Bool) -> (ChatMovableDateCellModel, ChatMovableDateCell) {
|
|
let cell: ChatGenericFileCell
|
|
|
|
if incoming {
|
|
cell = tableView!.dequeueReusableCell(withIdentifier: ChatIncomingFileCell.staticReuseIdentifier) as! ChatIncomingFileCell
|
|
}
|
|
else {
|
|
cell = tableView!.dequeueReusableCell(withIdentifier: ChatOutgoingFileCell.staticReuseIdentifier) as! ChatOutgoingFileCell
|
|
}
|
|
let model = ChatIncomingFileCellModel()
|
|
|
|
prepareFileCell(cell, andModel: model, withMessage: message)
|
|
|
|
return (model, cell)
|
|
}
|
|
|
|
func prepareFileCell(_ cell: ChatGenericFileCell, andModel model: ChatGenericFileCellModel, withMessage message: OCTMessageAbstract) {
|
|
cell.progressObject = nil
|
|
|
|
model.fileName = message.messageFile!.fileName
|
|
model.fileSize = ByteCountFormatter.string(fromByteCount: message.messageFile!.fileSize, countStyle: .file)
|
|
model.fileUTI = message.messageFile!.fileUTI
|
|
|
|
switch message.messageFile!.fileType {
|
|
case .waitingConfirmation:
|
|
model.state = .waitingConfirmation
|
|
case .loading:
|
|
model.state = .loading
|
|
|
|
let bridge = ChatProgressBridge()
|
|
cell.progressObject = bridge
|
|
_ = try? self.submanagerFiles.add(bridge, forFileTransfer: message)
|
|
case .paused:
|
|
model.state = .paused
|
|
case .canceled:
|
|
model.state = .cancelled
|
|
case .ready:
|
|
model.state = .done
|
|
}
|
|
|
|
if !message.isOutgoing() {
|
|
model.startLoadingHandle = { [weak self] in
|
|
self?.submanagerFiles.acceptFileTransfer(message) { (error: Error) -> Void in
|
|
handleErrorWithType(.acceptIncomingFile, error: error as NSError)
|
|
}
|
|
}
|
|
}
|
|
|
|
model.cancelHandle = { [weak self] in
|
|
do {
|
|
try self?.submanagerFiles.cancelFileTransfer(message)
|
|
}
|
|
catch let error as NSError {
|
|
handleErrorWithType(.cancelFileTransfer, error: error)
|
|
}
|
|
}
|
|
|
|
model.retryHandle = { [weak self] in
|
|
self?.submanagerFiles.retrySendingFile(message) { (error: Error) in
|
|
handleErrorWithType(.sendFileToFriend, error: error as NSError)
|
|
}
|
|
}
|
|
|
|
model.pauseOrResumeHandle = { [weak self] in
|
|
let isPaused = (message.messageFile!.pausedBy.rawValue & OCTMessageFilePausedBy.user.rawValue) != 0
|
|
|
|
do {
|
|
try self?.submanagerFiles.pauseFileTransfer(!isPaused, message: message)
|
|
}
|
|
catch let error as NSError {
|
|
handleErrorWithType(.cancelFileTransfer, error: error)
|
|
}
|
|
}
|
|
|
|
model.openHandle = { [weak self] in
|
|
guard let sself = self else {
|
|
return
|
|
}
|
|
let qlDataSource = FilePreviewControllerDataSource(chat: sself.chat, submanagerObjects: sself.submanagerObjects)
|
|
guard let index = qlDataSource.indexOfMessage(message) else {
|
|
return
|
|
}
|
|
|
|
sself.delegate?.chatPrivateControllerShowQuickLookController(sself, dataSource: qlDataSource, selectedIndex: index)
|
|
}
|
|
}
|
|
|
|
func maybeLoadImageForCellAtPath(_ cell: UITableViewCell, indexPath: IndexPath) {
|
|
let message = messages[indexPath.row]
|
|
|
|
guard let messageFile = message.messageFile else {
|
|
return
|
|
}
|
|
|
|
guard UTTypeConformsTo(messageFile.fileUTI as CFString? ?? "" as CFString, kUTTypeImage) else {
|
|
return
|
|
}
|
|
|
|
guard let file = messageFile.filePath() else {
|
|
return
|
|
}
|
|
|
|
if messageFile.fileSize >= Constants.MaxImageSizeToShowInline {
|
|
return
|
|
}
|
|
|
|
if let image = imageCache.object(forKey: file as AnyObject) as? UIImage {
|
|
let cell = (cell as? ChatIncomingFileCell) ?? (cell as? ChatOutgoingFileCell)
|
|
|
|
cell?.setButtonImage(image)
|
|
}
|
|
else {
|
|
loadImageForCellAtIndexPath(indexPath, fromFile: file)
|
|
}
|
|
}
|
|
|
|
func loadImageForCellAtIndexPath(_ indexPath: IndexPath, fromFile: String) {
|
|
DispatchQueue.global(qos: .default).async { [weak self] in
|
|
guard var image = UIImage(contentsOfFile: fromFile) else {
|
|
return
|
|
}
|
|
|
|
var size = image.size
|
|
guard size.width > 0 || size.height > 0 else {
|
|
return
|
|
}
|
|
|
|
let delta = (size.width > size.height) ? (Constants.MaxInlineImageSide / size.width) : (Constants.MaxInlineImageSide / size.height)
|
|
|
|
size.width *= delta
|
|
size.height *= delta
|
|
|
|
image = image.scaleToSize(size)
|
|
self?.imageCache.setObject(image, forKey: fromFile as AnyObject)
|
|
|
|
DispatchQueue.main.async {
|
|
let optionalCell = self?.tableView?.cellForRow(at: indexPath)
|
|
guard let cell = (optionalCell as? ChatIncomingFileCell) ?? (optionalCell as? ChatOutgoingFileCell) else {
|
|
return
|
|
}
|
|
|
|
cell.setButtonImage(image)
|
|
}
|
|
}
|
|
}
|
|
|
|
func toggleTableViewEditing(_ editing: Bool, animated: Bool) {
|
|
tableView?.setEditing(editing, animated: animated)
|
|
|
|
tableViewTapGestureRecognizer.isEnabled = !editing
|
|
editMessagesToolbar.isHidden = !editing
|
|
|
|
if editing {
|
|
_ = chatInputView.resignFirstResponder()
|
|
|
|
navigationItem.leftBarButtonItems = [UIBarButtonItem(
|
|
title: String(localized: "delete_all_messages"),
|
|
style: .plain,
|
|
target: self,
|
|
action: #selector(ChatPrivateController.deleteAllMessagesButtonPressed(_:)))]
|
|
|
|
navigationItem.rightBarButtonItems = [UIBarButtonItem(
|
|
barButtonSystemItem: .cancel,
|
|
target: self,
|
|
action: #selector(ChatPrivateController.cancelEditingButtonPressed))]
|
|
}
|
|
else {
|
|
let audioImage = UIImage(named: "start-call-medium")!
|
|
let videoImage = UIImage(named: "video-call-medium")!
|
|
let locationImage = UIImage(named: "location-call-medium")!.withRenderingMode(.alwaysOriginal)
|
|
audioButton = UIBarButtonItem(image: audioImage, style: .plain, target: self, action: #selector(ChatPrivateController.audioCallButtonPressed))
|
|
videoButton = UIBarButtonItem(image: videoImage, style: .plain, target: self, action: #selector(ChatPrivateController.videoCallButtonPressed))
|
|
// locationButton = UIBarButtonItem(image: locationImage, style: .plain, target: self, action: #selector(ChatPrivateController.locationButtonPressed))
|
|
|
|
// if (AppDelegate.location_sharing_contact_pubkey != "-1") {
|
|
// let locationImage = UIImage(named: "location-call-activated-medium")!.withRenderingMode(.alwaysOriginal)
|
|
// locationButton.setBackgroundImage(locationImage, for: .normal, barMetrics: .default)
|
|
// }
|
|
|
|
navigationItem.leftBarButtonItems = nil
|
|
navigationItem.rightBarButtonItems = [
|
|
videoButton,
|
|
audioButton//,
|
|
// locationButton
|
|
]
|
|
}
|
|
}
|
|
|
|
func showMessageDeletionConfirmation(messagesCount: Int,
|
|
showFromItem barButtonItem: UIBarButtonItem,
|
|
deleteClosure: @escaping () -> Void) {
|
|
let deleteButtonText = messagesCount > 1 ?
|
|
String(localized: "delete_multiple_messages") + " (\(messagesCount))" :
|
|
String(localized: "delete_single_message")
|
|
|
|
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
|
alert.popoverPresentationController?.barButtonItem = barButtonItem
|
|
|
|
alert.addAction(UIAlertAction(title: deleteButtonText, style: .destructive) { _ -> Void in
|
|
deleteClosure()
|
|
})
|
|
|
|
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .cancel, handler: nil))
|
|
|
|
present(alert, animated: true, completion: nil)
|
|
}
|
|
}
|