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