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