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