pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author buff <andreas@pep-project.org>
Thu, 24 Aug 2017 12:08:08 +0200
branchIOSAD-23-crash-or-merge-issues-deleting-mails
changeset 2850 3c5024ebf882
parent 2845 1db827abc7c8
child 2854 9c21ca9432a6
permissions -rw-r--r--
IOSAD-23 do not ignore prepareForSeque issues. Fixes: segueCompose unhandled
     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     /** The folder to display, if it exists */
    17     var folder: Folder?
    18 
    19     let imageProvider = IdentityImageProvider()
    20 }
    21 
    22 class EmailListViewController: TableViewControllerBase {
    23     public static let storyboardId = "EmailListViewController"
    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         title = NSLocalizedString("Inbox", comment: "General name for (unified) inbox")
    49         UIHelper.emailListTableHeight(self.tableView)
    50         addSearchBar()
    51     }
    52 
    53     override func viewWillAppear(_ animated: Bool) {
    54         super.viewWillAppear(animated)
    55         self.navigationController?.setToolbarHidden(false, animated: true)
    56         if MiscUtil.isUnitTest() {
    57             return
    58         }
    59 
    60         if let vm = viewModel {
    61             self.textFilterButton.isEnabled = vm.filterEnabled
    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                 textFilterButton.title = "Filter by: unread"
   145                 enableFilterButton.image = UIImage(named: "unread-icon-active")
   146                 if config != nil {
   147                     vm.updateFilter(filter: Filter.unread())
   148                 }
   149             }
   150             self.textFilterButton.isEnabled = vm.filterEnabled
   151         }
   152     }
   153 
   154     // MARK: - Private
   155 
   156     private func realName(of folder: Folder) -> String? {
   157         if folder.isUnified {
   158             return folder.name
   159         } else {
   160             return folder.realName
   161         }
   162     }
   163 
   164     // MARK: - UI State
   165 
   166     func updateUI() {
   167         UIApplication.shared.isNetworkActivityIndicatorVisible = state.isSynching
   168         if !state.isSynching {
   169             refreshControl?.endRefreshing()
   170         }
   171     }
   172 
   173     // MARK: - UITableViewDataSource
   174 
   175     override func numberOfSections(in tableView: UITableView) -> Int {
   176         if let _ = viewModel?.folderToShow {
   177             return 1
   178         }
   179         return 0
   180     }
   181 
   182     override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   183         if let vm = viewModel {
   184             return vm.count
   185         }
   186         return 0
   187     }
   188 
   189     override func tableView(_ tableView: UITableView,
   190                             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   191         let cell = tableView.dequeueReusableCell(
   192             withIdentifier: "EmailListViewCell", for: indexPath) as! EmailListViewCell
   193         //mantener el configure cell para tal de no generar un vm para celdas
   194         let _ = cell.configureCell(config: config, indexPath: indexPath, session: session)
   195         viewModel?.associate(cell: cell, position: indexPath.row)
   196         return cell
   197     }
   198 
   199     // MARK: - UITableViewDelegate
   200 
   201     override func tableView(_ tableView: UITableView, editActionsForRowAt
   202         indexPath: IndexPath)-> [UITableViewRowAction]? {
   203 
   204         let cell = tableView.cellForRow(at: indexPath) as! EmailListViewCell
   205         if let email = cell.messageAt(indexPath: indexPath, config: config) {
   206             let flagAction = createFlagAction(message: email, cell: cell)
   207             let deleteAction = createDeleteAction(message: email, cell: cell)
   208             let moreAction = createMoreAction(message: email, cell: cell)
   209             return [deleteAction, flagAction, moreAction]
   210         }
   211         return nil
   212     }
   213 
   214     // MARK: - Misc
   215 
   216     func createRowAction(cell: EmailListViewCell,
   217                          image: UIImage?, action: @escaping (UITableViewRowAction, IndexPath) -> Void,
   218                          title: String) -> UITableViewRowAction {
   219         let rowAction = UITableViewRowAction(
   220             style: .normal, title: title, handler: action)
   221 
   222         if let theImage = image {
   223             let iconColor = UIColor(patternImage: theImage)
   224             rowAction.backgroundColor = iconColor
   225         }
   226 
   227         return rowAction
   228     }
   229 
   230     func createFlagAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   231         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   232             if message.imapFlags == nil {
   233                 Log.warn(component: #function, content: "message.imapFlags == nil")
   234             }
   235             if cell.isFlagged(message: message) {
   236                 message.imapFlags?.flagged = false
   237             } else {
   238                 message.imapFlags?.flagged = true
   239             }
   240             message.save()
   241             self.tableView.reloadRows(at: [indexPath], with: .none)
   242         }
   243 
   244         let flagString = NSLocalizedString("Flag", comment: "Message action (on swipe)")
   245         var title = "\n\n\(flagString)"
   246         let unflagString = NSLocalizedString("Unflag", comment: "Message action (on swipe)")
   247         if message.imapFlags?.flagged ?? true {
   248             title = "\n\n\(unflagString)"
   249         }
   250 
   251         return createRowAction(
   252             cell: cell, image: UIImage(named: "swipe-flag"), action: action, title: title)
   253     }
   254 
   255     func createDeleteAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   256         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   257             guard let message = cell.messageAt(indexPath: indexPath, config: self.config) else {
   258                 return
   259             }
   260 
   261             message.delete() // mark for deletion/trash
   262             self.tableView.reloadData()
   263         }
   264 
   265         let title = NSLocalizedString("Delete", comment: "Message action (on swipe)")
   266         return createRowAction(
   267             cell: cell, image: UIImage(named: "swipe-trash"), action: action,
   268             title: "\n\n\(title)")
   269     }
   270 
   271     func createMarkAsReadAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   272         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   273             if cell.haveSeen(message: message) {
   274                 message.imapFlags?.seen = false
   275             } else {
   276                 message.imapFlags?.seen = true
   277             }
   278             self.tableView.reloadRows(at: [indexPath], with: .none)
   279         }
   280 
   281         var title = NSLocalizedString(
   282             "Unread", comment: "Message action (on swipe)")
   283         if !cell.haveSeen(message: message) {
   284             title = NSLocalizedString(
   285                 "Read", comment: "Message action (on swipe)")
   286         }
   287 
   288         let isReadAction = createRowAction(cell: cell, image: nil, action: action,
   289                                            title: title)
   290         isReadAction.backgroundColor = UIColor.blue
   291 
   292         return isReadAction
   293     }
   294 
   295     func createMoreAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   296         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   297             self.showMoreActionSheet(cell: cell)
   298         }
   299 
   300         let title = NSLocalizedString("More", comment: "Message action (on swipe)")
   301         return createRowAction(
   302             cell: cell, image: UIImage(named: "swipe-more"), action: action,
   303             title: "\n\n\(title)")
   304     }
   305 
   306     // MARK: - Action Sheet
   307 
   308     func showMoreActionSheet(cell: EmailListViewCell) {
   309         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   310         alertControler.view.tintColor = .pEpGreen
   311         let cancelAction = createCancelAction()
   312         let replyAction = createReplyAction(cell: cell)
   313         let replyAllAction = createReplyAllAction(cell: cell)
   314         let forwardAction = createForwardAction(cell: cell)
   315         alertControler.addAction(cancelAction)
   316         alertControler.addAction(replyAction)
   317         alertControler.addAction(replyAllAction)
   318         alertControler.addAction(forwardAction)
   319         if let popoverPresentationController = alertControler.popoverPresentationController {
   320             popoverPresentationController.sourceView = cell
   321         }
   322         present(alertControler, animated: true, completion: nil)
   323     }
   324 
   325     // MARK: - Action Sheet Actions
   326 
   327     func createCancelAction() -> UIAlertAction {
   328         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in}
   329     }
   330 
   331     func createReplyAction(cell: EmailListViewCell) ->  UIAlertAction {
   332         return UIAlertAction(title: "Reply", style: .default) { (action) in
   333             // self.performSegue(withIdentifier: self.segueCompose, sender: cell)
   334             self.performSegue(withIdentifier: .segueReply, sender: cell)
   335         }
   336     }
   337 
   338     func createReplyAllAction(cell: EmailListViewCell) ->  UIAlertAction {
   339         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   340             self.performSegue(withIdentifier: .segueReplyAll, sender: cell)
   341         }
   342     }
   343 
   344     func createForwardAction(cell: EmailListViewCell) -> UIAlertAction {
   345         return UIAlertAction(title: "Forward", style: .default) { (action) in
   346             self.performSegue(withIdentifier: .segueForward, sender: cell)
   347         }
   348     }
   349 
   350 }
   351 
   352 extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
   353     public func updateSearchResults(for searchController: UISearchController) {
   354         if let vm = viewModel {
   355             vm.filterContentForSearchText(searchText: searchController.searchBar.text!, clear: false)
   356         }
   357     }
   358 
   359     func didDismissSearchController(_ searchController: UISearchController) {
   360         if let vm = viewModel {
   361             vm.filterContentForSearchText(clear: true)
   362         }
   363     }
   364 }
   365 
   366 // MARK: - Navigation
   367 
   368 extension EmailListViewController: SegueHandlerType {
   369 
   370     // MARK: - SegueHandlerType
   371 
   372     enum SegueIdentifier: String {
   373         case segueAddNewAccount
   374         case segueEditAccounts
   375         case segueShowEmail
   376         case segueCompose
   377         case segueReply
   378         case segueReplyAll
   379         case segueForward
   380         case segueFilter
   381         case segueFolderViews
   382         case noSegue
   383     }
   384 
   385     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   386         switch segueIdentifier(for: segue) {
   387         case .segueReply:
   388             guard let nav = segue.destination as? UINavigationController,
   389                 let destination = nav.topViewController as? ComposeTableViewController,
   390                 let cell = sender as? EmailListViewCell,
   391                 let indexPath = self.tableView.indexPath(for: cell),
   392                 let email = cell.messageAt(indexPath: indexPath, config: config) else {
   393                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   394                     return
   395             }
   396             destination.appConfig = appConfig
   397             destination.composeMode = .replyFrom
   398             destination.originalMessage = email
   399         case .segueReplyAll:
   400             guard let nav = segue.destination as? UINavigationController,
   401                 let destination = nav.topViewController as? ComposeTableViewController,
   402                 let cell = sender as? EmailListViewCell,
   403                 let indexPath = self.tableView.indexPath(for: cell),
   404                 let email = cell.messageAt(indexPath: indexPath, config: config)  else {
   405                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   406                     return
   407             }
   408             destination.appConfig = appConfig
   409             destination.composeMode = .replyAll
   410             destination.originalMessage = email
   411         case .segueShowEmail:
   412             guard let vc = segue.destination as? EmailViewController,
   413                 let cell = sender as? EmailListViewCell,
   414                 let indexPath = self.tableView.indexPath(for: cell),
   415                 let email = cell.messageAt(indexPath: indexPath, config: config) else {
   416                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   417                     return
   418             }
   419             vc.appConfig = appConfig
   420             vc.message = email
   421             vc.folderShow = viewModel?.folderToShow
   422             vc.messageId = indexPath.row
   423         case .segueForward:
   424             guard let nav = segue.destination as? UINavigationController,
   425                 let destination = nav.topViewController as? ComposeTableViewController,
   426                 let cell = sender as? EmailListViewCell,
   427                 let indexPath = self.tableView.indexPath(for: cell),
   428                 let email = cell.messageAt(indexPath: indexPath, config: config) else {
   429                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   430                     return
   431             }
   432             destination.composeMode = .forward
   433             destination.appConfig = appConfig
   434             destination.originalMessage = email
   435         case .segueFilter:
   436             guard let destiny = segue.destination as? FilterTableViewController  else {
   437                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   438                 return
   439             }
   440             destiny.appConfig = appConfig
   441             destiny.filterDelegate = viewModel
   442             destiny.inFolder = false
   443             destiny.filterEnabled = viewModel?.folderToShow?.filter
   444             destiny.hidesBottomBarWhenPushed = true
   445         case .segueAddNewAccount:
   446             guard let vc = segue.destination as? LoginTableViewController  else {
   447                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   448                 return
   449             }
   450             vc.appConfig = appConfig
   451             vc.hidesBottomBarWhenPushed = true
   452             break
   453         case .segueFolderViews:
   454             guard let vC = segue.destination as? FolderTableViewController  else {
   455                 Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   456                 return
   457             }
   458             vC.appConfig = appConfig
   459             vC.hidesBottomBarWhenPushed = true
   460             break
   461         case .segueCompose:
   462             guard let nav = segue.destination as? UINavigationController,
   463                 let destination = nav.rootViewController as? ComposeTableViewController else {
   464                     Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
   465                     return
   466             }
   467             destination.appConfig = appConfig
   468             destination.composeMode = .normal
   469         default:
   470             Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
   471             break
   472         }
   473     }
   474 
   475     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   476     }
   477 
   478     func didChangeInternal(messageFolder: MessageFolder) {
   479         if let folder = config?.folder,
   480             let message = messageFolder as? Message,
   481             folder.contains(message: message, deletedMessagesAreContained: true) {
   482             if message.isOriginal {
   483                 // new message has arrived
   484                 if let index = folder.indexOf(message: message) {
   485                     let ip = IndexPath(row: index, section: 0)
   486                     Log.info(
   487                         component: #function,
   488                         content: "insert message at \(index), \(folder.messageCount()) messages")
   489                     tableView.insertRows(at: [ip], with: .automatic)
   490                 } else {
   491                     tableView.reloadData()
   492                 }
   493             } else if message.isGhost {
   494                 if let vm = viewModel, let cell = vm.cellFor(message: message), let ip = tableView.indexPath(for: cell) {
   495                     Log.info(
   496                         component: #function,
   497                         content: "delete message at \(index), \(folder.messageCount()) messages")
   498                     tableView.deleteRows(at: [ip], with: .automatic)
   499                 } else {
   500                     tableView.reloadData()
   501                 }
   502             } else {
   503                 // other flags than delete must have been changed
   504                 if let vm = viewModel, let cell = vm.cellFor(message: message) {
   505                     cell.updateFlags(message: message)
   506                 } else {
   507                     tableView.reloadData()
   508                 }
   509             }
   510         }
   511     }
   512 
   513 }
   514 
   515 // MARK: - MessageFolderDelegate
   516 
   517 extension EmailListViewController: MessageFolderDelegate {
   518     func didChange(messageFolder: MessageFolder) {
   519         GCD.onMainWait {
   520             self.didChangeInternal(messageFolder: messageFolder)
   521         }
   522     }
   523 }
   524 
   525 extension EmailListViewController: TableViewUpdate {
   526     func updateView() {
   527         self.tableView.reloadData()
   528     }
   529 }