// // ConversationsViewController.swift // Yep // // Created by NIX on 15/3/16. // Copyright (c) 2015年 Catch Inc. All rights reserved. // import UIKit import RealmSwift import Navi import Kingfisher import Proposer let YepNotificationCommentAction = "YepNotificationCommentAction" let YepNotificationOKAction = "YepNotificationOKAction" class ConversationsViewController: UIViewController { private lazy var activityIndicatorTitleView = ActivityIndicatorTitleView(frame: CGRect(x: 0, y: 0, width: 120, height: 30)) @IBOutlet weak var conversationsTableView: UITableView! private let feedConversationDockCellID = "FeedConversationDockCell" private let cellIdentifier = "ConversationCell" private var realm: Realm! private var realmNotificationToken: NotificationToken? private var haveUnreadMessages = false { didSet { if haveUnreadMessages != oldValue { if haveUnreadMessages { navigationController?.tabBarItem.image = UIImage(named: "icon_chat_unread") navigationController?.tabBarItem.selectedImage = UIImage(named: "icon_chat_active_unread") } else { navigationController?.tabBarItem.image = UIImage(named: "icon_chat") navigationController?.tabBarItem.selectedImage = UIImage(named: "icon_chat_active") } } } } private lazy var noConversationFooterView: InfoView = InfoView(NSLocalizedString("Have a nice day!", comment: "")) private var noConversation = false { didSet { if noConversation != oldValue { conversationsTableView.tableFooterView = UIView() } } } #if DEBUG private lazy var conversationsFPSLabel: FPSLabel = { let label = FPSLabel() return label }() #endif private lazy var conversations: Results = { let predicate = NSPredicate(format: "type = %d", ConversationType.OneToOne.rawValue) return self.realm.objects(Conversation).filter(predicate).sorted("updatedUnixTime", ascending: false) }() private struct Listener { static let Nickname = "ConversationsViewController.Nickname" static let Avatar = "ConversationsViewController.Avatar" static let isFetchingUnreadMessages = "ConversationsViewController.isFetchingUnreadMessages" } deinit { NSNotificationCenter.defaultCenter().removeObserver(self) YepUserDefaults.avatarURLString.removeListenerWithName(Listener.Avatar) YepUserDefaults.nickname.removeListenerWithName(Listener.Nickname) isFetchingUnreadMessages.removeListenerWithName(Listener.isFetchingUnreadMessages) conversationsTableView?.delegate = nil println("deinit Conversations") } override func viewDidLoad() { super.viewDidLoad() realm = try! Realm() NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadConversationsTableView", name: YepConfig.Notification.newMessages, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadConversationsTableView", name: YepConfig.Notification.changedConversation, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadConversationsTableView", name: YepConfig.Notification.markAsReaded, object: nil) // 确保自己发送消息的时候,会话列表也会刷新,避免时间戳不一致 NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadConversationsTableView", name: MessageNotification.MessageStateChanged, object: nil) YepUserDefaults.nickname.bindListener(Listener.Nickname) { [weak self] _ in dispatch_async(dispatch_get_main_queue()) { self?.reloadConversationsTableView() } } YepUserDefaults.avatarURLString.bindListener(Listener.Avatar) { [weak self] _ in dispatch_async(dispatch_get_main_queue()) { self?.reloadConversationsTableView() } } navigationItem.titleView = activityIndicatorTitleView isFetchingUnreadMessages.bindListener(Listener.isFetchingUnreadMessages) { [weak self] isFetching in dispatch_async(dispatch_get_main_queue()) { self?.activityIndicatorTitleView.state = isFetching ? .Active : .Normal } } view.backgroundColor = UIColor.whiteColor() conversationsTableView.separatorColor = UIColor.yepCellSeparatorColor() conversationsTableView.separatorInset = YepConfig.ContactsCell.separatorInset conversationsTableView.registerNib(UINib(nibName: feedConversationDockCellID, bundle: nil), forCellReuseIdentifier: feedConversationDockCellID) conversationsTableView.registerNib(UINib(nibName: cellIdentifier, bundle: nil), forCellReuseIdentifier: cellIdentifier) conversationsTableView.rowHeight = 80 conversationsTableView.tableFooterView = UIView() noConversation = conversations.isEmpty realmNotificationToken = realm.addNotificationBlock { [weak self] notification, realm in if let strongSelf = self { let haveOneToOneUnreadMessages = countOfUnreadMessagesInRealm(realm, withConversationType: .OneToOne) > 0 strongSelf.haveUnreadMessages = haveOneToOneUnreadMessages || (countOfUnreadMessagesInRealm(realm, withConversationType: .Group) > 0) strongSelf.noConversation = countOfConversationsInRealm(realm) == 0 } } if PrivateResource.Location(.WhenInUse).isAuthorized { YepLocationService.turnOn() } #if DEBUG view.addSubview(conversationsFPSLabel) #endif } private func cacheInAdvance() { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { // 聊天界面的小头像 for user in normalUsers() { let userAvatar = UserAvatar(userID: user.userID, avatarStyle: nanoAvatarStyle) AvatarPod.wakeAvatar(userAvatar, completion: { _, _, _ in }) } /* // 每个对话的最近 10 条消息(image or thumbnail) guard let realm = try? Realm() else { return } for conversation in realm.objects(Conversation) { let messages = messagesOfConversation(conversation, inRealm: realm) let latestBatch = min(10, messages.count) let messageImagePreferredWidth = YepConfig.ChatCell.mediaPreferredWidth let messageImagePreferredHeight = YepConfig.ChatCell.mediaPreferredHeight for i in (messages.count - latestBatch)..= 1 { ImageCache.sharedInstance.imageOfMessage(message, withSize: CGSize(width: messageImagePreferredWidth, height: ceil(messageImagePreferredWidth / aspectRatio)), tailDirection: tailDirection, completion: { _ in }) } else { ImageCache.sharedInstance.imageOfMessage(message, withSize: CGSize(width: messageImagePreferredHeight * aspectRatio, height: messageImagePreferredHeight), tailDirection: tailDirection, completion: { _ in }) } } case MessageMediaType.Video.rawValue: if let (videoWidth, videoHeight) = videoMetaOfMessage(message) { let aspectRatio = videoWidth / videoHeight let messageImagePreferredWidth = max(messageImagePreferredWidth, ceil(YepConfig.ChatCell.mediaMinHeight * aspectRatio)) let messageImagePreferredHeight = max(messageImagePreferredHeight, ceil(YepConfig.ChatCell.mediaMinWidth / aspectRatio)) if aspectRatio >= 1 { ImageCache.sharedInstance.imageOfMessage(message, withSize: CGSize(width: messageImagePreferredWidth, height: ceil(messageImagePreferredWidth / aspectRatio)), tailDirection: tailDirection, completion: { _ in }) } else { ImageCache.sharedInstance.imageOfMessage(message, withSize: CGSize(width: messageImagePreferredHeight * aspectRatio, height: messageImagePreferredHeight), tailDirection: tailDirection, completion: { _ in }) } } default: break } } } } */ } } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) delay(0.5) { [weak self] in self?.askForNotification() } // 预先生成小头像 cacheInAdvance() } private func askForNotification() { if #available(iOS 9.0, *) { let replyAction = UIMutableUserNotificationAction() replyAction.title = NSLocalizedString("Reply", comment: "") replyAction.identifier = YepNotificationCommentAction replyAction.activationMode = .Background replyAction.behavior = .TextInput replyAction.authenticationRequired = false let replyOKAction = UIMutableUserNotificationAction() replyOKAction.title = "OK" replyOKAction.identifier = YepNotificationOKAction replyOKAction.activationMode = .Background replyOKAction.behavior = .Default replyOKAction.authenticationRequired = false let category = UIMutableUserNotificationCategory() category.identifier = "YepMessageNotification" category.setActions([replyAction, replyOKAction], forContext: UIUserNotificationActionContext.Minimal) APService.registerForRemoteNotificationTypes( UIUserNotificationType.Badge.rawValue | UIUserNotificationType.Sound.rawValue | UIUserNotificationType.Alert.rawValue, categories: [category]) } else { // 这里才开始向用户提示推送 APService.registerForRemoteNotificationTypes( UIUserNotificationType.Badge.rawValue | UIUserNotificationType.Sound.rawValue | UIUserNotificationType.Alert.rawValue, categories: nil) } } // MARK: Navigation override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool { guard navigationController?.topViewController == self else { return false } return true } override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "showConversation" { let vc = segue.destinationViewController as! ConversationViewController let conversation = sender as! Conversation vc.conversation = conversation vc.afterSentMessageAction = { // 自己发送消息后,更新 Cell dispatch_async(dispatch_get_main_queue()) { [weak self] in guard let row = self?.conversations.indexOf(conversation) else { return } let indexPath = NSIndexPath(forRow: row, inSection: Section.Conversation.rawValue) if let cell = self?.conversationsTableView.cellForRowAtIndexPath(indexPath) as? ConversationCell { cell.updateInfoLabels() } } } } } // MARK: Actions @objc private func reloadConversationsTableView() { dispatch_async(dispatch_get_main_queue()) { self.conversationsTableView.reloadData() } } } // MARK: - UITableViewDataSource, UITableViewDelegat extension ConversationsViewController: UITableViewDataSource, UITableViewDelegate { private enum Section: Int { case FeedConversation case Conversation } func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 2 } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case Section.FeedConversation.rawValue: return 1 case Section.Conversation.rawValue: return conversations.count default: return 0 } } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { switch indexPath.section { case Section.FeedConversation.rawValue: let cell = tableView.dequeueReusableCellWithIdentifier(feedConversationDockCellID) as! FeedConversationDockCell cell.haveGroupUnreadMessages = countOfUnreadMessagesInRealm(realm, withConversationType: ConversationType.Group) > 0 // 先找最新且未读的消息 let latestUnreadMessage = latestUnreadMessageInRealm(realm, withConversationType: ConversationType.Group) // 找不到就找最新的消息 if let latestMessage = (latestUnreadMessage ?? latestMessageInRealm(realm, withConversationType: ConversationType.Group)) { if let mediaType = MessageMediaType(rawValue: latestMessage.mediaType), placeholder = mediaType.placeholder { cell.chatLabel.text = placeholder } else { cell.chatLabel.text = latestMessage.nicknameWithTextContent } } else { cell.chatLabel.text = NSLocalizedString("No messages yet.", comment: "") } return cell case Section.Conversation.rawValue: let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! ConversationCell if let conversation = conversations[safe: indexPath.row] { let radius = YepConfig.ConversationCell.avatarSize * 0.5 cell.configureWithConversation(conversation, avatarRadius: radius, tableView: tableView, indexPath: indexPath) } return cell default: return UITableViewCell() } } func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { defer { tableView.deselectRowAtIndexPath(indexPath, animated: true) } guard navigationController?.topViewController == self else { return } switch indexPath.section { case Section.FeedConversation.rawValue: performSegueWithIdentifier("showFeedConversations", sender: nil) case Section.Conversation.rawValue: if let cell = tableView.cellForRowAtIndexPath(indexPath) as? ConversationCell { performSegueWithIdentifier("showConversation", sender: cell.conversation) } default: break } } // Edit (for Delete) func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { if indexPath.section == Section.Conversation.rawValue { return true } return false } func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == .Delete { guard let conversation = conversations[safe: indexPath.row] else { tableView.setEditing(false, animated: true) return } tryDeleteOrClearHistoryOfConversation(conversation, inViewController: self, whenAfterClearedHistory: { tableView.setEditing(false, animated: true) // update cell if let cell = tableView.cellForRowAtIndexPath(indexPath) as? ConversationCell { if let conversation = self.conversations[safe: indexPath.row] { let radius = min(CGRectGetWidth(cell.avatarImageView.bounds), CGRectGetHeight(cell.avatarImageView.bounds)) * 0.5 cell.configureWithConversation(conversation, avatarRadius: radius, tableView: tableView, indexPath: indexPath) } } }, afterDeleted: { tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) }, orCanceled: { tableView.setEditing(false, animated: true) }) } } }