pEpForiOS/UI/EmailDisplay/EmailListViewController.swift
author Dirk Zimmermann <dirk@pep-project.org>
Mon, 17 Oct 2016 16:48:35 +0200
branchIOS-222
changeset 810 42d0f50da61b
parent 808 fa37eb55380d
child 819 1e2af6f3d05b
permissions -rw-r--r--
mv CdFolder extensions in separate class
dirk@30
     1
//
dirk@30
     2
//  EmailListViewController.swift
dirk@30
     3
//  pEpForiOS
dirk@30
     4
//
dirk@30
     5
//  Created by Dirk Zimmermann on 16/04/16.
dirk@30
     6
//  Copyright © 2016 p≡p Security S.A. All rights reserved.
dirk@30
     7
//
dirk@30
     8
dirk@30
     9
import Foundation
dirk@30
    10
import UIKit
dirk@31
    11
import CoreData
dirk@808
    12
dirk@810
    13
import MessageModel
dirk@810
    14
dirk@784
    15
fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
dirk@784
    16
  switch (lhs, rhs) {
dirk@784
    17
  case let (l?, r?):
dirk@784
    18
    return l < r
dirk@784
    19
  case (nil, _?):
dirk@784
    20
    return true
dirk@784
    21
  default:
dirk@784
    22
    return false
dirk@784
    23
  }
dirk@784
    24
}
dirk@784
    25
dirk@784
    26
fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
dirk@784
    27
  switch (lhs, rhs) {
dirk@784
    28
  case let (l?, r?):
dirk@784
    29
    return l > r
dirk@784
    30
  default:
dirk@784
    31
    return rhs < lhs
dirk@784
    32
  }
dirk@784
    33
}
dirk@784
    34
dirk@30
    35
dirk@745
    36
