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