pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author Dirk Zimmermann <dirk@pep-project.org>
Thu, 24 Aug 2017 13:14:17 +0200
changeset 2854 9c21ca9432a6
parent 2850 3c5024ebf882
child 2956 21c8a6d8a5c0
permissions -rw-r--r--
IOS-670 cosmetics
     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 Foundation
    10 import UIKit
    11 import CoreData
    12 import MessageModel
    13 
    14 struct EmailListConfig {
    15     var appConfig: AppConfig
    16 
    17     /** The folder to display, if it exists */
    18     var folder: Folder?
    19 
    20     let imageProvider = IdentityImageProvider()
    21 }
    22 
    23 class EmailListViewController: TableViewControllerBase {
    24     public static let storyboardId = "EmailListViewController"
    25     struct UIState {
    26         var isSynching: Bool = false
    27     }
    28 
    29     var config: EmailListConfig?
    30     var viewModel: EmailListViewModel?
    31     var state = UIState()
    32     let searchController = UISearchController(searchResultsController: nil)
    33 
    34     /**
    35      After trustwords have been invoked, this will be the partner identity that
    36      was either confirmed or mistrusted.
    37      */
    38     var partnerIdentity: Identity?
    39 
    40     @IBOutlet weak var enableFilterButton: UIBarButtonItem!
    41     @IBOutlet weak var textFilterButton: UIBarButtonItem!
    42 
    43     @IBOutlet var showFoldersButton: UIBarButtonItem!
    44 
    45     override func viewDidLoad() {
    46         super.viewDidLoad()
    47         title = NSLocalizedString("Inbox", comment: "General name for (unified) inbox")
    48         UIHelper.emailListTableHeight(self.tableView)
    49         addSearchBar()
    50     }
    51 
    52     override func viewWillAppear(_ animated: Bool) {
    53         super.viewWillAppear(animated)
    54         self.navigationController?.setToolbarHidden(false, animated: true)
    55         if MiscUtil.isUnitTest() {
    56             return
    57         }
    58 
    59         if let vm = viewModel {
    60             self.textFilterButton.isEnabled = vm.filterEnabled
    61         } else {
    62             self.textFilterButton.isEnabled = false
    63         }
    64 
    65         setDefaultColors()
    66         setupConfig()
    67         updateModel()
    68 
    69         // Mark this folder as having been looked at by the user
    70         if let folder = config?.folder {
    71             updateLastLookAt(on: folder)
    72         }
    73         if viewModel == nil {
    74             viewModel = EmailListViewModel(config: config, delegate: self)
    75         }
    76         MessageModelConfig.messageFolderDelegate = self
    77 
    78         if let size = navigationController?.viewControllers.count, size > 1 {
    79             self.showFoldersButton.isEnabled = false
    80         } else {
    81             self.showFoldersButton.isEnabled = true
    82         }
    83 
    84     }
    85 
    86     private func updateLastLookAt(on folder: Folder) {
    87         if folder.isUnified {
    88             folder.updateLastLookAt()
    89         } else {
    90             folder.updateLastLookAtAndSave()
    91         }
    92     }
    93 
    94     override func viewWillDisappear(_ animated: Bool) {
    95         super.viewWillDisappear(animated)
    96         MessageModelConfig.messageFolderDelegate = nil
    97     }
    98 
    99     func setupConfig() {
   100         guard let saveAppConfig = appConfig else {
   101             Log.shared.errorAndCrash(component: #function, errorString: "No AppConfig!")
   102             return
   103         }
   104         if config == nil {
   105             config = EmailListConfig(appConfig: saveAppConfig,
   106                                      folder: Folder.unifiedInbox())
   107         }
   108 
   109         if Account.all().isEmpty {
   110             performSegue(withIdentifier:.segueAddNewAccount, sender: self)
   111         }
   112 
   113         guard let folder = config?.folder else {
   114             return
   115         }
   116         self.title = realName(of: folder)
   117     }
   118 
   119     func addSearchBar() {
   120         searchController.searchResultsUpdater = self
   121         searchController.dimsBackgroundDuringPresentation = false
   122         searchController.delegate = self
   123         definesPresentationContext = true
   124         tableView.tableHeaderView = searchController.searchBar
   125         tableView.setContentOffset(CGPoint(x: 0.0, y: 40.0), animated: false)
   126     }
   127 
   128     func updateModel() {
   129         tableView.reloadData()
   130     }
   131 
   132     @IBAction func showUnreadButtonTapped(_ sender: UIBarButtonItem) {
   133         if let vm = viewModel {
   134             if vm.filterEnabled {
   135                 vm.filterEnabled = false
   136                 textFilterButton.title = ""
   137                 enableFilterButton.image = UIImage(named: "unread-icon")
   138                 if config != nil {
   139                     vm.resetFilters()
   140                 }
   141             } else {
   142                 vm.filterEnabled = true
   143                 textFilterButton.title = "Filter by: unread"
   144                 enableFilterButton.image = UIImage(named: "unread-icon-active")
   145                 if config != nil {
   146                     vm.updateFilter(filter: Filter.unread())
   147                 }
   148             }
   149             self.textFilterButton.isEnabled = vm.filterEnabled
   150         }
   151     }
   152 
   153     // MARK: - Private
   154 
   155     private func realName(of folder: Folder) -> String? {
   156         if folder.isUnified {
   157             return folder.name
   158         } else {
   159             return folder.realName
   160         }
   161     }
   162 
   163     // MARK: - UI State
   164 
   165     func updateUI() {
   166         UIApplication.shared.isNetworkActivityIndicatorVisible = state.isSynching
   167         if !state.isSynching {
   168             refreshControl?.endRefreshing()
   169         }
   170     }
   171 
   172     // MARK: - UITableViewDataSource
   173 
   174     override func numberOfSections(in tableView: UITableView) -> Int {
   175         if let _ = viewModel?.folderToShow {
   176             return 1
   177         }
   178         return 0
   179     }
   180 
   181     override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   182         if let vm = viewModel {
   183             return vm.count
   184         }
   185         return 0
   186     }
   187 
   188     override func tableView(_ tableView: UITableView,
   189                             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   190         let cell = tableView.dequeueReusableCell(
   191             withIdentifier: "EmailListViewCell", for: indexPath) as! EmailListViewCell
   192         //mantener el configure cell para tal de no generar un vm para celdas
   193         let _ = cell.configureCell(config: config, indexPath: indexPath, session: session)
   194         viewModel?.associate(cell: cell, position: indexPath.row)
   195         return cell
   196     }
   197 
   198     // MARK: - UITableViewDelegate
   199 
   200     override func tableView(_ tableView: UITableView, editActionsForRowAt
   201         indexPath: IndexPath)-> [UITableViewRowAction]? {
   202 
   203         let cell = tableView.cellForRow(at: indexPath) as! EmailListViewCell
   204         if let email = cell.messageAt(indexPath: indexPath, config: config) {
   205             let flagAction = createFlagAction(message: email, cell: cell)
   206             let deleteAction = createDeleteAction(message: email, cell: cell)
   207             let moreAction = createMoreAction(message: email, cell: cell)
   208             return [deleteAction, flagAction, moreAction]
   209         }
   210         return nil
   211     }
   212 
   213     // MARK: - Misc
   214 
   215     func createRowAction(cell: EmailListViewCell,
   216                          image: UIImage?, action: @escaping (UITableViewRowAction, IndexPath) -> Void,
   217                          title: String) -> UITableViewRowAction {
   218         let rowAction = UITableViewRowAction(
   219             style: .normal, title: title, handler: action)
   220 
   221         if let theImage = image {
   222             let iconColor = UIColor(patternImage: theImage)
   223             rowAction.backgroundColor = iconColor
   224         }
   225 
   226         return rowAction
   227     }
   228 
   229     func createFlagAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   230         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   231             if message.imapFlags == nil {
   232                 Log.warn(component: #function, content: "message.imapFlags == nil")
   233             }
   234             if cell.isFlagged(message: message) {
   235                 message.imapFlags?.flagged = false
   236             } else {
   237                 message.imapFlags?.flagged = true
   238             }
   239             message.save()
   240             self.tableView.reloadRows(at: [indexPath], with: .none)
   241         }
   242 
   243         let flagString = NSLocalizedString("Flag", comment: "Message action (on swipe)")
   244         var title = "\n\n\(flagString)"
   245         let unflagString = NSLocalizedString("Unflag", comment: "Message action (on swipe)")
   246         if message.imapFlags?.flagged ?? true {
   247             title = "\n\n\(unflagString)"
   248         }
   249 
   250         return createRowAction(
   251             cell: cell, image: UIImage(named: "swipe-flag"), action: action, title: title)
   252     }
   253 
   254     func createDeleteAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   255         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   256             guard let message = cell.messageAt(indexPath: indexPath, config: self.config) else {
   257                 return
   258             }
   259 
   260             message.delete() // mark for deletion/trash
   261             self.tableView.reloadData()
   262         }
   263 
   264         let title = NSLocalizedString("Delete", comment: "Message action (on swipe)")
   265         return createRowAction(
   266             cell: cell, image: UIImage(named: "swipe-trash"), action: action,
   267             title: "\n\n\(title)")
   268     }
   269 
   270     func createMarkAsReadAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   271         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   272             if cell.haveSeen(message: message) {
   273                 message.imapFlags?.seen = false
   274             } else {
   275                 message.imapFlags?.seen = true
   276             }
   277             self.tableView.reloadRows(at: [indexPath], with: .none)
   278         }
   279 
   280         var title = NSLocalizedString(
   281             "Unread", comment: "Message action (on swipe)")
   282         if !cell.haveSeen(message: message) {
   283             title = NSLocalizedString(
   284                 "Read", comment: "Message action (on swipe)")
   285         }
   286 
   287         let isReadAction = createRowAction(cell: cell, image: nil, action: action,
   288                                            title: title)
   289         isReadAction.backgroundColor = UIColor.blue
   290 
   291         return isReadAction
   292     }
   293 
   294     func createMoreAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   295         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   296             self.showMoreActionSheet(cell: cell)
   297         }
   298 
   299         let title = NSLocalizedString("More", comment: "Message action (on swipe)")
   300         return createRowAction(
   301             cell: cell, image: UIImage(named: "swipe-more"), action: action,
   302             title: "\n\n\(title)")
   303     }
   304 
   305     // MARK: - Action Sheet
   306 
   307     func showMoreActionSheet(cell: EmailListViewCell) {
   308         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   309         alertControler.view.tintColor = .pEpGreen
   310         let cancelAction = createCancelAction()
   311         let replyAction = createReplyAction(cell: cell)
   312         let replyAllAction = createReplyAllAction(cell: cell)
   313         let forwardAction = createForwardAction(cell: cell)
   314         alertControler.addAction(cancelAction)
   315         alertControler.addAction(replyAction)
   316         alertControler.addAction(replyAllAction)
   317         alertControler.addAction(forwardAction)
   318         if let popoverPresentationController = alertControler.popoverPresentationController {
   319             popoverPresentationController.sourceView = cell
   320         }
   321         present(alertControler, animated: true, completion: nil)
   322     }
   323 
   324     // MARK: - Action Sheet Actions
   325 
   326     func createCancelAction() -> UIAlertAction {
   327         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in}
   328     }
   329 
   330     func createReplyAction(cell: EmailListViewCell) ->  UIAlertAction {
   331         return UIAlertAction(title: "Reply", style: .default) { (action) in
   332             // self.performSegue(withIdentifier: self.segueCompose, sender: cell)
   333             self.performSegue(withIdentifier: .segueReply, sender: cell)
   334         }
   335     }
   336 
   337     func createReplyAllAction(cell: EmailListViewCell) ->  UIAlertAction {
   338         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   339             self.performSegue(withIdentifier: .segueReplyAll, sender: cell)
   340         }
   341     }
   342 
   343     func createForwardAction(cell: EmailListViewCell) -> UIAlertAction {
   344         return UIAlertAction(title: "Forward", style: .default) { (action) in
   345             self.performSegue(withIdentifier: .segueForward, sender: cell)
   346         }
   347     }
   348 
   349 }
   350 
   351 extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
   352     public func updateSearchResults(for searchController: UISearchController) {
   353         if let vm = viewModel {
   354             vm.filterContentForSearchText(searchText: searchController.searchBar.text!, clear: false)
   355         }
   356     }
   357 
   358     func didDismissSearchController(_ searchController: UISearchController) {
   359         if let vm = viewModel {
   360             vm.filterContentForSearchText(clear: true)
   361         }
   362     }
   363 }
   364 
   365 // MARK: - Navigation
   366 
   367 extension EmailListViewController: SegueHandlerType {
   368 
   369     // MARK: - SegueHandlerType
   370 
   371     enum SegueIdentifier: String {
   372         case segueAddNewAccount
   373         case segueEditAccounts
   374         case segueShowEmail
   375         case segueCompose
   376         case segueReply
   377         case segueReplyAll
   378         case segueForward
   379         case segueFilter
   380         case segueFolderViews
   381         case noSegue
   382     }
   383 
   384     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   385         switch segueIdentifier(for: segue) {
   386         case .segueReply:
   387             guard let nav = segue.destination as? UINavigationController,
   388                 let destination = nav.topViewController as? ComposeTableViewController,
   389                 let cell = sender as? EmailListViewCell,
   390                 let indexPath = self.tableView.indexPath(for: cell),
   391                 let email = cell.messageAt(indexPath: indexPath, config: config) else {
   392                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   393                     return
   394             }
   395             destination.appConfig = appConfig
   396             destination.composeMode = .replyFrom
   397             destination.originalMessage = email
   398         case .segueReplyAll:
   399             guard let nav = segue.destination as? UINavigationController,
   400                 let destination = nav.topViewController as? ComposeTableViewController,
   401                 let cell = sender as? EmailListViewCell,
   402                 let indexPath = self.tableView.indexPath(for: cell),
   403                 let email = cell.messageAt(indexPath: indexPath, config: config)  else {
   404                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   405                     return
   406             }
   407             destination.appConfig = appConfig
   408             destination.composeMode = .replyAll
   409             destination.originalMessage = email
   410         case .segueShowEmail:
   411             guard let vc = segue.destination as? EmailViewController,
   412                 let cell = sender as? EmailListViewCell,
   413                 let indexPath = self.tableView.indexPath(for: cell),
   414                 let email = cell.messageAt(indexPath: indexPath, config: config) else {
   415                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   416                     return
   417             }
   418             vc.appConfig = appConfig
   419             vc.message = email
   420             vc.folderShow = viewModel?.folderToShow
   421             vc.messageId = indexPath.row
   422         case .segueForward:
   423             guard let nav = segue.destination as? UINavigationController,
   424                 let destination = nav.topViewController as? ComposeTableViewController,
   425                 let cell = sender as? EmailListViewCell,
   426                 let indexPath = self.tableView.indexPath(for: cell),
   427                 let email = cell.messageAt(indexPath: indexPath, config: config) else {
   428                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   429                     return
   430             }
   431             destination.composeMode = .forward
   432             destination.appConfig = appConfig
   433             destination.originalMessage = email
   434         case .segueFilter:
   435             guard let destiny = segue.destination as? FilterTableViewController  else {
   436                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   437                 return
   438             }
   439             destiny.appConfig = appConfig
   440             destiny.filterDelegate = viewModel
   441             destiny.inFolder = false
   442             destiny.filterEnabled = viewModel?.folderToShow?.filter
   443             destiny.hidesBottomBarWhenPushed = true
   444         case .segueAddNewAccount:
   445             guard let vc = segue.destination as? LoginTableViewController  else {
   446                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   447                 return
   448             }
   449             vc.appConfig = appConfig
   450             vc.hidesBottomBarWhenPushed = true
   451             break
   452         case .segueFolderViews:
   453             guard let vC = segue.destination as? FolderTableViewController  else {
   454                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   455                 return
   456             }
   457             vC.appConfig = appConfig
   458             vC.hidesBottomBarWhenPushed = true
   459             break
   460         case .segueCompose:
   461             guard let nav = segue.destination as? UINavigationController,
   462                 let destination = nav.rootViewController as? ComposeTableViewController else {
   463                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   464                     return
   465             }
   466             destination.appConfig = appConfig
   467             destination.composeMode = .normal
   468         default:
   469             Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
   470             break
   471         }
   472     }
   473 
   474     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   475     }
   476 
   477     func didChangeInternal(messageFolder: MessageFolder) {
   478         if let folder = config?.folder,
   479             let message = messageFolder as? Message,
   480             folder.contains(message: message, deletedMessagesAreContained: true) {
   481             if message.isOriginal {
   482                 // new message has arrived
   483                 if let index = folder.indexOf(message: message) {
   484                     let ip = IndexPath(row: index, section: 0)
   485                     Log.info(
   486                         component: #function,
   487                         content: "insert message at \(index), \(folder.messageCount()) messages")
   488                     tableView.insertRows(at: [ip], with: .automatic)
   489                 } else {
   490                     tableView.reloadData()
   491                 }
   492             } else if message.isGhost {
   493                 if let vm = viewModel, let cell = vm.cellFor(message: message), let ip = tableView.indexPath(for: cell) {
   494                     Log.info(
   495                         component: #function,
   496                         content: "delete message at \(index), \(folder.messageCount()) messages")
   497                     tableView.deleteRows(at: [ip], with: .automatic)
   498                 } else {
   499                     tableView.reloadData()
   500                 }
   501             } else {
   502                 // other flags than delete must have been changed
   503                 if let vm = viewModel, let cell = vm.cellFor(message: message) {
   504                     cell.updateFlags(message: message)
   505                 } else {
   506                     tableView.reloadData()
   507                 }
   508             }
   509         }
   510     }
   511 
   512 }
   513 
   514 // MARK: - MessageFolderDelegate
   515 
   516 extension EmailListViewController: MessageFolderDelegate {
   517     func didChange(messageFolder: MessageFolder) {
   518         GCD.onMainWait {
   519             self.didChangeInternal(messageFolder: messageFolder)
   520         }
   521     }
   522 }
   523 
   524 extension EmailListViewController: TableViewUpdate {
   525     func updateView() {
   526         self.tableView.reloadData()
   527     }
   528 }