class EmailListViewController: FetchTableViewController {
dirk@591
    37
    struct EmailListConfig {
dirk@591
    38
        let appConfig: AppConfig
dirk@583
    39
dirk@591
    40
        /** Set to whatever criteria you want to have mails displayed */
dirk@591
    41
        let predicate: NSPredicate?
dirk@583
    42
dirk@591
    43
        /** The sort descriptors to be used for displaying emails */
dirk@591
    44
        let sortDescriptors: [NSSortDescriptor]?
dirk@583
    45
dirk@606
    46
        /** If applicable, the account to refresh from */
dirk@802
    47
        let account: CdAccount?
dirk@585
    48
dirk@591
    49
        /** If applicable, the folder name to sync */
dirk@591
    50
        let folderName: String?
dirk@593
    51
dirk@593
    52
        /** Should there be a sync directly when the view appears? */
dirk@593
    53
        let syncOnAppear: Bool
dirk@591
    54
    }
dirk@58
    55
dirk@583
    56
    struct UIState {
dirk@583
    57
        var isSynching: Bool = false
dirk@583
    58
    }
dirk@583
    59
    
dirk@273
    60
    let segueShowEmail = "segueShowEmail"
dirk@397
    61
    let segueCompose = "segueCompose"
dirk@397
    62
    let segueUserSettings = "segueUserSettings"
dirk@30
    63
dirk@583
    64
    var config: EmailListConfig!
dirk@583
    65
dirk@275
    66
    var state = UIState()
dirk@342
    67
    let dateFormatter = UIHelper.dateFormatterEmailList()
dirk@275
    68
dirk@502
    69
    /**
dirk@502
    70
     The default background color for an email cell, as determined the first time a cell is
dirk@502
    71
     created.
dirk@502
    72
     */
dirk@502
    73
    var defaultCellBackgroundColor: UIColor?
dirk@502
    74
dirk@502
    75
    /**
dirk@502
    76
     Indicates whether `defaultCellBackgroundColor` has been determined or not.
dirk@502
    77
     */
dirk@502
    78
    var determinedCellBackgroundColor: Bool = false
dirk@502
    79
dirk@594
    80
    var refreshController: UIRefreshControl!
dirk@594
    81
dirk@705
    82
    /**
dirk@705
    83
     The message that should be saved as a draft when compose gets aborted.
dirk@705
    84
     */
dirk@802
    85
    var draftMessageToStore: CdMessage?
dirk@705
    86
dirk@719
    87
    /**
dirk@719
    88
     When the user taps on a draft email, this is the message that was selected
dirk@719
    89
     and should be given to the compose view.
dirk@719
    90
     */
dirk@802
    91
    var draftMessageToCompose: CdMessage?
dirk@719
    92
dirk@745
    93
    required init?(coder aDecoder: NSCoder) {
dirk@745
    94
        super.init(coder: aDecoder)
dirk@745
    95
        self.comp = "EmailListViewController"
dirk@745
    96
    }
dirk@745
    97
dirk@802
    98
    func isReadedMessage(_ message: CdMessage)-> Bool {
dirk@637
    99
        return message.flagSeen.boolValue
ana@614
   100
    }
ana@614
   101
dirk@802
   102
    func isImportantMessage(_ message: CdMessage)-> Bool {
ana@621
   103
        return message.flagFlagged.boolValue
ana@614
   104
    }
ana@614
   105
dirk@275
   106
    override func viewDidLoad() {
dirk@594
   107
        refreshController = UIRefreshControl.init()
dirk@593
   108
        refreshController.addTarget(self, action: #selector(self.fetchMailsRefreshControl(_:)),
dirk@784
   109
                                    for: UIControlEvents.valueChanged)
dirk@356
   110
        UIHelper.variableCellHeightsTableView(self.tableView)
dirk@275
   111
    }
dirk@31
   112
dirk@784
   113
    override func viewWillAppear(_ animated: Bool) {
dirk@594
   114
        // Disable fetching if there is no account
dirk@594
   115
        if config.account != nil {
dirk@594
   116
            self.refreshControl = refreshController
dirk@594
   117
        } else {
dirk@594
   118
            self.refreshControl = nil
dirk@594
   119
        }
dirk@594
   120
dirk@209
   121
        prepareFetchRequest()
dirk@593
   122
        if config.syncOnAppear {
dirk@593
   123
            fetchMailsRefreshControl()
dirk@593
   124
        }
ana@207
   125
        super.viewWillAppear(animated)
ana@207
   126
    }
ana@152
   127
dirk@784
   128
    func fetchMailsRefreshControl(_ refreshControl: UIRefreshControl? = nil) {
dirk@583
   129
        if let account = config.account {
dirk@55
   130
            let connectInfo = account.connectInfo
dirk@55
   131
dirk@275
   132
            state.isSynching = true
dirk@588
   133
            updateUI()
dirk@368
   134
dirk@588
   135
            config.appConfig.grandOperator.fetchEmailsAndDecryptConnectInfos(
dirk@590
   136
                [connectInfo], folderName: config.folderName,
dirk@466
   137
                completionBlock: { error in
dirk@466
   138
                    Log.infoComponent(self.comp, "Sync completed, error: \(error)")
dirk@707
   139
                    UIHelper.displayError(error, controller: self)
dirk@583
   140
                    self.config.appConfig.model.save()
dirk@466
   141
                    self.state.isSynching = false
dirk@466
   142
                    refreshControl?.endRefreshing()
dirk@466
   143
                    self.updateUI()
dirk@466
   144
            })
dirk@594
   145
        } else {
dirk@594
   146
            state.isSynching = false
dirk@594
   147
            updateUI()
dirk@55
   148
        }
dirk@31
   149
    }
dirk@31
   150
dirk@784
   151
    @IBAction func mailSentSegue(_ segue: UIStoryboardSegue) {
dirk@452
   152
    }
dirk@452
   153
dirk@784
   154
    @IBAction func backFromComposeWithoutSavingDraftSegue(_ segue: UIStoryboardSegue) {
dirk@705
   155
    }
dirk@705
   156
dirk@784
   157
    @IBAction func backFromComposeSaveDraftSegue(_ segue: UIStoryboardSegue) {
dirk@705
   158
        guard let msg = draftMessageToStore else {
dirk@705
   159
            return
dirk@705
   160
        }
dirk@707
   161
dirk@707
   162
        state.isSynching = true
dirk@707
   163
        updateUI()
dirk@707
   164
dirk@706
   165
        config.appConfig.grandOperator.saveDraftMail(
dirk@706
   166
            msg, account: msg.folder.account, completionBlock: { error in
dirk@707
   167
                GCD.onMain() {
dirk@709
   168
                    UIHelper.displayError(error, controller: self)
dirk@707
   169
                    self.state.isSynching = false
dirk@707
   170
                    self.updateUI()
dirk@707
   171
                }
dirk@706
   172
        })
dirk@705
   173
    }
dirk@705
   174
ana@143
   175
    func prepareFetchRequest() {
dirk@802
   176
        let fetchRequest = NSFetchRequest<NSManagedObject>.init(entityName: CdMessage.entityName())
dirk@583
   177
        fetchRequest.predicate = config.predicate
dirk@583
   178
        fetchRequest.sortDescriptors = config.sortDescriptors
dirk@31
   179
        fetchController = NSFetchedResultsController.init(
dirk@31
   180
            fetchRequest: fetchRequest,
dirk@583
   181
            managedObjectContext: config.appConfig.coreDataUtil.managedObjectContext,
dirk@31
   182
            sectionNameKeyPath: nil, cacheName: nil)
dirk@31
   183
        fetchController?.delegate = self
dirk@31
   184
        do {
dirk@31
   185
            try fetchController?.performFetch()
dirk@31
   186
        } catch let err as NSError {
dirk@414
   187
            Log.errorComponent(comp, error: err)
dirk@31
   188
        }
dirk@31
   189
    }
dirk@31
   190
dirk@58
   191
    // MARK: - UI State
dirk@58
   192
dirk@58
   193
    func updateUI() {
dirk@784
   194
        UIApplication.shared.isNetworkActivityIndicatorVisible = state.isSynching
dirk@748
   195
        if !state.isSynching {
dirk@594
   196
            self.refreshControl?.endRefreshing()
dirk@58
   197
        }
dirk@58
   198
    }
dirk@58
   199
dirk@31
   200
    // MARK: - UITableViewDataSource
dirk@31
   201
dirk@784
   202
    override func numberOfSections(in tableView: UITableView) -> Int {
dirk@209
   203
        if let count = fetchController?.sections?.count {
dirk@209
   204
            return count
dirk@209
   205
        } else {
dirk@209
   206
            return 0
dirk@209
   207
        }
dirk@31
   208
    }
dirk@31
   209
dirk@784
   210
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dirk@31
   211
        if fetchController?.sections?.count > 0 {
dirk@31
   212
            if let sections = fetchController?.sections {
dirk@31
   213
                let sectionInfo = sections[section]
dirk@31
   214
                return sectionInfo.numberOfObjects
dirk@31
   215
            }
dirk@31
   216
        }
dirk@31
   217
        return 0
dirk@31
   218
    }
