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