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