dirk@31
   219
dirk@784
   220
    override func tableView(_ tableView: UITableView,
dirk@784
   221
                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
dirk@784
   222
        let cell = tableView.dequeueReusableCell(
dirk@784
   223
            withIdentifier: "EmailListViewCell", for: indexPath) as! EmailListViewCell
dirk@502
   224
        if !determinedCellBackgroundColor {
dirk@502
   225
            defaultCellBackgroundColor = cell.backgroundColor
dirk@502
   226
            determinedCellBackgroundColor = true
dirk@502
   227
        }
dirk@31
   228
        configureCell(cell, indexPath: indexPath)
dirk@31
   229
        return cell
dirk@31
   230
    }
dirk@31
   231
dirk@719
   232
    // MARK: - UITableViewDelegate
dirk@719
   233
dirk@784
   234
    override func tableView(_ tableView: UITableView,
dirk@784
   235
                            didSelectRowAt indexPath: IndexPath) {
dirk@719
   236
        draftMessageToCompose = nil
dirk@719
   237
dirk@784
   238
        let cell = tableView.cellForRow(at: indexPath)
dirk@719
   239
dirk@719
   240
        if let fn = config.folderName, let ac = config.account,
dirk@719
   241
            let folder = config.appConfig.model.folderByName(fn, email: ac.email) {
dirk@784
   242
            if folder.folderType.intValue == FolderType.drafts.rawValue {
dirk@784
   243
                draftMessageToCompose = fetchController?.object(at: indexPath)
dirk@802
   244
                    as? CdMessage
dirk@784
   245
                performSegue(withIdentifier: segueCompose, sender: cell)
dirk@719
   246
                return
dirk@719
   247
            }
dirk@719
   248
        }
dirk@719
   249
dirk@784
   250
        performSegue(withIdentifier: segueShowEmail, sender: cell)
dirk@719
   251
    }
