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