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