pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author buff <andreas@pep-project.org>
Tue, 05 Dec 2017 10:40:25 +0100
changeset 3522 9326deae631d
parent 3521 2628538df4d5
child 3546 663428e4eb70
permissions -rw-r--r--
IOS-851 shows receipient, not sender, in drafts and sent folders
     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 import SwipeCellKit
    12 
    13 class EmailListViewController: BaseTableViewController, SwipeTableViewCellDelegate {
    14     var folderToShow: Folder?
    15 
    16     func updateLastLookAt() {
    17         guard let saveFolder = folderToShow else {
    18             return
    19         }
    20         saveFolder.updateLastLookAt()
    21     }
    22     
    23     private var model: EmailListViewModel?
    24     
    25     private let queue: OperationQueue = {
    26         let createe = OperationQueue()
    27         createe.qualityOfService = .userInteractive
    28         createe.maxConcurrentOperationCount = 10
    29         return createe
    30     }()
    31     private var operations = [IndexPath:Operation]()
    32     public static let storyboardId = "EmailListViewController"
    33     fileprivate var lastSelectedIndexPath: IndexPath?
    34     
    35     let searchController = UISearchController(searchResultsController: nil)
    36 
    37     //swipe acctions types
    38     var buttonDisplayMode: ButtonDisplayMode = .titleAndImage
    39     var buttonStyle: ButtonStyle = .backgroundColor
    40     
    41     // MARK: - Outlets
    42     
    43     @IBOutlet weak var enableFilterButton: UIBarButtonItem!
    44     @IBOutlet weak var textFilterButton: UIBarButtonItem!
    45     @IBOutlet var showFoldersButton: UIBarButtonItem!
    46     
    47     // MARK: - Life Cycle
    48     
    49     override func viewDidLoad() {
    50         super.viewDidLoad()
    51         title = NSLocalizedString("Inbox", comment: "General name for (unified) inbox")
    52         UIHelper.emailListTableHeight(self.tableView)
    53         self.textFilterButton.isEnabled = false
    54         addSearchBar()
    55     }
    56     
    57     override func viewWillAppear(_ animated: Bool) {
    58         super.viewWillAppear(animated)
    59         self.navigationController?.setToolbarHidden(false, animated: true)
    60         if MiscUtil.isUnitTest() {
    61             return
    62         }
    63 
    64         setDefaultColors()
    65         setup()
    66         
    67         // Mark this folder as having been looked at by the user
    68         updateLastLookAt()
    69         setupFoldersBarButton()
    70     }
    71     
    72     // MARK: - NavigationBar
    73     
    74     private func hideFoldersNavigationBarButton() {
    75         self.showFoldersButton.isEnabled = false
    76         self.showFoldersButton.tintColor = UIColor.clear
    77     }
    78     
    79     private func showFoldersNavigationBarButton() {
    80         self.showFoldersButton.isEnabled = true
    81         self.showFoldersButton.tintColor = nil
    82     }
    83     
    84     private func resetModel() {
    85         if folderToShow != nil {
    86             model = EmailListViewModel(delegate: self,
    87                                        messageSyncService: appConfig.messageSyncService,
    88                                        folderToShow: folderToShow)
    89         }
    90     }
    91     
    92     private func setup() {
    93         if noAccountsExist() {
    94             // No account exists. Show account setup.
    95             performSegue(withIdentifier:.segueAddNewAccount, sender: self)
    96         } else if let vm = model {
    97             // We came back from e.g EmailView ...
    98             updateFilterText()
    99             // ... so we want to update "seen" status
   100             vm.reloadData()
   101         } else if folderToShow == nil {
   102             // We have not been created to show a specific folder, thus we show unified inbox
   103             folderToShow = UnifiedInbox()
   104             resetModel()
   105         } else if model == nil {
   106             // We still got no model, because:
   107             // - We are not coming back from a pushed view (for instance ComposeEmailView)
   108             // - We are not a UnifiedInbox
   109             // So we have been created to show a specific folder. Show it!
   110             resetModel()
   111         }
   112 
   113         self.title = realNameOfFolderToShow()
   114     }
   115 
   116     private func weCameBackFromAPushedView() -> Bool {
   117         return model != nil
   118     }
   119     
   120     private func noAccountsExist() -> Bool {
   121         return Account.all().isEmpty
   122     }
   123     
   124     private func setupFoldersBarButton() {
   125         if let size = navigationController?.viewControllers.count, size > 1 {
   126             hideFoldersNavigationBarButton()
   127         } else {
   128             showFoldersNavigationBarButton()
   129         }
   130     }
   131     
   132     private func addSearchBar() {
   133         searchController.searchResultsUpdater = self
   134         searchController.dimsBackgroundDuringPresentation = false
   135         searchController.delegate = self
   136         definesPresentationContext = true
   137         tableView.tableHeaderView = searchController.searchBar
   138         tableView.setContentOffset(CGPoint(x: 0.0, y: searchController.searchBar.frame.size.height), animated: false)
   139     }
   140     
   141     // MARK: - Other
   142     
   143     private func realNameOfFolderToShow() -> String? {
   144         return folderToShow?.realName
   145     }
   146 
   147     private func address(forRowAt indexPath: IndexPath) -> String {
   148         guard
   149             let saveModel = model,
   150             let row = saveModel.row(for: indexPath),
   151             let folder = folderToShow
   152             else {
   153                 Log.shared.errorAndCrash(component: #function, errorString: "Invalid state")
   154                 return ""
   155         }
   156         switch folder.folderType {
   157         case .all: fallthrough
   158         case .archive: fallthrough
   159         case .spam: fallthrough
   160         case .trash: fallthrough
   161         case .flagged: fallthrough
   162         case .inbox: fallthrough
   163         case .normal:
   164             return row.from
   165         case .drafts: fallthrough
   166         case .sent:
   167             return row.to
   168         }
   169     }
   170 
   171     private func shouldShowAccessoryDisclosureIndicator() -> Bool {
   172         guard let folder = folderToShow else {
   173             Log.shared.errorAndCrash(component: #function, errorString: "No folder")
   174             return true
   175         }
   176         if folder.folderType == .drafts {
   177             // Mails in drafts folder can only be opened in compose mode, which is shown modally.
   178             return false
   179         }
   180         return true
   181     }
   182     
   183     private func configure(cell: EmailListViewCell, for indexPath: IndexPath) {
   184         // Configure lightweight stuff on main thread ...
   185         guard let saveModel = model else {
   186             return
   187         }
   188         guard let row = saveModel.row(for: indexPath) else {
   189             Log.shared.errorAndCrash(component: #function, errorString: "We should have a row here")
   190             return
   191         }
   192         // Mails in drafts folder can only be opened in compose mode, which is shown modally.
   193         cell.accessoryDisclosureIndicator.isHidden = shouldShowAccessoryDisclosureIndicator()
   194         cell.addressLabel.text = address(forRowAt: indexPath)
   195         cell.subjectLabel.text = row.subject
   196         cell.summaryLabel.text = row.bodyPeek
   197         cell.isFlagged = row.isFlagged
   198         cell.isSeen = row.isSeen
   199         cell.hasAttachment = row.showAttchmentIcon
   200         cell.dateLabel.text = row.dateText
   201         // Set image from cache if any
   202         cell.setContactImage(image: row.senderContactImage)
   203 
   204         let op = BlockOperation() { [weak self] in
   205             MessageModel.performAndWait {
   206                 // ... and expensive computations in background
   207                 guard let strongSelf = self else {
   208                     // View is gone, nothing to do.
   209                     return
   210                 }
   211                 
   212                 var senderImage: UIImage?
   213                 if row.senderContactImage == nil {
   214                     // image for identity has not been cached yet
   215                     // Get (and cache) it here in the background ...
   216                     senderImage = strongSelf.model?.senderImage(forCellAt: indexPath)
   217                     
   218                     // ... and set it on the main queue
   219                     DispatchQueue.main.async {
   220                         if senderImage != nil && senderImage != cell.contactImageView.image {
   221                             cell.contactImageView.image  = senderImage
   222                         }
   223                     }
   224                 }
   225                 
   226                 let pEpRatingImage = strongSelf.model?.pEpRatingColorImage(forCellAt: indexPath)
   227                 
   228                 // In theory we want to set all data in *one* async call. But as pEpRatingColorImage takes
   229                 // very long, we are setting the sender image seperatelly.
   230                 DispatchQueue.main.async {
   231                     if pEpRatingImage != nil {
   232                         cell.setPepRatingImage(image: pEpRatingImage)
   233                     }
   234                 }
   235             }
   236         }
   237         queue(operation: op, for: indexPath)
   238     }
   239 
   240     private func showComposeView() {
   241         self.performSegue(withIdentifier: SegueIdentifier.segueEditDraft, sender: self)
   242     }
   243 
   244     private func showEmail(forCellAt indexPath: IndexPath) {
   245         performSegue(withIdentifier: SegueIdentifier.segueShowEmail, sender: self)
   246         guard let vm = model else {
   247             Log.shared.errorAndCrash(component: #function, errorString: "No model.")
   248             return
   249         }
   250         vm.markRead(forIndexPath: indexPath)
   251     }
   252     
   253     // MARK: - Actions
   254     
   255     @IBAction func filterButtonHasBeenPressed(_ sender: UIBarButtonItem) {
   256         guard let vm = model else {
   257             Log.shared.errorAndCrash(component: #function, errorString: "We should have a model here")
   258             return
   259         }
   260         vm.isFilterEnabled = !vm.isFilterEnabled
   261         updateFilterButtonView()
   262     }
   263     
   264     func updateFilterButtonView() {
   265         guard let vm = model else {
   266             Log.shared.errorAndCrash(component: #function, errorString: "We should have a model here")
   267             return
   268         }
   269         
   270         textFilterButton.isEnabled = vm.isFilterEnabled
   271         if textFilterButton.isEnabled {
   272             enableFilterButton.image = UIImage(named: "unread-icon-active")
   273             updateFilterText()
   274         } else {
   275             textFilterButton.title = ""
   276             enableFilterButton.image = UIImage(named: "unread-icon")
   277         }
   278     }
   279     
   280     func updateFilterText() {
   281         if let vm = model, let txt = vm.activeFilter?.title {
   282             textFilterButton.title = "Filter by: " + txt
   283         }
   284     }
   285     
   286     // MARK: - UITableViewDataSource
   287     
   288     override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   289         return model?.rowCount ?? 0
   290     }
   291     
   292     override func tableView(_ tableView: UITableView,
   293                             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   294         guard let cell = tableView.dequeueReusableCell(withIdentifier: EmailListViewCell.storyboardId,
   295                                                        for: indexPath) as? EmailListViewCell
   296             else {
   297                 Log.shared.errorAndCrash(component: #function, errorString: "Wrong cell!")
   298                 return UITableViewCell()
   299         }
   300         cell.delegate = self
   301         configure(cell: cell, for: indexPath)
   302         return cell
   303     }
   304     
   305     // MARK: - SwipeTableViewCellDelegate
   306 
   307     func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
   308         /// Create swipe actions, taking the currently displayed folder into account
   309         var swipeActions = [SwipeAction]()
   310         let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
   311             self.deleteAction(forCellAt: indexPath)
   312         }
   313         configure(action: deleteAction, with: .trash)
   314         swipeActions.append(deleteAction)
   315 
   316         let flagAction = SwipeAction(style: .default, title: "Flag") { action, indexPath in
   317             self.flagAction(forCellAt: indexPath)
   318         }
   319         flagAction.hidesWhenSelected = true
   320         configure(action: flagAction, with: .flag)
   321         swipeActions.append(flagAction)
   322 
   323         guard let folder = folderToShow else {
   324             Log.shared.errorAndCrash(component: #function, errorString: "No folder")
   325             return nil
   326         }
   327         if folder.folderType != .drafts {
   328             // Do not add "more" actions (reply...) to drafted mails.
   329             let moreAction = SwipeAction(style: .default, title: "More") { action, indexPath in
   330                 self.moreAction(forCellAt: indexPath)
   331             }
   332             moreAction.hidesWhenSelected = true
   333             configure(action: moreAction, with: .more)
   334             swipeActions.append(moreAction)
   335         }
   336         return (orientation == .right ?   swipeActions : nil)
   337     }
   338 
   339     func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeTableOptions {
   340         var options = SwipeTableOptions()
   341         options.expansionStyle = .destructive
   342         options.transitionStyle = .border
   343         options.buttonSpacing = 11
   344         return options
   345     }
   346 
   347     func configure(action: SwipeAction, with descriptor: ActionDescriptor) {
   348         action.title = descriptor.title(forDisplayMode: buttonDisplayMode)
   349         action.image = descriptor.image(forStyle: buttonStyle, displayMode: buttonDisplayMode)
   350 
   351         switch buttonStyle {
   352         case .backgroundColor:
   353             action.backgroundColor = descriptor.color
   354         case .circular:
   355             action.backgroundColor = .clear
   356             action.textColor = descriptor.color
   357             action.font = .systemFont(ofSize: 13)
   358             action.transitionDelegate = ScaleTransition.default
   359         }
   360     }
   361     
   362     override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
   363         cancelOperation(for: indexPath)
   364     }
   365     
   366     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   367         guard let folder = folderToShow else {
   368             Log.shared.errorAndCrash(component: #function, errorString: "No folder")
   369             return
   370         }
   371         lastSelectedIndexPath = indexPath
   372         tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
   373 
   374         if folder.folderType == .drafts {
   375             showComposeView()
   376         } else {
   377             showEmail(forCellAt: indexPath)
   378         }
   379     }
   380 
   381     // Implemented to get informed about the scrolling position.
   382     // If the user has scrolled down (almost) to the end, we need to get older emails to display.
   383     override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
   384                             forRowAt indexPath: IndexPath) {
   385         guard let vm = model else {
   386             Log.shared.errorAndCrash(component: #function, errorString: "No model.")
   387             return
   388         }
   389         vm.fetchOlderMessagesIfRequired(forIndexPath: indexPath)
   390     }
   391 
   392     // MARK: - Queue Handling
   393     
   394     private func queue(operation op:Operation, for indexPath: IndexPath) {
   395         operations[indexPath] = op
   396         queue.addOperation(op)
   397     }
   398     
   399     private func cancelOperation(for indexPath:IndexPath) {
   400         guard let op = operations.removeValue(forKey: indexPath) else {
   401             return
   402         }
   403         if !op.isCancelled  {
   404             op.cancel()
   405         }
   406     }
   407 
   408     // MARK: -
   409 
   410     override func didReceiveMemoryWarning() {
   411         model?.freeMemory()
   412     }
   413 }
   414 
   415 // MARK: - UISearchResultsUpdating, UISearchControllerDelegate
   416 
   417 extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
   418     public func updateSearchResults(for searchController: UISearchController) {
   419         guard let vm = model, let searchText = searchController.searchBar.text else {
   420             return
   421         }
   422         vm.setSearchFilter(forSearchText: searchText)
   423     }
   424     
   425     func didDismissSearchController(_ searchController: UISearchController) {
   426         guard let vm = model else {
   427             return
   428         }
   429         vm.removeSearchFilter()
   430     }
   431 }
   432 
   433 // MARK: - EmailListModelDelegate
   434 
   435 extension EmailListViewController: EmailListViewModelDelegate {
   436     func emailListViewModel(viewModel: EmailListViewModel, didInsertDataAt indexPath: IndexPath) {
   437         tableView.beginUpdates()
   438         tableView.insertRows(at: [indexPath], with: .automatic)
   439         tableView.endUpdates()
   440     }
   441     
   442     func emailListViewModel(viewModel: EmailListViewModel, didRemoveDataAt indexPath: IndexPath) {
   443         tableView.beginUpdates()
   444         tableView.deleteRows(at: [indexPath], with: .automatic)
   445         tableView.endUpdates()
   446     }
   447     
   448     func emailListViewModel(viewModel: EmailListViewModel, didUpdateDataAt indexPath: IndexPath) {
   449         tableView.beginUpdates()
   450         tableView.reloadRows(at: [indexPath], with: .none)
   451         tableView.endUpdates()
   452     }
   453     
   454     func updateView() {
   455         self.tableView.reloadData()
   456     }
   457 }
   458 
   459 // MARK: - ActionSheet & ActionSheet Actions
   460 
   461 extension EmailListViewController {
   462     func showMoreActionSheet(forRowAt indexPath: IndexPath) {
   463         lastSelectedIndexPath = indexPath
   464         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   465         alertControler.view.tintColor = .pEpGreen
   466         let cancelAction = createCancelAction()
   467         let replyAction = createReplyAction()
   468         let replyAllAction = createReplyAllAction()
   469         let forwardAction = createForwardAction()
   470         alertControler.addAction(cancelAction)
   471         alertControler.addAction(replyAction)
   472         alertControler.addAction(replyAllAction)
   473         alertControler.addAction(forwardAction)
   474         if let popoverPresentationController = alertControler.popoverPresentationController {
   475             popoverPresentationController.sourceView = tableView
   476         }
   477         present(alertControler, animated: true, completion: nil)
   478     }
   479     
   480     // MARK: Action Sheet Actions
   481     
   482     func createCancelAction() -> UIAlertAction {
   483         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in
   484             self.tableView.beginUpdates()
   485             self.tableView.setEditing(false, animated: true)
   486             self.tableView.endUpdates()
   487         }
   488     }
   489     
   490     func createReplyAction() ->  UIAlertAction {
   491         return UIAlertAction(title: "Reply", style: .default) { (action) in
   492             self.performSegue(withIdentifier: .segueReply, sender: self)
   493         }
   494     }
   495     
   496     func createReplyAllAction() ->  UIAlertAction {
   497         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   498             self.performSegue(withIdentifier: .segueReplyAll, sender: self)
   499         }
   500     }
   501     
   502     func createForwardAction() -> UIAlertAction {
   503         return UIAlertAction(title: "Forward", style: .default) { (action) in
   504             self.performSegue(withIdentifier: .segueForward, sender: self)
   505         }
   506     }
   507 }
   508 
   509 // MARK: - TableViewCell Actions
   510 
   511 extension EmailListViewController {
   512     private func createRowAction(image: UIImage?,
   513                                  action: @escaping (UITableViewRowAction, IndexPath) -> Void
   514         ) -> UITableViewRowAction {
   515         let rowAction = UITableViewRowAction(style: .normal, title: nil, handler: action)
   516         if let theImage = image {
   517             let iconColor = UIColor(patternImage: theImage)
   518             rowAction.backgroundColor = iconColor
   519         }
   520         return rowAction
   521     }
   522     
   523     func flagAction(forCellAt indexPath: IndexPath) {
   524         guard let row = model?.row(for: indexPath) else {
   525             Log.shared.errorAndCrash(component: #function, errorString: "No data for indexPath!")
   526             return
   527         }
   528         if row.isFlagged {
   529             model?.unsetFlagged(forIndexPath: indexPath)
   530         } else {
   531             model?.setFlagged(forIndexPath: indexPath)
   532         }
   533         tableView.beginUpdates()
   534         tableView.setEditing(false, animated: true)
   535         tableView.reloadRows(at: [indexPath], with: .none)
   536         tableView.endUpdates()
   537     }
   538     
   539     func deleteAction(forCellAt indexPath: IndexPath) {
   540         model?.delete(forIndexPath: indexPath) // mark for deletion/trash
   541     }
   542     
   543     func moreAction(forCellAt indexPath: IndexPath) {
   544         self.showMoreActionSheet(forRowAt: indexPath)
   545     }
   546 }
   547 
   548 // MARK: - SegueHandlerType
   549 
   550 extension EmailListViewController: SegueHandlerType {
   551     
   552     enum SegueIdentifier: String {
   553         case segueAddNewAccount
   554         case segueShowEmail
   555         case segueCompose
   556         case segueReply
   557         case segueReplyAll
   558         case segueForward
   559         case segueEditDraft
   560         case segueFilter
   561         case segueFolderViews
   562         case noSegue
   563     }
   564     
   565     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   566         let segueId = segueIdentifier(for: segue)
   567         switch segueId {
   568         case .segueReply,
   569              .segueReplyAll,
   570              .segueForward,
   571              .segueCompose,
   572              .segueEditDraft:
   573             setupComposeViewController(for: segue)
   574         case .segueShowEmail:
   575             guard let vc = segue.destination as? EmailViewController,
   576                 let indexPath = lastSelectedIndexPath,
   577                 let message = model?.message(representedByRowAt: indexPath) else {
   578                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   579                     return
   580             }
   581             vc.appConfig = appConfig
   582             vc.message = message
   583             vc.folderShow = folderToShow
   584             vc.messageId = indexPath.row //that looks wrong
   585         case .segueFilter:
   586             guard let destiny = segue.destination as? FilterTableViewController  else {
   587                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   588                 return
   589             }
   590             destiny.appConfig = appConfig
   591             destiny.filterDelegate = model
   592             destiny.inFolder = false
   593             destiny.filterEnabled = folderToShow?.filter
   594             destiny.hidesBottomBarWhenPushed = true
   595         case .segueAddNewAccount:
   596             guard let vc = segue.destination as? LoginTableViewController  else {
   597                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   598                 return
   599             }
   600             vc.appConfig = appConfig
   601             vc.hidesBottomBarWhenPushed = true
   602             break
   603         case .segueFolderViews:
   604             guard let vC = segue.destination as? FolderTableViewController  else {
   605                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   606                 return
   607             }
   608             vC.appConfig = appConfig
   609             vC.hidesBottomBarWhenPushed = true
   610             break
   611         default:
   612             Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
   613             break
   614         }
   615     }
   616     
   617     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   618         // nothing to do.
   619     }
   620 
   621     private func setupComposeViewController(for segue: UIStoryboardSegue) {
   622         let segueId = segueIdentifier(for: segue)
   623         guard
   624             let nav = segue.destination as? UINavigationController,
   625             let composeVc = nav.topViewController as? ComposeTableViewController,
   626             let composeMode = composeMode(for: segueId) else {
   627                 Log.shared.errorAndCrash(component: #function,
   628                                          errorString: "composeViewController setup issue")
   629                 return
   630         }
   631         composeVc.appConfig = appConfig
   632         composeVc.composeMode = composeMode
   633         composeVc.origin = folderToShow?.account.user
   634         if composeMode != .normal {
   635             // This is not a simple compose (but reply, forward or such),
   636             // thus we have to pass the original meaasge.
   637             guard
   638                 let indexPath = lastSelectedIndexPath,
   639                 let message = model?.message(representedByRowAt: indexPath) else {
   640                     Log.shared.errorAndCrash(component: #function,
   641                                              errorString: "No original message")
   642                     return
   643             }
   644             composeVc.originalMessage = message
   645         }
   646     }
   647 
   648     private func composeMode(for segueId: SegueIdentifier) -> ComposeTableViewController.ComposeMode? {
   649         switch segueId {
   650         case .segueReply:
   651             return .replyFrom
   652         case .segueReplyAll:
   653             return .replyAll
   654         case .segueForward:
   655             return .forward
   656         case .segueCompose:
   657             return .normal
   658         case .segueEditDraft:
   659             return .draft
   660         default:
   661             return nil
   662         }
   663     }
   664 }
   665 
   666 //enums to simplify configurations
   667 
   668 enum ActionDescriptor {
   669     case read, more, flag, trash
   670 
   671     func title(forDisplayMode displayMode: ButtonDisplayMode) -> String? {
   672         guard displayMode != .imageOnly else { return nil }
   673 
   674         switch self {
   675         case .read: return NSLocalizedString("Read", comment: "read button")
   676         case .more: return NSLocalizedString("More", comment: "more button")
   677         case .flag: return NSLocalizedString("Flag", comment: "read button")
   678         case .trash: return NSLocalizedString("Trash", comment: "Trash button")
   679         }
   680     }
   681 
   682     func image(forStyle style: ButtonStyle, displayMode: ButtonDisplayMode) -> UIImage? {
   683         guard displayMode != .titleOnly else { return nil }
   684 
   685         let name: String
   686         switch self {
   687         case .read: name = "read"
   688         case .more: name = "more"
   689         case .flag: name = "flag"
   690         case .trash: name = "trash"
   691         }
   692 
   693         return UIImage(named: "swipe-" + name)
   694     }
   695 
   696     var color: UIColor {
   697         switch self {
   698         case .read: return #colorLiteral(red: 0.2980392157, green: 0.8509803922, blue: 0.3921568627, alpha: 1)
   699         case .more: return #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
   700         case .flag: return #colorLiteral(red: 1, green: 0.5803921569, blue: 0, alpha: 1)
   701         case .trash: return #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
   702         }
   703     }
   704 }
   705 
   706 enum ButtonDisplayMode {
   707     case titleAndImage, titleOnly, imageOnly
   708 }
   709 
   710 enum ButtonStyle {
   711     case backgroundColor, circular
   712 }