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