dirk@719
   252
dirk@784
   253
    override func tableView(_ tableView: UITableView, editActionsForRowAt
dirk@784
   254
        indexPath: IndexPath)-> [UITableViewRowAction]? {
dirk@744
   255
dirk@784
   256
        let cell = tableView.cellForRow(at: indexPath) as! EmailListViewCell
dirk@802
   257
        let email = fetchController?.object(at: indexPath) as! CdMessage
dirk@744
   258
dirk@744
   259
        let isFlagAction = createIsFlagAction(email, cell: cell)
dirk@744
   260
        let deleteAction = createDeleteAction(cell)
dirk@744
   261
        let isReadAction = createIsReadAction(email, cell: cell)
dirk@744
   262
        return [deleteAction,isFlagAction,isReadAction]
dirk@744
   263
    }
dirk@744
   264
dirk@719
   265
    // MARK: - Misc
dirk@719
   266
dirk@784
   267
    override func configureCell(_ theCell: UITableViewCell, indexPath: IndexPath) {
dirk@745
   268
        guard let cell = theCell as? EmailListViewCell else {
dirk@745
   269
            return
dirk@745
   270
        }
dirk@802
   271
        if let email = fetchController?.object(at: indexPath) as? CdMessage {
dirk@784
   272
            if let colorRating = PEPUtil.colorRatingFromInt(email.pepColorRating?.intValue) {
dirk@574
   273
                let privacyColor = PEPUtil.colorFromPepRating(colorRating)
dirk@502
   274
                if let uiColor = UIHelper.textBackgroundUIColorFromPrivacyColor(privacyColor) {
dirk@502
   275
                    cell.backgroundColor = uiColor
dirk@502
   276
                } else {
dirk@502
   277
                    if determinedCellBackgroundColor {
dirk@502
   278
                        cell.backgroundColor = defaultCellBackgroundColor
dirk@502
   279
                    }
dirk@502
   280
                }
dirk@502
   281
            }
dirk@342
   282
            UIHelper.putString(email.from?.displayString(), toLabel: cell.senderLabel)
dirk@342
   283
            UIHelper.putString(email.subject, toLabel: cell.subjectLabel)
dirk@572
   284
dirk@572
   285
            // Snippet
dirk@565
   286
            if let text = email.longMessage {
dirk@572
   287
                let theText = text.replaceNewLinesWith(" ").trimmedWhiteSpace()
dirk@565
   288
                UIHelper.putString(theText, toLabel: cell.summaryLabel)
dirk@572
   289
            } else if let html = email.longMessageFormatted {
dirk@568
   290
                var text = html.extractTextFromHTML()
dirk@572
   291
                text = text.replaceNewLinesWith(" ").trimmedWhiteSpace()
dirk@568
   292
                UIHelper.putString(text, toLabel: cell.summaryLabel)
dirk@568
   293
            } else {
dirk@568
   294
                UIHelper.putString(nil, toLabel: cell.summaryLabel)
dirk@565
   295
            }
dirk@32
   296
dirk@480
   297
            if let receivedDate = email.receivedDate {
dirk@788
   298
                UIHelper.putString(dateFormatter.string(from: receivedDate as Date),
dirk@342
   299
                                   toLabel: cell.dateLabel)
dirk@32
   300
            } else {
dirk@342
   301
                UIHelper.putString(nil, toLabel: cell.dateLabel)
dirk@32
   302
            }
ana@620
   303
ana@656
   304
            if (isImportantMessage(email) && isReadedMessage(email)) {
dirk@784
   305
                cell.isImportantImage.isHidden = false
dirk@784
   306
                cell.isImportantImage.backgroundColor = UIColor.orange
ana@666
   307
            }
ana@666
   308
            else if (isImportantMessage(email) && !isReadedMessage(email)) {
dirk@784
   309
                cell.isImportantImage.isHidden = false
dirk@784
   310
                cell.isImportantImage.backgroundColor = UIColor.blue
ana@656
   311
                cell.isImportantImage.layer.borderWidth = 2
dirk@784
   312
                cell.isImportantImage.layer.borderColor = UIColor.orange.cgColor
ana@666
   313
            } else if (!isImportantMessage(email) && isReadedMessage(email)) {
dirk@784
   314
                    cell.isImportantImage.isHidden = true
ana@666
   315
            } else if (!isImportantMessage(email) && !isReadedMessage(email)) {
dirk@784
   316
                cell.isImportantImage.isHidden = false
dirk@784
   317
                cell.isImportantImage.backgroundColor = UIColor.blue
ana@620
   318
            }
dirk@31
   319
        }
dirk@30
   320
    }
