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