pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author buff <andreas@pep-project.org>
Thu, 10 Aug 2017 16:57:18 +0200
changeset 2697 a9130d482e40
parent 2687 bb1dcce7a0d6
child 2715 50b73f5ea496
permissions -rw-r--r--
fixes: unified inbox messes with persistant store, even it never gets saved
     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: .segueCompose, 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 segueReplyAll
   387         case segueForward
   388         case segueFilter
   389         case segueFolderViews
   390         case noSegue
   391     }
   392 
   393     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   394         switch segueIdentifier(for: segue) {
   395         case .segueReplyAll:
   396             if let nav = segue.destination as? UINavigationController,
   397                 let destination = nav.topViewController as? ComposeTableViewController,
   398                 let cell = sender as? EmailListViewCell,
   399                 let indexPath = self.tableView.indexPath(for: cell),
   400                 let email = cell.messageAt(indexPath: indexPath, config: config) {
   401                 destination.composeMode = .replyAll
   402                 destination.appConfig = config?.appConfig
   403                 destination.originalMessage = email
   404             }
   405             break
   406         case .segueShowEmail:
   407             if let vc = segue.destination as? EmailViewController,
   408                 let cell = sender as? EmailListViewCell,
   409                 let indexPath = self.tableView.indexPath(for: cell),
   410                 let email = cell.messageAt(indexPath: indexPath, config: config) {
   411                 vc.appConfig = config?.appConfig
   412                 vc.message = email
   413                 vc.folderShow = viewModel?.folderToShow
   414                 vc.messageId = indexPath.row
   415             }
   416             break
   417         case .segueForward:
   418             if let nav = segue.destination as? UINavigationController,
   419                 let destination = nav.topViewController as? ComposeTableViewController,
   420                 let cell = sender as? EmailListViewCell,
   421                 let indexPath = self.tableView.indexPath(for: cell),
   422                 let email = cell.messageAt(indexPath: indexPath, config: config) {
   423                 destination.composeMode = .forward
   424                 destination.appConfig = config?.appConfig
   425                 destination.originalMessage = email
   426             }
   427             break
   428         case .segueFilter:
   429             if let destiny = segue.destination as? FilterTableViewController {
   430                 destiny.filterDelegate = self.viewModel
   431                 destiny.inFolder = false
   432                 destiny.filterEnabled = self.viewModel?.folderToShow?.filter
   433                 destiny.hidesBottomBarWhenPushed = true
   434             }
   435             break
   436         case .segueAddNewAccount:
   437             if let vc = segue.destination as? LoginTableViewController {
   438                 vc.appConfig = config?.appConfig
   439                 vc.hidesBottomBarWhenPushed = true
   440             }
   441         case .segueFolderViews:
   442             if let vC = segue.destination as? FolderTableViewController {
   443                 vC.appConfig = config?.appConfig
   444                 vC.hidesBottomBarWhenPushed = true
   445             }
   446         case .segueEditAccounts, .segueCompose, .noSegue:
   447             break
   448         }
   449 
   450     }
   451     
   452     @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
   453     }
   454 
   455     func didChangeInternal(messageFolder: MessageFolder) {
   456         if let folder = config?.folder,
   457             let message = messageFolder as? Message,
   458             folder.contains(message: message, deletedMessagesAreContained: true) {
   459             if message.isOriginal {
   460                 // new message has arrived
   461                 if let index = folder.indexOf(message: message) {
   462                     let ip = IndexPath(row: index, section: 0)
   463                     Log.info(
   464                         component: #function,
   465                         content: "insert message at \(index), \(folder.messageCount()) messages")
   466                     tableView.insertRows(at: [ip], with: .automatic)
   467                 } else {
   468                     tableView.reloadData()
   469                 }
   470             } else if message.isGhost {
   471                 if let vm = viewModel, let cell = vm.cellFor(message: message), let ip = tableView.indexPath(for: cell) {
   472                     Log.info(
   473                         component: #function,
   474                         content: "delete message at \(index), \(folder.messageCount()) messages")
   475                     tableView.deleteRows(at: [ip], with: .automatic)
   476                 } else {
   477                     tableView.reloadData()
   478                 }
   479             } else {
   480                 // other flags than delete must have been changed
   481                 if let vm = viewModel, let cell = vm.cellFor(message: message) {
   482                     cell.updateFlags(message: message)
   483                 } else {
   484                     tableView.reloadData()
   485                 }
   486             }
   487         }
   488     }
   489 
   490 }
   491 
   492 // MARK: - MessageFolderDelegate
   493 
   494 extension EmailListViewController: MessageFolderDelegate {
   495     func didChange(messageFolder: MessageFolder) {
   496         GCD.onMainWait {
   497             self.didChangeInternal(messageFolder: messageFolder)
   498         }
   499     }
   500 }
   501 
   502 extension EmailListViewController: tableViewUpdate {
   503     func updateView() {
   504         self.tableView.reloadData()
   505     }
   506 }