dirk@30
   321
dirk@784
   322
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
dirk@606
   323
        // Make sure the current account is set, if defined
dirk@606
   324
        config.appConfig.currentAccount = config.account
dirk@606
   325
dirk@361
   326
        if segue.identifier == segueCompose {
dirk@784
   327
            let destination = segue.destination
dirk@434
   328
                as! ComposeViewController
dirk@583
   329
            destination.appConfig = config.appConfig
dirk@719
   330
            if let draft = draftMessageToCompose {
dirk@723
   331
                draft.flagSeen = true
dirk@723
   332
                draft.updateFlags()
dirk@723
   333
                config.appConfig.model.save()
dirk@723
   334
dirk@719
   335
                destination.originalMessage = draft
dirk@784
   336
                destination.composeMode = .composeDraft
dirk@719
   337
            }
dirk@273
   338
        } else if segue.identifier == segueShowEmail {
dirk@273
   339
            guard
dirk@784
   340
                let vc = segue.destination as? EmailViewController,
dirk@273
   341
                let cell = sender as? UITableViewCell,
dirk@784
   342
                let indexPath = self.tableView.indexPath(for: cell),
dirk@802
   343
                let email = fetchController?.object(at: indexPath) as? CdMessage else {
dirk@273
   344
                    return
dirk@273
   345
            }
dirk@583
   346
            vc.appConfig = config.appConfig
dirk@273
   347
            vc.message = email
ana@246
   348
        }
ana@246
   349
    }
dirk@744
   350
dirk@802
   351
    func syncFlagsToServer(_ message: CdMessage) {
dirk@744
   352
        self.config.appConfig.grandOperator.syncFlagsToServerForFolder(
dirk@744
   353
            message.folder,
dirk@744
   354
            completionBlock: { error in
dirk@744
   355
                UIHelper.displayError(error, controller: self)
dirk@744
   356
        })
dirk@744
   357
    }
dirk@744
   358
dirk@802
   359
    func createIsFlagAction(_ message: CdMessage, cell: EmailListViewCell) -> UITableViewRowAction {
dirk@744
   360
dirk@744
   361
        // preparing the title action to show when user swipe
dirk@744
   362
        var localizedIsFlagTitle = " "
dirk@744
   363
        if (isImportantMessage(message)) {
dirk@744
   364
            localizedIsFlagTitle = NSLocalizedString("Unflag",
dirk@744
   365
                                                     comment: "Unflag button title in swipe action on EmailListViewController")
dirk@744
   366
        } else {
dirk@744
   367
            localizedIsFlagTitle = NSLocalizedString("Flag",
dirk@744
   368
                                                     comment: "Flag button title in swipe action on EmailListViewController")
dirk@744
   369
        }
dirk@744
   370
dirk@744
   371
        // preparing action to trigger when user swipe
dirk@784
   372
        let isFlagCompletionHandler: (UITableViewRowAction, IndexPath) -> Void =
dirk@744
   373
            { (action, indexPath) in
dirk@744
   374
                if (self.isImportantMessage(message)) {
dirk@744
   375
                    message.flagFlagged = false
dirk@744
   376
dirk@744
   377
                } else {
dirk@744
   378
                    message.flagFlagged = true
dirk@744
   379
                }
dirk@744
   380
                message.updateFlags()
dirk@744
   381
                self.syncFlagsToServer(message)
dirk@784
   382
                self.tableView.reloadRows(at: [indexPath], with: .none)
dirk@744
   383
        }
dirk@744
   384
        // creating the action
dirk@784
   385
        let isFlagAction = UITableViewRowAction(style: .default, title: localizedIsFlagTitle,
dirk@744
   386
                                                handler: isFlagCompletionHandler)
dirk@744
   387
        // changing default action color
dirk@784
   388
        isFlagAction.backgroundColor = UIColor.orange
dirk@744
   389
dirk@744
   390
        return isFlagAction
dirk@744
   391
    }
