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