pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author buff <andreas@pep-project.org>
Mon, 04 Dec 2017 13:55:14 +0100
changeset 3514 9a4b16d7434d
parent 3513 f840199c40bc
child 3521 2628538df4d5
permissions -rw-r--r--
IOS-827 removes obsolete if clause
     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         /// Create swipe actions, taking the currently displayed folder into account
   271         var swipeActions = [SwipeAction]()
   272         let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
   273             self.deleteAction(forCellAt: indexPath)
   274         }
   275         configure(action: deleteAction, with: .trash)
   276         swipeActions.append(deleteAction)
   277 
   278         let flagAction = SwipeAction(style: .default, title: "Flag") { action, indexPath in
   279             self.flagAction(forCellAt: indexPath)
   280         }
   281         flagAction.hidesWhenSelected = true
   282         configure(action: flagAction, with: .flag)
   283         swipeActions.append(flagAction)
   284 
   285         guard let folder = folderToShow else {
   286             Log.shared.errorAndCrash(component: #function, errorString: "No folder")
   287             return nil
   288         }
   289         if folder.folderType != .drafts {
   290             // Do not add "more" actions (reply...) to drafted mails.
   291             let moreAction = SwipeAction(style: .default, title: "More") { action, indexPath in
   292                 self.moreAction(forCellAt: indexPath)
   293             }
   294             moreAction.hidesWhenSelected = true
   295             configure(action: moreAction, with: .more)
   296             swipeActions.append(moreAction)
   297         }
   298         return (orientation == .right ?   swipeActions : nil)
   299     }
   300 
   301     func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeTableOptions {
   302         var options = SwipeTableOptions()
   303         options.expansionStyle = .destructive
   304         options.transitionStyle = .border
   305         options.buttonSpacing = 11
   306         return options
   307     }
   308 
   309     func configure(action: SwipeAction, with descriptor: ActionDescriptor) {
   310         action.title = descriptor.title(forDisplayMode: buttonDisplayMode)
   311         action.image = descriptor.image(forStyle: buttonStyle, displayMode: buttonDisplayMode)
   312 
   313         switch buttonStyle {
   314         case .backgroundColor:
   315             action.backgroundColor = descriptor.color
   316         case .circular:
   317             action.backgroundColor = .clear
   318             action.textColor = descriptor.color
   319             action.font = .systemFont(ofSize: 13)
   320             action.transitionDelegate = ScaleTransition.default
   321         }
   322     }
   323     
   324     override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
   325         cancelOperation(for: indexPath)
   326     }
   327     
   328     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   329         guard let folder = folderToShow else {
   330             Log.shared.errorAndCrash(component: #function, errorString: "No folder")
   331             return
   332         }
   333         lastSelectedIndexPath = indexPath
   334         tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
   335 
   336         if folder.folderType == .drafts {
   337             showComposeView()
   338         } else {
   339             showEmail(forCellAt: indexPath)
   340         }
   341     }
   342 
   343     // Implemented to get informed about the scrolling position.
   344     // If the user has scrolled down (almost) to the end, we need to get older emails to display.
   345     override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
   346                             forRowAt indexPath: IndexPath) {
   347         guard let vm = model else {
   348             Log.shared.errorAndCrash(component: #function, errorString: "No model.")
   349             return
   350         }
   351         vm.fetchOlderMessagesIfRequired(forIndexPath: indexPath)
   352     }
   353 
   354     // MARK: - Queue Handling
   355     
   356     private func queue(operation op:Operation, for indexPath: IndexPath) {
   357         operations[indexPath] = op
   358         queue.addOperation(op)
   359     }
   360     
   361     private func cancelOperation(for indexPath:IndexPath) {
   362         guard let op = operations.removeValue(forKey: indexPath) else {
   363             return
   364         }
   365         if !op.isCancelled  {
   366             op.cancel()
   367         }
   368     }
   369 
   370     // MARK: -
   371 
   372     override func didReceiveMemoryWarning() {
   373         model?.freeMemory()
   374     }
   375 }
   376 
   377 // MARK: - UISearchResultsUpdating, UISearchControllerDelegate
   378 
   379 extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
   380     public func updateSearchResults(for searchController: UISearchController) {
   381         guard let vm = model, let searchText = searchController.searchBar.text else {
   382             return
   383         }
   384         vm.setSearchFilter(forSearchText: searchText)
   385     }
   386     
   387     func didDismissSearchController(_ searchController: UISearchController) {
   388         guard let vm = model else {
   389             return
   390         }
   391         vm.removeSearchFilter()
   392     }
   393 }
   394 
   395 // MARK: - EmailListModelDelegate
   396 
   397 extension EmailListViewController: EmailListViewModelDelegate {
   398     func emailListViewModel(viewModel: EmailListViewModel, didInsertDataAt indexPath: IndexPath) {
   399         tableView.beginUpdates()
   400         tableView.insertRows(at: [indexPath], with: .automatic)
   401         tableView.endUpdates()
   402     }
   403     
   404     func emailListViewModel(viewModel: EmailListViewModel, didRemoveDataAt indexPath: IndexPath) {
   405         tableView.beginUpdates()
   406         tableView.deleteRows(at: [indexPath], with: .automatic)
   407         tableView.endUpdates()
   408     }
   409     
   410     func emailListViewModel(viewModel: EmailListViewModel, didUpdateDataAt indexPath: IndexPath) {
   411         tableView.beginUpdates()
   412         tableView.reloadRows(at: [indexPath], with: .none)
   413         tableView.endUpdates()
   414     }
   415     
   416     func updateView() {
   417         self.tableView.reloadData()
   418     }
   419 }
   420 
   421 // MARK: - ActionSheet & ActionSheet Actions
   422 
   423 extension EmailListViewController {
   424     func showMoreActionSheet(forRowAt indexPath: IndexPath) {
   425         lastSelectedIndexPath = indexPath
   426         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   427         alertControler.view.tintColor = .pEpGreen
   428         let cancelAction = createCancelAction()
   429         let replyAction = createReplyAction()
   430         let replyAllAction = createReplyAllAction()
   431         let forwardAction = createForwardAction()
   432         alertControler.addAction(cancelAction)
   433         alertControler.addAction(replyAction)
   434         alertControler.addAction(replyAllAction)
   435         alertControler.addAction(forwardAction)
   436         if let popoverPresentationController = alertControler.popoverPresentationController {
   437             popoverPresentationController.sourceView = tableView
   438         }
   439         present(alertControler, animated: true, completion: nil)
   440     }
   441     
   442     // MARK: Action Sheet Actions
   443     
   444     func createCancelAction() -> UIAlertAction {
   445         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in
   446             self.tableView.beginUpdates()
   447             self.tableView.setEditing(false, animated: true)
   448             self.tableView.endUpdates()
   449         }
   450     }
   451     
   452     func createReplyAction() ->  UIAlertAction {
   453         return UIAlertAction(title: "Reply", style: .default) { (action) in
   454             self.performSegue(withIdentifier: .segueReply, sender: self)
   455         }
   456     }
   457     
   458     func createReplyAllAction() ->  UIAlertAction {
   459         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   460             self.performSegue(withIdentifier: .segueReplyAll, sender: self)
   461         }
   462     }
   463     
   464     func createForwardAction() -> UIAlertAction {
   465         return UIAlertAction(title: "Forward", style: .default) { (action) in
   466             self.performSegue(withIdentifier: .segueForward, sender: self)
   467         }
   468     }
   469 }
   470 
   471 // MARK: - TableViewCell Actions
   472 
   473 extension EmailListViewController {
   474     private func createRowAction(image: UIImage?,
   475                                  action: @escaping (UITableViewRowAction, IndexPath) -> Void
   476         ) -> UITableViewRowAction {
   477         let rowAction = UITableViewRowAction(style: .normal, title: nil, handler: action)
   478         if let theImage = image {
   479             let iconColor = UIColor(patternImage: theImage)
   480             rowAction.backgroundColor = iconColor
   481         }
   482         return rowAction
   483     }
   484     
   485     func flagAction(forCellAt indexPath: IndexPath) {
   486         guard let row = model?.row(for: indexPath) else {
   487             Log.shared.errorAndCrash(component: #function, errorString: "No data for indexPath!")
   488             return
   489         }
   490         if row.isFlagged {
   491             model?.unsetFlagged(forIndexPath: indexPath)
   492         } else {
   493             model?.setFlagged(forIndexPath: indexPath)
   494         }
   495         tableView.beginUpdates()
   496         tableView.setEditing(false, animated: true)
   497         tableView.reloadRows(at: [indexPath], with: .none)
   498         tableView.endUpdates()
   499     }
   500     
   501     func deleteAction(forCellAt indexPath: IndexPath) {
   502         model?.delete(forIndexPath: indexPath) // mark for deletion/trash
   503     }
   504     
   505     func moreAction(forCellAt indexPath: IndexPath) {
   506         self.showMoreActionSheet(forRowAt: indexPath)
   507     }
   508 }
   509 
   510 // MARK: - SegueHandlerType
   511 
   512 extension EmailListViewController: SegueHandlerType {
   513     
   514     enum SegueIdentifier: String {
   515         case segueAddNewAccount
   516         case segueShowEmail
   517         case segueCompose
   518         case segueReply
   519         case segueReplyAll
   520         case segueForward
   521         case segueEditDraft
   522         case segueFilter
   523         case segueFolderViews
   524         case noSegue
   525     }
   526     
   527     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   528         let segueId = segueIdentifier(for: segue)
   529         switch segueId {
   530         case .segueReply,
   531              .segueReplyAll,
   532              .segueForward,
   533              .segueCompose,
   534              .segueEditDraft:
   535             setupComposeViewController(for: segue)
   536         case .segueShowEmail:
   537             guard let vc = segue.destination as? EmailViewController,
   538                 let indexPath = lastSelectedIndexPath,
   539                 let message = model?.message(representedByRowAt: indexPath) else {
   540                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   541                     return
   542             }
   543             vc.appConfig = appConfig
   544             vc.message = message
   545             vc.folderShow = folderToShow
   546             vc.messageId = indexPath.row //that looks wrong
   547         case .segueFilter:
   548             guard let destiny = segue.destination as? FilterTableViewController  else {
   549                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   550                 return
   551             }
   552             destiny.appConfig = appConfig
   553             destiny.filterDelegate = model
   554             destiny.inFolder = false
   555             destiny.filterEnabled = folderToShow?.filter
   556             destiny.hidesBottomBarWhenPushed = true
   557         case .segueAddNewAccount:
   558             guard let vc = segue.destination as? LoginTableViewController  else {
   559                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   560                 return
   561             }
   562             vc.appConfig = appConfig
   563             vc.hidesBottomBarWhenPushed = true
   564             break
   565         case .segueFolderViews:
   566             guard let vC = segue.destination as? FolderTableViewController  else {
   567                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   568                 return
   569             }
   570             vC.appConfig = appConfig
   571             vC.hidesBottomBarWhenPushed = true
   572             break
   573         default:
   574             Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
   575             break
   576         }
   577     }
   578     
   579     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   580         // nothing to do.
   581     }
   582 
   583     private func setupComposeViewController(for segue: UIStoryboardSegue) {
   584         let segueId = segueIdentifier(for: segue)
   585         guard
   586             let nav = segue.destination as? UINavigationController,
   587             let composeVc = nav.topViewController as? ComposeTableViewController,
   588             let composeMode = composeMode(for: segueId) else {
   589                 Log.shared.errorAndCrash(component: #function,
   590                                          errorString: "composeViewController setup issue")
   591                 return
   592         }
   593         composeVc.appConfig = appConfig
   594         composeVc.composeMode = composeMode
   595         composeVc.origin = folderToShow?.account.user
   596         if composeMode != .normal {
   597             // This is not a simple compose (but reply, forward or such),
   598             // thus we have to pass the original meaasge.
   599             guard
   600                 let indexPath = lastSelectedIndexPath,
   601                 let message = model?.message(representedByRowAt: indexPath) else {
   602                     Log.shared.errorAndCrash(component: #function,
   603                                              errorString: "No original message")
   604                     return
   605             }
   606             composeVc.originalMessage = message
   607         }
   608     }
   609 
   610     private func composeMode(for segueId: SegueIdentifier) -> ComposeTableViewController.ComposeMode? {
   611         switch segueId {
   612         case .segueReply:
   613             return .replyFrom
   614         case .segueReplyAll:
   615             return .replyAll
   616         case .segueForward:
   617             return .forward
   618         case .segueCompose:
   619             return .normal
   620         case .segueEditDraft:
   621             return .draft
   622         default:
   623             return nil
   624         }
   625     }
   626 }
   627 
   628 //enums to simplify configurations
   629 
   630 enum ActionDescriptor {
   631     case read, more, flag, trash
   632 
   633     func title(forDisplayMode displayMode: ButtonDisplayMode) -> String? {
   634         guard displayMode != .imageOnly else { return nil }
   635 
   636         switch self {
   637         case .read: return NSLocalizedString("Read", comment: "read button")
   638         case .more: return NSLocalizedString("More", comment: "more button")
   639         case .flag: return NSLocalizedString("Flag", comment: "read button")
   640         case .trash: return NSLocalizedString("Trash", comment: "Trash button")
   641         }
   642     }
   643 
   644     func image(forStyle style: ButtonStyle, displayMode: ButtonDisplayMode) -> UIImage? {
   645         guard displayMode != .titleOnly else { return nil }
   646 
   647         let name: String
   648         switch self {
   649         case .read: name = "read"
   650         case .more: name = "more"
   651         case .flag: name = "flag"
   652         case .trash: name = "trash"
   653         }
   654 
   655         return UIImage(named: "swipe-" + name)
   656     }
   657 
   658     var color: UIColor {
   659         switch self {
   660         case .read: return #colorLiteral(red: 0.2980392157, green: 0.8509803922, blue: 0.3921568627, alpha: 1)
   661         case .more: return #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
   662         case .flag: return #colorLiteral(red: 1, green: 0.5803921569, blue: 0, alpha: 1)
   663         case .trash: return #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
   664         }
   665     }
   666 }
   667 
   668 enum ButtonDisplayMode {
   669     case titleAndImage, titleOnly, imageOnly
   670 }
   671 
   672 enum ButtonStyle {
   673     case backgroundColor, circular
   674 }