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