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