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