pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author Dirk Zimmermann <dirk@pep-project.org>
Wed, 23 Aug 2017 10:54:10 +0200
changeset 2818 91e41e9e4b4f
parent 2804 8f0caacaa004
child 2838 5210d224f53c
permissions -rw-r--r--
IOS-664 tableViewUpdate -> TableViewUpdate
     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             self.tableView.reloadData()
   264         }
   265 
   266         let title = NSLocalizedString("Delete", comment: "Message action (on swipe)")
   267         return createRowAction(
   268             cell: cell, image: UIImage(named: "swipe-trash"), action: action,
   269             title: "\n\n\(title)")
   270     }
   271 
   272     func createMarkAsReadAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   273         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   274             if cell.haveSeen(message: message) {
   275                 message.imapFlags?.seen = false
   276             } else {
   277                 message.imapFlags?.seen = true
   278             }
   279             self.tableView.reloadRows(at: [indexPath], with: .none)
   280         }
   281 
   282         var title = NSLocalizedString(
   283             "Unread", comment: "Message action (on swipe)")
   284         if !cell.haveSeen(message: message) {
   285             title = NSLocalizedString(
   286                 "Read", comment: "Message action (on swipe)")
   287         }
   288 
   289         let isReadAction = createRowAction(cell: cell, image: nil, action: action,
   290                                            title: title)
   291         isReadAction.backgroundColor = UIColor.blue
   292 
   293         return isReadAction
   294     }
   295 
   296     func createMoreAction(message: Message, cell: EmailListViewCell) -> UITableViewRowAction {
   297         func action(action: UITableViewRowAction, indexPath: IndexPath) -> Void {
   298             self.showMoreActionSheet(cell: cell)
   299         }
   300 
   301         let title = NSLocalizedString("More", comment: "Message action (on swipe)")
   302         return createRowAction(
   303             cell: cell, image: UIImage(named: "swipe-more"), action: action,
   304             title: "\n\n\(title)")
   305     }
   306 
   307     // MARK: - Action Sheet
   308 
   309     func showMoreActionSheet(cell: EmailListViewCell) {
   310         let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
   311         alertControler.view.tintColor = .pEpGreen
   312         let cancelAction = createCancelAction()
   313         let replyAction = createReplyAction(cell: cell)
   314         let replyAllAction = createReplyAllAction(cell: cell)
   315         let forwardAction = createForwardAction(cell: cell)
   316         alertControler.addAction(cancelAction)
   317         alertControler.addAction(replyAction)
   318         alertControler.addAction(replyAllAction)
   319         alertControler.addAction(forwardAction)
   320         if let popoverPresentationController = alertControler.popoverPresentationController {
   321             popoverPresentationController.sourceView = cell
   322         }
   323         present(alertControler, animated: true, completion: nil)
   324     }
   325 
   326     // MARK: - Action Sheet Actions
   327 
   328     func createCancelAction() -> UIAlertAction {
   329         return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in}
   330     }
   331 
   332     func createReplyAction(cell: EmailListViewCell) ->  UIAlertAction {
   333         return UIAlertAction(title: "Reply", style: .default) { (action) in
   334             // self.performSegue(withIdentifier: self.segueCompose, sender: cell)
   335             self.performSegue(withIdentifier: .segueReply, sender: cell)
   336         }
   337     }
   338 
   339     func createReplyAllAction(cell: EmailListViewCell) ->  UIAlertAction {
   340         return UIAlertAction(title: "Reply All", style: .default) { (action) in
   341             self.performSegue(withIdentifier: .segueReplyAll, sender: cell)
   342         }
   343     }
   344 
   345     func createForwardAction(cell: EmailListViewCell) -> UIAlertAction {
   346         return UIAlertAction(title: "Forward", style: .default) { (action) in
   347             self.performSegue(withIdentifier: .segueForward, sender: cell)
   348         }
   349     }
   350 
   351 }
   352 
   353 extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
   354     public func updateSearchResults(for searchController: UISearchController) {
   355         if let vm = viewModel {
   356             vm.filterContentForSearchText(searchText: searchController.searchBar.text!, clear: false)
   357         }
   358     }
   359 
   360     func didDismissSearchController(_ searchController: UISearchController) {
   361         if let vm = viewModel {
   362             vm.filterContentForSearchText(clear: true)
   363         }
   364     }
   365 }
   366 
   367 // MARK: - Navigation
   368 
   369 extension EmailListViewController: SegueHandlerType {
   370 
   371     // MARK: - SegueHandlerType
   372 
   373     enum SegueIdentifier: String {
   374         case segueAddNewAccount
   375         case segueEditAccounts
   376         case segueShowEmail
   377         case segueCompose
   378         case segueReply
   379         case segueReplyAll
   380         case segueForward
   381         case segueFilter
   382         case segueFolderViews
   383         case noSegue
   384     }
   385 
   386     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   387         switch segueIdentifier(for: segue) {
   388         case .segueReply:
   389             if let nav = segue.destination as? UINavigationController,
   390                 let destination = nav.topViewController as? ComposeTableViewController,
   391                 let cell = sender as? EmailListViewCell,
   392                 let indexPath = self.tableView.indexPath(for: cell),
   393                 let email = cell.messageAt(indexPath: indexPath, config: config) {
   394                 destination.composeMode = .replyFrom
   395                 destination.appConfig = config?.appConfig
   396                 destination.originalMessage = email
   397             }
   398         case .segueReplyAll:
   399             if 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) {
   404                 destination.composeMode = .replyAll
   405                 destination.appConfig = config?.appConfig
   406                 destination.originalMessage = email
   407             }
   408             break
   409         case .segueShowEmail:
   410             if let vc = segue.destination as? EmailViewController,
   411                 let cell = sender as? EmailListViewCell,
   412                 let indexPath = self.tableView.indexPath(for: cell),
   413                 let email = cell.messageAt(indexPath: indexPath, config: config) {
   414                 vc.appConfig = config?.appConfig
   415                 vc.message = email
   416                 vc.folderShow = viewModel?.folderToShow
   417                 vc.messageId = indexPath.row
   418             }
   419             break
   420         case .segueForward:
   421             if let nav = segue.destination as? UINavigationController,
   422                 let destination = nav.topViewController as? ComposeTableViewController,
   423                 let cell = sender as? EmailListViewCell,
   424                 let indexPath = self.tableView.indexPath(for: cell),
   425                 let email = cell.messageAt(indexPath: indexPath, config: config) {
   426                 destination.composeMode = .forward
   427                 destination.appConfig = config?.appConfig
   428                 destination.originalMessage = email
   429             }
   430             break
   431         case .segueFilter:
   432             if let destiny = segue.destination as? FilterTableViewController {
   433                 destiny.filterDelegate = self.viewModel
   434                 destiny.inFolder = false
   435                 destiny.filterEnabled = self.viewModel?.folderToShow?.filter
   436                 destiny.hidesBottomBarWhenPushed = true
   437             }
   438             break
   439         case .segueAddNewAccount:
   440             if let vc = segue.destination as? LoginTableViewController {
   441                 vc.appConfig = config?.appConfig
   442                 vc.hidesBottomBarWhenPushed = true
   443             }
   444         case .segueFolderViews:
   445             if let vC = segue.destination as? FolderTableViewController {
   446                 vC.appConfig = config?.appConfig
   447                 vC.hidesBottomBarWhenPushed = true
   448             }
   449         case .segueEditAccounts, .segueCompose, .noSegue:
   450             break
   451         }
   452 
   453     }
   454     
   455     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   456     }
   457 
   458     func didChangeInternal(messageFolder: MessageFolder) {
   459         if let folder = config?.folder,
   460             let message = messageFolder as? Message,
   461             folder.contains(message: message, deletedMessagesAreContained: true) {
   462             if message.isOriginal {
   463                 // new message has arrived
   464                 if let index = folder.indexOf(message: message) {
   465                     let ip = IndexPath(row: index, section: 0)
   466                     Log.info(
   467                         component: #function,
   468                         content: "insert message at \(index), \(folder.messageCount()) messages")
   469                     tableView.insertRows(at: [ip], with: .automatic)
   470                 } else {
   471                     tableView.reloadData()
   472                 }
   473             } else if message.isGhost {
   474                 if let vm = viewModel, let cell = vm.cellFor(message: message), let ip = tableView.indexPath(for: cell) {
   475                     Log.info(
   476                         component: #function,
   477                         content: "delete message at \(index), \(folder.messageCount()) messages")
   478                     tableView.deleteRows(at: [ip], with: .automatic)
   479                 } else {
   480                     tableView.reloadData()
   481                 }
   482             } else {
   483                 // other flags than delete must have been changed
   484                 if let vm = viewModel, let cell = vm.cellFor(message: message) {
   485                     cell.updateFlags(message: message)
   486                 } else {
   487                     tableView.reloadData()
   488                 }
   489             }
   490         }
   491     }
   492 
   493 }
   494 
   495 // MARK: - MessageFolderDelegate
   496 
   497 extension EmailListViewController: MessageFolderDelegate {
   498     func didChange(messageFolder: MessageFolder) {
   499         GCD.onMainWait {
   500             self.didChangeInternal(messageFolder: messageFolder)
   501         }
   502     }
   503 }
   504 
   505 extension EmailListViewController: TableViewUpdate {
   506     func updateView() {
   507         self.tableView.reloadData()
   508     }
   509 }