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