pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author Dirk Zimmermann <dirk@pep-project.org>
Thu, 01 Feb 2018 11:29:01 +0100
changeset 3866 b5901734830a
parent 3678 cbe8bbfdfb7f
child 3909 2c0076194a16
permissions -rw-r--r--
IOS-915 logging to pinpoint the problem
     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         Log.shared.info(component: #function, content: "\(model?.rowCount ?? 0)")
   457         tableView.beginUpdates()
   458         tableView.insertRows(at: [indexPath], with: .automatic)
   459         tableView.endUpdates()
   460     }
   461     
   462     func emailListViewModel(viewModel: EmailListViewModel, didRemoveDataAt indexPath: IndexPath) {
   463         Log.shared.info(component: #function, content: "\(model?.rowCount ?? 0)")
   464         tableView.beginUpdates()
   465         tableView.deleteRows(at: [indexPath], with: .automatic)
   466         tableView.endUpdates()
   467     }
   468     
   469     func emailListViewModel(viewModel: EmailListViewModel, didUpdateDataAt indexPath: IndexPath) {
   470         Log.shared.info(component: #function, content: "\(model?.rowCount ?? 0)")
   471         tableView.beginUpdates()
   472         tableView.reloadRows(at: [indexPath], with: .none)
   473         tableView.endUpdates()
   474     }
   475     
   476     func updateView() {
   477         loadingBlocked = false
   478         tableView.dataSource = self
   479         tableView.reloadData()
   480     }
   481 }
   482 
   483 // MARK: - ActionSheet & ActionSheet Actions
   484 
   485 extension EmailListViewController {
   486     func showMoreActionSheet(forRowAt indexPath: IndexPath) {
   487         lastSelectedIndexPath = indexPath
   488         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   489         alertControler.view.tintColor = .pEpGreen
   490         let cancelAction = createCancelAction()
   491         let replyAction = createReplyAction()
   492         let replyAllAction = createReplyAllAction()
   493         let forwardAction = createForwardAction()
   494         alertControler.addAction(cancelAction)
   495         alertControler.addAction(replyAction)
   496         alertControler.addAction(replyAllAction)
   497         alertControler.addAction(forwardAction)
   498         if let popoverPresentationController = alertControler.popoverPresentationController {
   499             popoverPresentationController.sourceView = tableView
   500         }
   501         present(alertControler, animated: true, completion: nil)
   502     }
   503     
   504     // MARK: Action Sheet Actions
   505     
   506     func createCancelAction() -> UIAlertAction {
   507         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in
   508             self.tableView.beginUpdates()
   509             self.tableView.setEditing(false, animated: true)
   510             self.tableView.endUpdates()
   511         }
   512     }
   513     
   514     func createReplyAction() ->  UIAlertAction {
   515         return UIAlertAction(title: "Reply", style: .default) { (action) in
   516             self.performSegue(withIdentifier: .segueReply, sender: self)
   517         }
   518     }
   519     
   520     func createReplyAllAction() ->  UIAlertAction {
   521         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   522             self.performSegue(withIdentifier: .segueReplyAll, sender: self)
   523         }
   524     }
   525     
   526     func createForwardAction() -> UIAlertAction {
   527         return UIAlertAction(title: "Forward", style: .default) { (action) in
   528             self.performSegue(withIdentifier: .segueForward, sender: self)
   529         }
   530     }
   531 }
   532 
   533 // MARK: - TableViewCell Actions
   534 
   535 extension EmailListViewController {
   536     private func createRowAction(image: UIImage?,
   537                                  action: @escaping (UITableViewRowAction, IndexPath) -> Void
   538         ) -> UITableViewRowAction {
   539         let rowAction = UITableViewRowAction(style: .normal, title: nil, handler: action)
   540         if let theImage = image {
   541             let iconColor = UIColor(patternImage: theImage)
   542             rowAction.backgroundColor = iconColor
   543         }
   544         return rowAction
   545     }
   546     
   547     func flagAction(forCellAt indexPath: IndexPath) {
   548         guard let row = model?.row(for: indexPath) else {
   549             Log.shared.errorAndCrash(component: #function, errorString: "No data for indexPath!")
   550             return
   551         }
   552         if row.isFlagged {
   553             model?.unsetFlagged(forIndexPath: indexPath)
   554         } else {
   555             model?.setFlagged(forIndexPath: indexPath)
   556         }
   557         tableView.beginUpdates()
   558         tableView.setEditing(false, animated: true)
   559         tableView.reloadRows(at: [indexPath], with: .none)
   560         tableView.endUpdates()
   561     }
   562     
   563     func deleteAction(forCellAt indexPath: IndexPath) {
   564         model?.delete(forIndexPath: indexPath) // mark for deletion/trash
   565     }
   566     
   567     func moreAction(forCellAt indexPath: IndexPath) {
   568         self.showMoreActionSheet(forRowAt: indexPath)
   569     }
   570 }
   571 
   572 // MARK: - SegueHandlerType
   573 
   574 extension EmailListViewController: SegueHandlerType {
   575     
   576     enum SegueIdentifier: String {
   577         case segueAddNewAccount
   578         case segueShowEmail
   579         case segueCompose
   580         case segueReply
   581         case segueReplyAll
   582         case segueForward
   583         case segueEditDraft
   584         case segueFilter
   585         case segueFolderViews
   586         case noSegue
   587     }
   588     
   589     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   590         let segueId = segueIdentifier(for: segue)
   591         switch segueId {
   592         case .segueReply,
   593              .segueReplyAll,
   594              .segueForward,
   595              .segueCompose,
   596              .segueEditDraft:
   597             setupComposeViewController(for: segue)
   598         case .segueShowEmail:
   599             guard let vc = segue.destination as? EmailViewController,
   600                 let indexPath = lastSelectedIndexPath,
   601                 let message = model?.message(representedByRowAt: indexPath) else {
   602                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   603                     return
   604             }
   605             vc.appConfig = appConfig
   606             vc.message = message
   607             vc.folderShow = folderToShow
   608             vc.messageId = indexPath.row //that looks wrong
   609         case .segueFilter:
   610             guard let destiny = segue.destination as? FilterTableViewController  else {
   611                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   612                 return
   613             }
   614             destiny.appConfig = appConfig
   615             destiny.filterDelegate = model
   616             destiny.inFolder = false
   617             destiny.filterEnabled = folderToShow?.filter
   618             destiny.hidesBottomBarWhenPushed = true
   619         case .segueAddNewAccount:
   620             guard let vc = segue.destination as? LoginTableViewController  else {
   621                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   622                 return
   623             }
   624             vc.appConfig = appConfig
   625             vc.hidesBottomBarWhenPushed = true
   626             break
   627         case .segueFolderViews:
   628             guard let vC = segue.destination as? FolderTableViewController  else {
   629                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   630                 return
   631             }
   632             vC.appConfig = appConfig
   633             vC.hidesBottomBarWhenPushed = true
   634             break
   635         default:
   636             Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
   637             break
   638         }
   639     }
   640     
   641     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   642         // nothing to do.
   643     }
   644 
   645     private func setupComposeViewController(for segue: UIStoryboardSegue) {
   646         let segueId = segueIdentifier(for: segue)
   647         guard
   648             let nav = segue.destination as? UINavigationController,
   649             let composeVc = nav.topViewController as? ComposeTableViewController,
   650             let composeMode = composeMode(for: segueId) else {
   651                 Log.shared.errorAndCrash(component: #function,
   652                                          errorString: "composeViewController setup issue")
   653                 return
   654         }
   655         composeVc.appConfig = appConfig
   656         composeVc.composeMode = composeMode
   657         composeVc.origin = folderToShow?.account.user
   658         if composeMode != .normal {
   659             // This is not a simple compose (but reply, forward or such),
   660             // thus we have to pass the original meaasge.
   661             guard
   662                 let indexPath = lastSelectedIndexPath,
   663                 let message = model?.message(representedByRowAt: indexPath) else {
   664                     Log.shared.errorAndCrash(component: #function,
   665                                              errorString: "No original message")
   666                     return
   667             }
   668             composeVc.originalMessage = message
   669         }
   670     }
   671 
   672     private func composeMode(for segueId: SegueIdentifier) -> ComposeTableViewController.ComposeMode? {
   673         switch segueId {
   674         case .segueReply:
   675             return .replyFrom
   676         case .segueReplyAll:
   677             return .replyAll
   678         case .segueForward:
   679             return .forward
   680         case .segueCompose:
   681             return .normal
   682         case .segueEditDraft:
   683             return .draft
   684         default:
   685             return nil
   686         }
   687     }
   688 }
   689 
   690 //enums to simplify configurations
   691 
   692 enum ActionDescriptor {
   693     case read, more, flag, trash
   694 
   695     func title(forDisplayMode displayMode: ButtonDisplayMode) -> String? {
   696         guard displayMode != .imageOnly else { return nil }
   697 
   698         switch self {
   699         case .read: return NSLocalizedString("Read", comment: "read button")
   700         case .more: return NSLocalizedString("More", comment: "more button")
   701         case .flag: return NSLocalizedString("Flag", comment: "read button")
   702         case .trash: return NSLocalizedString("Trash", comment: "Trash button")
   703         }
   704     }
   705 
   706     func image(forStyle style: ButtonStyle, displayMode: ButtonDisplayMode) -> UIImage? {
   707         guard displayMode != .titleOnly else { return nil }
   708 
   709         let name: String
   710         switch self {
   711         case .read: name = "read"
   712         case .more: name = "more"
   713         case .flag: name = "flag"
   714         case .trash: name = "trash"
   715         }
   716 
   717         return UIImage(named: "swipe-" + name)
   718     }
   719 
   720     var color: UIColor {
   721         switch self {
   722         case .read: return #colorLiteral(red: 0.2980392157, green: 0.8509803922, blue: 0.3921568627, alpha: 1)
   723         case .more: return #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
   724         case .flag: return #colorLiteral(red: 1, green: 0.5803921569, blue: 0, alpha: 1)
   725         case .trash: return #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
   726         }
   727     }
   728 }
   729 
   730 enum ButtonDisplayMode {
   731     case titleAndImage, titleOnly, imageOnly
   732 }
   733 
   734 enum ButtonStyle {
   735     case backgroundColor, circular
   736 }