dirk@744
   392
dirk@784
   393
    func createDeleteAction (_ cell: EmailListViewCell) -> UITableViewRowAction {
dirk@744
   394
dirk@744
   395
        // preparing the title action to show when user swipe
dirk@744
   396
        let localizedDeleteTitle = NSLocalizedString("Erase",
dirk@744
   397
                                                     comment: "Erase button title in swipe action on EmailListViewController")
dirk@744
   398
dirk@784
   399
        let deleteCompletionHandler: (UITableViewRowAction, IndexPath) -> Void =
dirk@744
   400
            { (action, indexPath) in
dirk@802
   401
                let managedObject = self.fetchController?.object(at: indexPath) as? CdMessage
dirk@744
   402
                managedObject?.flagDeleted = true
dirk@744
   403
                managedObject?.updateFlags()
dirk@744
   404
                self.syncFlagsToServer(managedObject!)
dirk@744
   405
        }
dirk@744
   406
dirk@744
   407
        // creating the action
dirk@784
   408
        let deleteAction = UITableViewRowAction(style: .default, title: localizedDeleteTitle,
dirk@744
   409
                                                handler: deleteCompletionHandler)
dirk@744
   410
        return deleteAction
dirk@744
   411
    }
dirk@744
   412
dirk@802
   413
    func createIsReadAction (_ message: CdMessage, cell: EmailListViewCell) -> UITableViewRowAction {
dirk@744
   414
dirk@744
   415
        // preparing the title action to show when user swipe
dirk@744
   416
        var localizedisReadTitle = " "
dirk@744
   417
        if (isReadedMessage(message)) {
dirk@744
   418
            localizedisReadTitle = NSLocalizedString("Unread",
dirk@744
   419
                                                     comment: "Unread button title in swipe action on EmailListViewController")
dirk@744
   420
        } else {
dirk@744
   421
            localizedisReadTitle = NSLocalizedString("Read",
dirk@744
   422
                                                     comment: "Read button title in swipe action on EmailListViewController")
dirk@744
   423
        }
dirk@744
   424
dirk@744
   425
        // creating the action
dirk@784
   426
        let isReadCompletionHandler: (UITableViewRowAction, IndexPath) -> Void =
dirk@744
   427
            { (action, indexPath) in
dirk@744
   428
                if (self.isReadedMessage(message)) {
dirk@744
   429
                    message.flagSeen = false
dirk@744
   430
                    message.updateFlags()
dirk@744
   431
                } else {
dirk@744
   432
                    message.flagSeen = true
dirk@744
   433
                    message.updateFlags()
dirk@744
   434
                }
dirk@744
   435
                self.syncFlagsToServer(message)
dirk@784
   436
                self.tableView.reloadRows(at: [indexPath], with: .none)
dirk@744
   437
        }
dirk@784
   438
        let isReadAction = UITableViewRowAction(style: .default, title: localizedisReadTitle,
dirk@744
   439
                                                handler: isReadCompletionHandler)
dirk@784
   440
        isReadAction.backgroundColor = UIColor.blue
dirk@744
   441
dirk@744
   442
        return isReadAction
dirk@744
   443
    }
dirk@784
   444
}