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