pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author buff <andreas@pep-project.org>
Wed, 04 Oct 2017 19:37:41 +0200
branchIOS-700-sluggish-ui
changeset 3166 eb086ae49145
parent 3165 386d43a8738e
child 3168 0c7016866858
permissions -rw-r--r--
IOS-744 cleanup
     1 //
     2 //  EmailListViewController.swift
     3 //  pEpForiOS
     4 //
     5 //  Created by Dirk Zimmermann on 16/04/16.
     6 //  Copyright © 2016 p≡p Security S.A. All rights reserved.
     7 //
     8 
     9 import UIKit
    10 import MessageModel
    11 
    12 class EmailListViewController: BaseTableViewController {
    13     private var _folderToShow: Folder?
    14     var folderToShow: Folder? {
    15         set {
    16             if newValue === _folderToShow {
    17                 return
    18             }
    19             if newValue == nil {
    20                 model = nil
    21                 _folderToShow = newValue
    22                 return
    23             }
    24             _folderToShow = newValue
    25             // Update the model to data of new folder/filter
    26             resetModel()
    27         }
    28         get {
    29             Log.shared.errorAndCrash(component: #function,
    30                                      errorString: "Use only the folderToShow from model (model?folderToShow).")
    31             return _folderToShow
    32         }
    33     }
    34 
    35     func updateLastLookAt() {
    36         guard let saveFolder = model?.folderToShow else {
    37             return
    38         }
    39         saveFolder.updateLastLookAt()
    40     }
    41 
    42     private var model: EmailListViewModel?
    43 
    44     private let queue: OperationQueue = {
    45         let createe = OperationQueue()
    46         createe.qualityOfService = .userInteractive
    47         createe.maxConcurrentOperationCount = 10
    48         return createe
    49     }()
    50     private var operations = [IndexPath:Operation]()
    51     public static let storyboardId = "EmailListViewController"
    52     fileprivate var lastSelectedIndexPath: IndexPath?
    53 
    54     let searchController = UISearchController(searchResultsController: nil)
    55 
    56     // MARK: - Outlets
    57 
    58     @IBOutlet weak var enableFilterButton: UIBarButtonItem!
    59     @IBOutlet weak var textFilterButton: UIBarButtonItem!
    60     @IBOutlet var showFoldersButton: UIBarButtonItem!
    61 
    62     // MARK: - Life Cycle
    63 
    64     override func viewDidLoad() {
    65         super.viewDidLoad()
    66         title = NSLocalizedString("Inbox", comment: "General name for (unified) inbox")
    67         UIHelper.emailListTableHeight(self.tableView)
    68         self.textFilterButton.isEnabled = false
    69         addSearchBar()
    70     }
    71 
    72     override func viewWillAppear(_ animated: Bool) {
    73         super.viewWillAppear(animated)
    74         self.navigationController?.setToolbarHidden(false, animated: true)
    75         if MiscUtil.isUnitTest() {
    76             return
    77         }
    78 
    79         //BUFF: TODO
    80         if let vm = model {
    81             self.textFilterButton.isEnabled = vm.isFilterEnabled
    82             updateFilterText()
    83         } else {
    84             self.textFilterButton.isEnabled = false
    85         }
    86 
    87         setDefaultColors()
    88         setup()
    89         updateView()
    90 
    91         // Mark this folder as having been looked at by the user
    92         updateLastLookAt()
    93         setupFoldersBarButton()
    94     }
    95 
    96     // MARK: - NavigationBar
    97 
    98     private func hideFoldersNavigationBarButton() {
    99         self.showFoldersButton.isEnabled = false
   100         self.showFoldersButton.tintColor = UIColor.clear
   101     }
   102 
   103     private func showFoldersNavigationBarButton() {
   104         self.showFoldersButton.isEnabled = true
   105         self.showFoldersButton.tintColor = nil
   106     }
   107 
   108     private func resetModel() {
   109         if _folderToShow != nil {
   110             model = EmailListViewModel(delegate: self, folderToShow: _folderToShow)
   111         }
   112     }
   113 
   114     private func setup() {
   115         // We have not been created to show a specific folder, thus we show unified inbox
   116         if model?.folderToShow == nil {
   117             folderToShow = UnifiedInbox()
   118         }
   119 
   120         if noAccountsExist() {
   121             performSegue(withIdentifier:.segueAddNewAccount, sender: self)
   122         }
   123         self.title = realNameOfFolderToShow()
   124     }
   125 
   126     private func noAccountsExist() -> Bool {
   127         return Account.all().isEmpty
   128     }
   129 
   130     private func setupFoldersBarButton() {
   131         if let size = navigationController?.viewControllers.count, size > 1 {
   132             hideFoldersNavigationBarButton()
   133         } else {
   134             showFoldersNavigationBarButton()
   135         }
   136     }
   137 
   138     private func addSearchBar() {
   139         searchController.searchResultsUpdater = self
   140         searchController.dimsBackgroundDuringPresentation = false
   141         searchController.delegate = self
   142         definesPresentationContext = true
   143         tableView.tableHeaderView = searchController.searchBar
   144         tableView.setContentOffset(CGPoint(x: 0.0, y: 40.0), animated: false)
   145     }
   146 
   147     // MARK: - Other
   148 
   149     private func realNameOfFolderToShow() -> String? {
   150         return model?.folderToShow?.realName
   151     }
   152 
   153     private func configure(cell: EmailListViewCell, for indexPath: IndexPath) {
   154         // Configure lightweight stuff on main thread ...
   155         guard let saveModel = model else {
   156             return
   157         }
   158         guard let row = saveModel.row(for: indexPath) else {
   159             Log.shared.errorAndCrash(component: #function, errorString: "We should have a row here")
   160             return
   161         }
   162         cell.senderLabel.text = row.from
   163         cell.subjectLabel.text = row.subject
   164         cell.summaryLabel.text = row.bodyPeek
   165         cell.isFlagged = row.isFlagged
   166         cell.isSeen = row.isSeen
   167         cell.hasAttachment = row.showAttchmentIcon
   168         cell.dateLabel.text = row.dateText
   169         // Set image from cache if any
   170         cell.setContactImage(image: row.senderContactImage)
   171 
   172         let op = BlockOperation() { [weak self] in
   173             // ... and expensive computations in background
   174             guard let strongSelf = self else {
   175                 // View is gone, nothing to do.
   176                 return
   177             }
   178 
   179             var senderImage: UIImage?
   180             if row.senderContactImage == nil {
   181                 // image for identity has not been cached yet, get and cache it
   182                 senderImage = strongSelf.model?.senderImage(forCellAt: indexPath)
   183             }
   184 
   185             // Set data on cell on main queue.
   186             // In theory we want to set all data in *one* async call. But as pEpRatingColorImage takes
   187             // very long, we are setting the sender image seperatelly.
   188             DispatchQueue.main.async {
   189                 if senderImage != nil {
   190                     cell.contactImageView.image  = senderImage
   191                 }
   192             }
   193 
   194             let pEpRatingImage = strongSelf.model?.pEpRatingColorImage(forCellAt: indexPath)
   195 
   196             // Set data on cell on main queue, again ...
   197             DispatchQueue.main.async {
   198                 if pEpRatingImage != nil {
   199                     cell.setPepRatingImage(image: pEpRatingImage)
   200                 }
   201             }
   202         }
   203         queue(operation: op, for: indexPath)
   204     }
   205 
   206     // MARK: - Actions
   207 
   208     @IBAction func filterButtonHasBeenPressed(_ sender: UIBarButtonItem) {
   209         guard let vm = model else {
   210             Log.shared.errorAndCrash(component: #function, errorString: "We should have a model here")
   211             return
   212         }
   213         vm.isFilterEnabled = !vm.isFilterEnabled
   214         upadteFilterButtonView()
   215     }
   216 
   217     func upadteFilterButtonView() {
   218         guard let vm = model else {
   219             Log.shared.errorAndCrash(component: #function, errorString: "We should have a model here")
   220             return
   221         }
   222 
   223         textFilterButton.isEnabled = vm.isFilterEnabled
   224         if textFilterButton.isEnabled {
   225             enableFilterButton.image = UIImage(named: "unread-icon-active")
   226             updateFilterText()
   227         } else {
   228             textFilterButton.title = ""
   229             enableFilterButton.image = UIImage(named: "unread-icon")
   230         }
   231     }
   232 
   233     func updateFilterText() {
   234         if let vm = model, let txt = vm.activeFilter?.text {
   235             textFilterButton.title = "Filter by: " + txt
   236         }
   237     }
   238 
   239     // MARK: - UITableViewDataSource
   240 
   241     override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   242         return model?.rowCount ?? 0
   243     }
   244 
   245     override func tableView(_ tableView: UITableView,
   246                             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   247         guard let cell = tableView.dequeueReusableCell(withIdentifier: EmailListViewCell.storyboardId,
   248                                                        for: indexPath) as? EmailListViewCell
   249             else {
   250                 Log.shared.errorAndCrash(component: #function, errorString: "Wrong cell!")
   251                 return UITableViewCell()
   252         }
   253         configure(cell: cell, for: indexPath)
   254         return cell
   255     }
   256 
   257     // MARK: - UITableViewDelegate
   258 
   259     override func tableView(_ tableView: UITableView, editActionsForRowAt
   260         indexPath: IndexPath)-> [UITableViewRowAction]? {
   261         guard let flagAction = createFlagAction(forCellAt: indexPath),
   262             let deleteAction = createDeleteAction(forCellAt: indexPath),
   263             let moreAction = createMoreAction(forCellAt: indexPath) else {
   264                 Log.shared.errorAndCrash(component: #function, errorString: "Error creating action.")
   265                 return nil
   266         }
   267         return [deleteAction, flagAction, moreAction]
   268     }
   269 
   270     override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
   271         cancelOperation(for: indexPath)
   272     }
   273 
   274     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   275         lastSelectedIndexPath = indexPath
   276         tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
   277         performSegue(withIdentifier: SegueIdentifier.segueShowEmail, sender: self)
   278     }
   279 
   280     // MARK: - Queue Handling
   281 
   282     private func queue(operation op:Operation, for indexPath: IndexPath) {
   283         operations[indexPath] = op
   284         queue.addOperation(op)
   285     }
   286 
   287     private func cancelOperation(for indexPath:IndexPath) {
   288         guard let op = operations.removeValue(forKey: indexPath) else {
   289             return
   290         }
   291         if !op.isCancelled  {
   292             op.cancel()
   293         }
   294     }
   295 
   296     override func didReceiveMemoryWarning() {
   297         model?.freeMemory()
   298     }
   299 }
   300 
   301 // MARK: - UISearchResultsUpdating, UISearchControllerDelegate
   302 
   303 extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
   304     public func updateSearchResults(for searchController: UISearchController) {
   305         guard let vm = model, let searchText = searchController.searchBar.text else {
   306             return
   307         }
   308         vm.setSearchFilter(forSearchText: searchText)
   309     }
   310 
   311     func didDismissSearchController(_ searchController: UISearchController) {
   312         guard let vm = model else {
   313             return
   314         }
   315         vm.removeSearchFilter()
   316     }
   317 }
   318 
   319 // MARK: - EmailListModelDelegate
   320 
   321 extension EmailListViewController: EmailListViewModelDelegate {
   322     func emailListViewModel(viewModel: EmailListViewModel, didInsertDataAt indexPath: IndexPath) {
   323         tableView.beginUpdates()
   324         tableView.insertRows(at: [indexPath], with: .automatic)
   325         tableView.endUpdates()
   326     }
   327 
   328     func emailListViewModel(viewModel: EmailListViewModel, didRemoveDataAt indexPath: IndexPath) {
   329         tableView.beginUpdates()
   330         tableView.deleteRows(at: [indexPath], with: .automatic)
   331         tableView.endUpdates()
   332     }
   333 
   334     func emailListViewModel(viewModel: EmailListViewModel, didUpdateDataAt indexPath: IndexPath) {
   335         tableView.beginUpdates()
   336         tableView.reloadRows(at: [indexPath], with: .none)
   337         tableView.endUpdates()
   338     }
   339 
   340     func updateView() {
   341         self.tableView.reloadData()
   342     }
   343 }
   344 
   345 // MARK: - ActionSheet & ActionSheet Actions
   346 
   347 extension EmailListViewController {
   348     func showMoreActionSheet(forRowAt indexPath: IndexPath) {
   349         lastSelectedIndexPath = indexPath
   350         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   351         alertControler.view.tintColor = .pEpGreen
   352         let cancelAction = createCancelAction()
   353         let replyAction = createReplyAction()
   354         let replyAllAction = createReplyAllAction()
   355         let forwardAction = createForwardAction()
   356         alertControler.addAction(cancelAction)
   357         alertControler.addAction(replyAction)
   358         alertControler.addAction(replyAllAction)
   359         alertControler.addAction(forwardAction)
   360         if let popoverPresentationController = alertControler.popoverPresentationController {
   361             popoverPresentationController.sourceView = tableView
   362         }
   363         present(alertControler, animated: true, completion: nil)
   364     }
   365 
   366     // MARK: Action Sheet Actions
   367 
   368     func createCancelAction() -> UIAlertAction {
   369         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in
   370             self.tableView.beginUpdates()
   371             self.tableView.setEditing(false, animated: true)
   372             self.tableView.endUpdates()
   373         }
   374     }
   375 
   376     func createReplyAction() ->  UIAlertAction {
   377         return UIAlertAction(title: "Reply", style: .default) { (action) in
   378             self.performSegue(withIdentifier: .segueReply, sender: self)
   379         }
   380     }
   381 
   382     func createReplyAllAction() ->  UIAlertAction {
   383         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   384             self.performSegue(withIdentifier: .segueReplyAll, sender: self)
   385         }
   386     }
   387 
   388     func createForwardAction() -> UIAlertAction {
   389         return UIAlertAction(title: "Forward", style: .default) { (action) in
   390             self.performSegue(withIdentifier: .segueForward, sender: self)
   391         }
   392     }
   393 }
   394 
   395 // MARK: - TableViewCell Actions
   396 
   397 extension EmailListViewController {
   398     private func createRowAction(image: UIImage?,
   399                                  action: @escaping (UITableViewRowAction, IndexPath) -> Void,
   400                                  title: String) -> UITableViewRowAction {
   401         let rowAction = UITableViewRowAction(style: .normal, title: title, handler: action)
   402         if let theImage = image {
   403             let iconColor = UIColor(patternImage: theImage)
   404             rowAction.backgroundColor = iconColor
   405         }
   406         return rowAction
   407     }
   408 
   409     func createFlagAction(forCellAt indexPath: IndexPath) -> UITableViewRowAction? {
   410         guard let row = model?.row(for: indexPath) else {
   411             Log.shared.errorAndCrash(component: #function, errorString: "No data for indexPath!")
   412             return nil
   413         }
   414         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   415             if row.isFlagged {
   416                 model?.unsetFlagged(forIndexPath: indexPath)
   417             } else {
   418                 model?.setFlagged(forIndexPath: indexPath)
   419             }
   420             tableView.beginUpdates()
   421             tableView.setEditing(false, animated: true)
   422             tableView.reloadRows(at: [indexPath], with: .none)
   423             tableView.endUpdates()
   424         }
   425         let title: String
   426         if row.isFlagged{
   427             let unflagString = NSLocalizedString("Unflag", comment: "Message action (on swipe)")
   428             title = "\n\n\(unflagString)"
   429         } else {
   430             let flagString = NSLocalizedString("Flag", comment: "Message action (on swipe)")
   431             title = "\n\n\(flagString)"
   432         }
   433         return createRowAction(image: UIImage(named: "swipe-flag"), action: action, title: title)
   434     }
   435 
   436     func createDeleteAction(forCellAt indexPath: IndexPath) -> UITableViewRowAction? {
   437         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   438             tableView.beginUpdates()
   439             model?.delete(forIndexPath: indexPath) // mark for deletion/trash
   440             tableView.deleteRows(at: [indexPath], with: .none)
   441             tableView.endUpdates()
   442         }
   443 
   444         let title = NSLocalizedString("Delete", comment: "Message action (on swipe)")
   445         return createRowAction(image: UIImage(named: "swipe-trash"), action: action,
   446                                title: "\n\n\(title)")
   447     }
   448 
   449     func createMoreAction(forCellAt indexPath: IndexPath) -> UITableViewRowAction? {
   450         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   451             self.showMoreActionSheet(forRowAt: indexPath)
   452         }
   453 
   454         let title = NSLocalizedString("More", comment: "Message action (on swipe)")
   455         return createRowAction(image: UIImage(named: "swipe-more"),
   456                                action: action,
   457                                title: "\n\n\(title)")
   458     }
   459 }
   460 
   461 // MARK: - SegueHandlerType
   462 
   463 extension EmailListViewController: SegueHandlerType {
   464 
   465     enum SegueIdentifier: String {
   466         case segueAddNewAccount
   467         case segueShowEmail
   468         case segueCompose
   469         case segueReply
   470         case segueReplyAll
   471         case segueForward
   472         case segueFilter
   473         case segueFolderViews
   474         case noSegue
   475     }
   476 
   477     private func setup(composeViewController vc: ComposeTableViewController,
   478                        composeMode: ComposeTableViewController.ComposeMode = .normal,
   479                        originalMessage: Message? = nil) {
   480         vc.appConfig = appConfig
   481         vc.composeMode = composeMode
   482         vc.originalMessage = originalMessage
   483         vc.origin = model?.folderToShow?.account.user
   484     }
   485 
   486     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   487         switch segueIdentifier(for: segue) {
   488         case .segueReply:
   489             guard let nav = segue.destination as? UINavigationController,
   490                 let destination = nav.topViewController as? ComposeTableViewController,
   491                 let indexPath = lastSelectedIndexPath,
   492                 let message = model?.message(representedByRowAt: indexPath) else {
   493                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   494                     return
   495             }
   496             setup(composeViewController: destination, composeMode: .replyFrom,
   497                   originalMessage: message)
   498         case .segueReplyAll:
   499             guard let nav = segue.destination as? UINavigationController,
   500                 let destination = nav.topViewController as? ComposeTableViewController,
   501                 let indexPath = lastSelectedIndexPath,
   502                 let message = model?.message(representedByRowAt: indexPath) else {
   503                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   504                     return
   505             }
   506             setup(composeViewController: destination, composeMode: .replyAll,
   507                   originalMessage: message)
   508         case .segueShowEmail:
   509             guard let vc = segue.destination as? EmailViewController,
   510                 let indexPath = lastSelectedIndexPath,
   511                 let message = model?.message(representedByRowAt: indexPath) else { //BUFF: maybe remove message(representedByRowAt: and handle in dvc.
   512                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   513                     return
   514             }
   515             vc.appConfig = appConfig
   516             vc.message = message
   517             vc.folderShow = model?.folderToShow
   518             vc.messageId = indexPath.row //that looks wrong
   519         case .segueForward:
   520             guard let nav = segue.destination as? UINavigationController,
   521                 let destination = nav.topViewController as? ComposeTableViewController,
   522                 let indexPath = lastSelectedIndexPath,
   523                 let message = model?.message(representedByRowAt: indexPath) else {
   524                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   525                     return
   526             }
   527             setup(composeViewController: destination, composeMode: .forward,
   528                   originalMessage: message)
   529         case .segueFilter:
   530             guard let destiny = segue.destination as? FilterTableViewController  else {
   531                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   532                 return
   533             }
   534             destiny.appConfig = appConfig
   535             destiny.filterDelegate = model
   536             destiny.inFolder = false
   537             destiny.filterEnabled = model?.folderToShow?.filter?.clone()
   538             destiny.hidesBottomBarWhenPushed = true
   539         case .segueAddNewAccount:
   540             guard let vc = segue.destination as? LoginTableViewController  else {
   541                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   542                 return
   543             }
   544             vc.appConfig = appConfig
   545             vc.hidesBottomBarWhenPushed = true
   546             break
   547         case .segueFolderViews:
   548             guard let vC = segue.destination as? FolderTableViewController  else {
   549                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   550                 return
   551             }
   552             vC.appConfig = appConfig
   553             vC.hidesBottomBarWhenPushed = true
   554             break
   555         case .segueCompose:
   556             guard let nav = segue.destination as? UINavigationController,
   557                 let destination = nav.rootViewController as? ComposeTableViewController else {
   558                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   559                     return
   560             }
   561             setup(composeViewController: destination)
   562         default:
   563             Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
   564             break
   565         }
   566     }
   567 
   568     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   569         // nothing to do.
   570     }
   571 }