pEpForiOS/UI/EmailDisplayList/EmailListViewController.swift
author Xavier Algarra <xavier@pep-project.org>
Tue, 27 Mar 2018 15:30:54 +0200
changeset 4347 8792c0b2dafe
parent 4317 dc2cbe11bb3d
child 4348 20436ae8fa52
permissions -rw-r--r--
IOS-979 observer cleanet in the deinit of the listviewcontroller
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 UIKit
dirk@810
    10
import MessageModel
xavier@3368
    11
import SwipeCellKit
dirk@810
    12
xavier@3368
    13
class EmailListViewController: BaseTableViewController, SwipeTableViewCellDelegate {
andreas@3223
    14
    var folderToShow: Folder?
andreas@3677
    15
andreas@3677
    16
    func updateLastLookAt() {
andreas@3677
    17
        guard let saveFolder = folderToShow else {
andreas@3677
    18
            return
andreas@3677
    19
        }
andreas@3677
    20
        saveFolder.updateLastLookAt()
andreas@3677
    21
    }
andreas@3677
    22
    
andreas@3151
    23
    private var model: EmailListViewModel?
andreas@3677
    24
    
andreas@3151
    25
    private let queue: OperationQueue = {
andreas@3151
    26
        let createe = OperationQueue()
andreas@3151
    27
        createe.qualityOfService = .userInteractive
andreas@3678
    28
        createe.maxConcurrentOperationCount = 5
andreas@3151
    29
        return createe
andreas@3151
    30
    }()
andreas@3151
    31
    private var operations = [IndexPath:Operation]()
andreas@2838
    32
    public static let storyboardId = "EmailListViewController"
andreas@3151
    33
    fileprivate var lastSelectedIndexPath: IndexPath?
andreas@3208
    34
    
igor@1301
    35
    let searchController = UISearchController(searchResultsController: nil)
xavier@3368
    36
xavier@3368
    37
    //swipe acctions types
xavier@3368
    38
    var buttonDisplayMode: ButtonDisplayMode = .titleAndImage
xavier@3368
    39
    var buttonStyle: ButtonStyle = .backgroundColor
andreas@3678
    40
andreas@3678
    41
    /// Indicates that we must not trigger reloadData.
andreas@3678
    42
    private var loadingBlocked = false
andreas@3208
    43
    
andreas@3151
    44
    // MARK: - Outlets
andreas@3208
    45
    
dirk@2067
    46
    @IBOutlet weak var enableFilterButton: UIBarButtonItem!
dirk@2067
    47
    @IBOutlet weak var textFilterButton: UIBarButtonItem!
andreas@3151
    48
    @IBOutlet var showFoldersButton: UIBarButtonItem!
andreas@3677
    49
    
andreas@3151
    50
    // MARK: - Life Cycle
andreas@3208
    51
    
dirk@275
    52
    override func viewDidLoad() {
ylandert@935
    53
        super.viewDidLoad()
dirk@2189
    54
        title = NSLocalizedString("Inbox", comment: "General name for (unified) inbox")
igor@1284
    55
        UIHelper.emailListTableHeight(self.tableView)
andreas@3163
    56
        self.textFilterButton.isEnabled = false
igor@1301
    57
        addSearchBar()
xavier@4317
    58
        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
xavier@4317
    59
        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeInactive), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
xavier@4317
    60
    }
xavier@4317
    61
xavier@4347
    62
    deinit {
xavier@4347
    63
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
xavier@4347
    64
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
xavier@4347
    65
    }
xavier@4347
    66
xavier@4317
    67
    @objc func didBecomeActive() {
xavier@4317
    68
xavier@4317
    69
        if tableView.tableHeaderView == nil {
xavier@4317
    70
            tableView.tableHeaderView = searchController.searchBar
xavier@4347
    71
            //tableView.setContentOffset(CGPoint(x: 0.0, y: searchController.searchBar.frame.size.height), animated: false)
xavier@4317
    72
xavier@4317
    73
        }
xavier@4317
    74
    }
xavier@4317
    75
xavier@4317
    76
    @objc func didBecomeInactive() {
xavier@4317
    77
xavier@4317
    78
        tableView.tableHeaderView = nil
dirk@275
    79
    }
andreas@3208
    80
    
dirk@784
    81
    override func viewWillAppear(_ animated: Bool) {
igor@1301
    82
        super.viewWillAppear(animated)
xavier@2325
    83
        self.navigationController?.setToolbarHidden(false, animated: true)
dirk@1344
    84
        if MiscUtil.isUnitTest() {
dirk@1344
    85
            return
dirk@1344
    86
        }
andreas@3223
    87
dirk@1826
    88
        setDefaultColors()
andreas@3151
    89
        setup()
andreas@3208
    90
        
dirk@2191
    91
        // Mark this folder as having been looked at by the user
andreas@3151
    92
        updateLastLookAt()
andreas@3151
    93
        setupFoldersBarButton()
xavier@3562
    94
        if model != nil {
xavier@3562
    95
            updateFilterButtonView()
xavier@3562
    96
        }
andreas@3151
    97
    }
andreas@3208
    98
    
andreas@3151
    99
    // MARK: - NavigationBar
andreas@3208
   100
    
andreas@3151
   101
    private func hideFoldersNavigationBarButton() {
andreas@3151
   102
        self.showFoldersButton.isEnabled = false
andreas@3151
   103
        self.showFoldersButton.tintColor = UIColor.clear
andreas@3151
   104
    }
andreas@3208
   105
    
andreas@3151
   106
    private func showFoldersNavigationBarButton() {
andreas@3151
   107
        self.showFoldersButton.isEnabled = true
andreas@3151
   108
        self.showFoldersButton.tintColor = nil
andreas@3151
   109
    }
andreas@3208
   110
    
andreas@3151
   111
    private func resetModel() {
andreas@3223
   112
        if folderToShow != nil {
andreas@3291
   113
            model = EmailListViewModel(delegate: self,
andreas@3291
   114
                                       messageSyncService: appConfig.messageSyncService,
andreas@3291
   115
                                       folderToShow: folderToShow)
xavier@2585
   116
        }
dirk@1348
   117
    }
andreas@3208
   118
    
andreas@3151
   119
    private func setup() {
andreas@3289
   120
        if noAccountsExist() {
andreas@3272
   121
            // No account exists. Show account setup.
andreas@3272
   122
            performSegue(withIdentifier:.segueAddNewAccount, sender: self)
andreas@3289
   123
        } else if let vm = model {
andreas@3223
   124
            // We came back from e.g EmailView ...
andreas@3223
   125
            updateFilterText()
andreas@3223
   126
            // ... so we want to update "seen" status
andreas@3226
   127
            vm.reloadData()
andreas@3272
   128
        } else if folderToShow == nil {
andreas@3223
   129
            // We have not been created to show a specific folder, thus we show unified inbox
andreas@3151
   130
            folderToShow = UnifiedInbox()
andreas@3223
   131
            resetModel()
andreas@3272
   132
        } else if model == nil {
andreas@3223
   133
            // We still got no model, because:
andreas@3223
   134
            // - We are not coming back from a pushed view (for instance ComposeEmailView)
andreas@3223
   135
            // - We are not a UnifiedInbox
andreas@3226
   136
            // So we have been created to show a specific folder. Show it!
andreas@3223
   137
            resetModel()
andreas@3151
   138
        }
andreas@3272
   139
andreas@3151
   140
        self.title = realNameOfFolderToShow()
andreas@2683
   141
    }
andreas@3223
   142
andreas@3223
   143
    private func weCameBackFromAPushedView() -> Bool {
andreas@3223
   144
        return model != nil
andreas@3223
   145
    }
andreas@3208
   146
    
andreas@3151
   147
    private func noAccountsExist() -> Bool {
andreas@3151
   148
        return Account.all().isEmpty
igor@1301
   149
    }
andreas@3208
   150
    
andreas@3151
   151
    private func setupFoldersBarButton() {
andreas@3151
   152
        if let size = navigationController?.viewControllers.count, size > 1 {
andreas@3151
   153
            hideFoldersNavigationBarButton()
andreas@3151
   154
        } else {
andreas@3151
   155
            showFoldersNavigationBarButton()
xavier@2132
   156
        }
igor@1338
   157
    }
andreas@3208
   158
    
andreas@3151
   159
    private func addSearchBar() {
igor@1301
   160
        searchController.searchResultsUpdater = self
igor@1301
   161
        searchController.dimsBackgroundDuringPresentation = false
igor@1301
   162
        searchController.delegate = self
igor@1301
   163
        definesPresentationContext = true
igor@1301
   164
        tableView.tableHeaderView = searchController.searchBar
xavier@3209
   165
        tableView.setContentOffset(CGPoint(x: 0.0, y: searchController.searchBar.frame.size.height), animated: false)
igor@1301
   166
    }
andreas@3208
   167
    
andreas@3151
   168
    // MARK: - Other
andreas@3208
   169
    
andreas@3151
   170
    private func realNameOfFolderToShow() -> String? {
andreas@3224
   171
        return folderToShow?.realName
andreas@2697
   172
    }
andreas@3522
   173
andreas@3522
   174
    private func address(forRowAt indexPath: IndexPath) -> String {
andreas@3522
   175
        guard
andreas@3522
   176
            let saveModel = model,
andreas@3522
   177
            let row = saveModel.row(for: indexPath),
andreas@3522
   178
            let folder = folderToShow
andreas@3522
   179
            else {
andreas@3522
   180
                Log.shared.errorAndCrash(component: #function, errorString: "Invalid state")
andreas@3522
   181
                return ""
andreas@3522
   182
        }
andreas@3522
   183
        switch folder.folderType {
andreas@3522
   184
        case .all: fallthrough
andreas@3522
   185
        case .archive: fallthrough
andreas@3522
   186
        case .spam: fallthrough
andreas@3522
   187
        case .trash: fallthrough
andreas@3522
   188
        case .flagged: fallthrough
andreas@3522
   189
        case .inbox: fallthrough
andreas@3522
   190
        case .normal:
andreas@3522
   191
            return row.from
andreas@3522
   192
        case .drafts: fallthrough
andreas@3522
   193
        case .sent:
andreas@3522
   194
            return row.to
andreas@3522
   195
        }
andreas@3522
   196
    }
andreas@3522
   197
andreas@3522
   198
    private func shouldShowAccessoryDisclosureIndicator() -> Bool {
andreas@3522
   199
        guard let folder = folderToShow else {
andreas@3522
   200
            Log.shared.errorAndCrash(component: #function, errorString: "No folder")
andreas@3522
   201
            return true
andreas@3522
   202
        }
andreas@3522
   203
        if folder.folderType == .drafts {
andreas@3522
   204
            // Mails in drafts folder can only be opened in compose mode, which is shown modally.
andreas@3522
   205
            return false
andreas@3522
   206
        }
andreas@3522
   207
        return true
andreas@3522
   208
    }
andreas@3208
   209
    
andreas@3151
   210
    private func configure(cell: EmailListViewCell, for indexPath: IndexPath) {
andreas@3677
   211
        // Configure lightweight stuff on main thread ...
andreas@3677
   212
        guard let saveModel = model else {
andreas@3151
   213
            return
xavier@2967
   214
        }
andreas@3151
   215
        guard let row = saveModel.row(for: indexPath) else {
andreas@3151
   216
            Log.shared.errorAndCrash(component: #function, errorString: "We should have a row here")
andreas@3151
   217
            return
andreas@3151
   218
        }
andreas@3522
   219
        // Mails in drafts folder can only be opened in compose mode, which is shown modally.
andreas@3522
   220
        cell.accessoryDisclosureIndicator.isHidden = shouldShowAccessoryDisclosureIndicator()
andreas@3522
   221
        cell.addressLabel.text = address(forRowAt: indexPath)
andreas@3151
   222
        cell.subjectLabel.text = row.subject
andreas@3151
   223
        cell.summaryLabel.text = row.bodyPeek
andreas@3151
   224
        cell.isFlagged = row.isFlagged
andreas@3151
   225
        cell.isSeen = row.isSeen
andreas@3151
   226
        cell.hasAttachment = row.showAttchmentIcon
andreas@3151
   227
        cell.dateLabel.text = row.dateText
andreas@3151
   228
        // Set image from cache if any
andreas@3151
   229
        cell.setContactImage(image: row.senderContactImage)
andreas@3521
   230
andreas@3151
   231
        let op = BlockOperation() { [weak self] in
andreas@3419
   232
            MessageModel.performAndWait {
andreas@3419
   233
                // ... and expensive computations in background
andreas@3419
   234
                guard let strongSelf = self else {
andreas@3419
   235
                    // View is gone, nothing to do.
andreas@3419
   236
                    return
andreas@3419
   237
                }
andreas@3419
   238
                
andreas@3419
   239
                var senderImage: UIImage?
andreas@3419
   240
                if row.senderContactImage == nil {
andreas@3419
   241
                    // image for identity has not been cached yet
andreas@3522
   242
                    // Get (and cache) it here in the background ...
andreas@3419
   243
                    senderImage = strongSelf.model?.senderImage(forCellAt: indexPath)
andreas@3419
   244
                    
andreas@3419
   245
                    // ... and set it on the main queue
andreas@3419
   246
                    DispatchQueue.main.async {
andreas@3419
   247
                        if senderImage != nil && senderImage != cell.contactImageView.image {
andreas@3419
   248
                            cell.contactImageView.image  = senderImage
andreas@3419
   249
                        }
andreas@3220
   250
                    }
andreas@3220
   251
                }
andreas@3419
   252
                
andreas@3419
   253
                let pEpRatingImage = strongSelf.model?.pEpRatingColorImage(forCellAt: indexPath)
andreas@3419
   254
                
andreas@3419
   255
                // In theory we want to set all data in *one* async call. But as pEpRatingColorImage takes
andreas@3419
   256
                // very long, we are setting the sender image seperatelly.
andreas@3419
   257
                DispatchQueue.main.async {
andreas@3419
   258
                    if pEpRatingImage != nil {
andreas@3419
   259
                        cell.setPepRatingImage(image: pEpRatingImage)
andreas@3419
   260
                    }
andreas@3151
   261
                }
andreas@3151
   262
            }
andreas@3151
   263
        }
andreas@3151
   264
        queue(operation: op, for: indexPath)
xavier@2967
   265
    }
andreas@3505
   266
andreas@3505
   267
    private func showComposeView() {
andreas@3505
   268
        self.performSegue(withIdentifier: SegueIdentifier.segueEditDraft, sender: self)
andreas@3505
   269
    }
andreas@3505
   270
andreas@3505
   271
    private func showEmail(forCellAt indexPath: IndexPath) {
andreas@3505
   272
        performSegue(withIdentifier: SegueIdentifier.segueShowEmail, sender: self)
andreas@3505
   273
        guard let vm = model else {
andreas@3505
   274
            Log.shared.errorAndCrash(component: #function, errorString: "No model.")
andreas@3505
   275
            return
andreas@3505
   276
        }
andreas@3505
   277
        vm.markRead(forIndexPath: indexPath)
andreas@3505
   278
    }
andreas@3208
   279
    
andreas@3678
   280
    // MARK: - Action Filter Button
andreas@3208
   281
    
andreas@3151
   282
    @IBAction func filterButtonHasBeenPressed(_ sender: UIBarButtonItem) {
andreas@3678
   283
        guard !loadingBlocked else {
andreas@3678
   284
            return
andreas@3678
   285
        }
andreas@3163
   286
        guard let vm = model else {
andreas@3163
   287
            Log.shared.errorAndCrash(component: #function, errorString: "We should have a model here")
andreas@3163
   288
            return
andreas@3163
   289
        }
andreas@3678
   290
        stopLoading()
andreas@3163
   291
        vm.isFilterEnabled = !vm.isFilterEnabled
xavier@3193
   292
        updateFilterButtonView()
xavier@1832
   293
    }
andreas@3208
   294
    
andreas@3678
   295
    private func updateFilterButtonView() {
andreas@3163
   296
        guard let vm = model else {
andreas@3163
   297
            Log.shared.errorAndCrash(component: #function, errorString: "We should have a model here")
andreas@3163
   298
            return
andreas@3163
   299
        }
andreas@3208
   300
        
andreas@3163
   301
        textFilterButton.isEnabled = vm.isFilterEnabled
andreas@3163
   302
        if textFilterButton.isEnabled {
andreas@3163
   303
            enableFilterButton.image = UIImage(named: "unread-icon-active")
andreas@3163
   304
            updateFilterText()
andreas@3163
   305
        } else {
andreas@3163
   306
            textFilterButton.title = ""
andreas@3163
   307
            enableFilterButton.image = UIImage(named: "unread-icon")
andreas@3163
   308
        }
andreas@3163
   309
    }
andreas@3208
   310
    
andreas@3678
   311
    private func updateFilterText() {
xavier@3193
   312
        if let vm = model, let txt = vm.activeFilter?.title {
andreas@3163
   313
            textFilterButton.title = "Filter by: " + txt
andreas@3163
   314
        }
andreas@3151
   315
    }
andreas@3208
   316
    
dirk@31
   317
    // MARK: - UITableViewDataSource
andreas@3208
   318
    
dirk@784
   319
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
andreas@3151
   320
        return model?.rowCount ?? 0
dirk@31
   321
    }
andreas@3208
   322
    
dirk@784
   323
    override func tableView(_ tableView: UITableView,
dirk@784
   324
                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
andreas@3151
   325
        guard let cell = tableView.dequeueReusableCell(withIdentifier: EmailListViewCell.storyboardId,
andreas@3151
   326
                                                       for: indexPath) as? EmailListViewCell
andreas@3151
   327
            else {
andreas@3151
   328
                Log.shared.errorAndCrash(component: #function, errorString: "Wrong cell!")
andreas@3151
   329
                return UITableViewCell()
andreas@3151
   330
        }
xavier@3368
   331
        cell.delegate = self
andreas@3151
   332
        configure(cell: cell, for: indexPath)
dirk@31
   333
        return cell
dirk@31
   334
    }
andreas@3208
   335
    
andreas@3513
   336
    // MARK: - SwipeTableViewCellDelegate
xavier@3368
   337
andreas@3911
   338
    func tableView(_ tableView: UITableView,
andreas@3911
   339
                   editActionsForRowAt
andreas@3911
   340
        indexPath: IndexPath,
andreas@3911
   341
                   for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
andreas@3917
   342
andreas@3909
   343
        // Create swipe actions, taking the currently displayed folder into account
andreas@3514
   344
        var swipeActions = [SwipeAction]()
andreas@3911
   345
andreas@3917
   346
        // Get messages parent folder
andreas@3917
   347
        let parentFolder: Folder
andreas@3917
   348
        if let folder = folderToShow, !(folder is UnifiedInbox) {
andreas@3917
   349
            // Do not bother our imperformant MessageModel if we already know the parent folder
andreas@3917
   350
            parentFolder = folder
andreas@3917
   351
        } else {
andreas@3917
   352
            // folderToShow is unified inbox, fetch parent folder from DB.
andreas@3917
   353
            guard let vm = model,
andreas@3917
   354
                let folder = vm.message(representedByRowAt: indexPath)?.parent else {
andreas@3917
   355
                    Log.shared.errorAndCrash(component: #function, errorString: "Dangling Message")
andreas@3917
   356
                    return nil
andreas@3917
   357
            }
andreas@3917
   358
            parentFolder = folder
andreas@3917
   359
        }
andreas@3917
   360
andreas@3911
   361
        // Delete or Archive
andreas@3917
   362
        let defaultIsArchive = parentFolder.defaultDestructiveActionIsArchive
andreas@3917
   363
        let titleDestructive = defaultIsArchive ? "Archive" : "Delete"
andreas@3917
   364
        let descriptorDestructive: ActionDescriptor = defaultIsArchive ? .archive : .trash
andreas@3911
   365
        let archiveAction =
andreas@3911
   366
            SwipeAction(style: .destructive, title: titleDestructive) {action, indexPath in
andreas@3909
   367
                self.deleteAction(forCellAt: indexPath)
andreas@3514
   368
        }
andreas@3911
   369
        configure(action: archiveAction, with: descriptorDestructive)
andreas@3911
   370
        swipeActions.append(archiveAction)
andreas@3514
   371
andreas@3909
   372
        // Flag
andreas@3514
   373
        let flagAction = SwipeAction(style: .default, title: "Flag") { action, indexPath in
andreas@3514
   374
            self.flagAction(forCellAt: indexPath)
andreas@3514
   375
        }
andreas@3514
   376
        flagAction.hidesWhenSelected = true
andreas@3514
   377
        configure(action: flagAction, with: .flag)
andreas@3514
   378
        swipeActions.append(flagAction)
andreas@3514
   379
andreas@3909
   380
        // More (reply++)
andreas@3917
   381
        if parentFolder.folderType != .drafts {
andreas@3514
   382
            // Do not add "more" actions (reply...) to drafted mails.
andreas@3514
   383
            let moreAction = SwipeAction(style: .default, title: "More") { action, indexPath in
andreas@3514
   384
                self.moreAction(forCellAt: indexPath)
xavier@3368
   385
            }
andreas@3514
   386
            moreAction.hidesWhenSelected = true
andreas@3514
   387
            configure(action: moreAction, with: .more)
andreas@3514
   388
            swipeActions.append(moreAction)
xavier@3368
   389
        }
andreas@3514
   390
        return (orientation == .right ?   swipeActions : nil)
xavier@3368
   391
    }
xavier@3368
   392
xavier@3368
   393
    func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeTableOptions {
xavier@3368
   394
        var options = SwipeTableOptions()
xavier@3368
   395
        options.expansionStyle = .destructive
xavier@3368
   396
        options.transitionStyle = .border
xavier@3368
   397
        options.buttonSpacing = 11
xavier@3368
   398
        return options
xavier@3368
   399
    }
xavier@3368
   400
xavier@3368
   401
    func configure(action: SwipeAction, with descriptor: ActionDescriptor) {
xavier@3368
   402
        action.title = descriptor.title(forDisplayMode: buttonDisplayMode)
xavier@3368
   403
        action.image = descriptor.image(forStyle: buttonStyle, displayMode: buttonDisplayMode)
xavier@3368
   404
xavier@3368
   405
        switch buttonStyle {
xavier@3368
   406
        case .backgroundColor:
xavier@3368
   407
            action.backgroundColor = descriptor.color
xavier@3368
   408
        case .circular:
xavier@3368
   409
            action.backgroundColor = .clear
xavier@3368
   410
            action.textColor = descriptor.color
xavier@3368
   411
            action.font = .systemFont(ofSize: 13)
xavier@3368
   412
            action.transitionDelegate = ScaleTransition.default
xavier@3368
   413
        }
xavier@3368
   414
    }
andreas@3208
   415
    
andreas@3151
   416
    override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
andreas@3151
   417
        cancelOperation(for: indexPath)
dirk@744
   418
    }
andreas@3208
   419
    
andreas@3151
   420
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
andreas@3505
   421
        guard let folder = folderToShow else {
andreas@3505
   422
            Log.shared.errorAndCrash(component: #function, errorString: "No folder")
andreas@3505
   423
            return
andreas@3505
   424
        }
andreas@3151
   425
        lastSelectedIndexPath = indexPath
andreas@3151
   426
        tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
andreas@3505
   427
andreas@3505
   428
        if folder.folderType == .drafts {
andreas@3505
   429
            showComposeView()
andreas@3505
   430
        } else {
andreas@3505
   431
            showEmail(forCellAt: indexPath)
andreas@3425
   432
        }
dirk@744
   433
    }
andreas@3278
   434
andreas@3291
   435
    // Implemented to get informed about the scrolling position.
andreas@3291
   436
    // If the user has scrolled down (almost) to the end, we need to get older emails to display.
andreas@3291
   437
    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
andreas@3291
   438
                            forRowAt indexPath: IndexPath) {
andreas@3278
   439
        guard let vm = model else {
andreas@3291
   440
            Log.shared.errorAndCrash(component: #function, errorString: "No model.")
andreas@3291
   441
            return
andreas@3278
   442
        }
andreas@3291
   443
        vm.fetchOlderMessagesIfRequired(forIndexPath: indexPath)
andreas@3278
   444
    }
andreas@3278
   445
andreas@3151
   446
    // MARK: - Queue Handling
andreas@3678
   447
andreas@3678
   448
    /// Cancels all operations and sets tableView.dataSource to nil.
andreas@3678
   449
    /// Used to avoid that an operation accesses an outdated view model
andreas@3678
   450
    private func stopLoading() {
andreas@3678
   451
        loadingBlocked = true
andreas@3678
   452
        tableView.dataSource = nil
andreas@3678
   453
        queue.cancelAllOperations()
andreas@3678
   454
        queue.waitUntilAllOperationsAreFinished()
andreas@3678
   455
    }
andreas@3208
   456
    
andreas@3151
   457
    private func queue(operation op:Operation, for indexPath: IndexPath) {
andreas@3151
   458
        operations[indexPath] = op
andreas@3151
   459
        queue.addOperation(op)
dirk@1507
   460
    }
andreas@3208
   461
    
andreas@3151
   462
    private func cancelOperation(for indexPath:IndexPath) {
andreas@3151
   463
        guard let op = operations.removeValue(forKey: indexPath) else {
andreas@3151
   464
            return
dirk@744
   465
        }
andreas@3151
   466
        if !op.isCancelled  {
andreas@3151
   467
            op.cancel()
dirk@1507
   468
        }
dirk@744
   469
    }
andreas@3291
   470
andreas@3291
   471
    // MARK: -
andreas@3291
   472
andreas@3151
   473
    override func didReceiveMemoryWarning() {
andreas@3151
   474
        model?.freeMemory()
andreas@3151
   475
    }
andreas@3151
   476
}
andreas@3151
   477
andreas@3151
   478
// MARK: - UISearchResultsUpdating, UISearchControllerDelegate
andreas@3151
   479
andreas@3151
   480
extension EmailListViewController: UISearchResultsUpdating, UISearchControllerDelegate {
andreas@3151
   481
    public func updateSearchResults(for searchController: UISearchController) {
andreas@3151
   482
        guard let vm = model, let searchText = searchController.searchBar.text else {
andreas@3151
   483
            return
igor@1242
   484
        }
andreas@3163
   485
        vm.setSearchFilter(forSearchText: searchText)
igor@1242
   486
    }
andreas@3208
   487
    
andreas@3151
   488
    func didDismissSearchController(_ searchController: UISearchController) {
andreas@3151
   489
        guard let vm = model else {
andreas@3151
   490
            return
andreas@3151
   491
        }
andreas@3151
   492
        vm.removeSearchFilter()
andreas@3151
   493
    }
andreas@3151
   494
}
xavier@1623
   495
andreas@3151
   496
// MARK: - EmailListModelDelegate
andreas@3151
   497
andreas@3151
   498
extension EmailListViewController: EmailListViewModelDelegate {
andreas@3151
   499
    func emailListViewModel(viewModel: EmailListViewModel, didInsertDataAt indexPath: IndexPath) {
dirk@3866
   500
        Log.shared.info(component: #function, content: "\(model?.rowCount ?? 0)")
andreas@3166
   501
        tableView.beginUpdates()
andreas@3151
   502
        tableView.insertRows(at: [indexPath], with: .automatic)
andreas@3151
   503
        tableView.endUpdates()
andreas@3151
   504
    }
andreas@3208
   505
    
andreas@3151
   506
    func emailListViewModel(viewModel: EmailListViewModel, didRemoveDataAt indexPath: IndexPath) {
dirk@3866
   507
        Log.shared.info(component: #function, content: "\(model?.rowCount ?? 0)")
andreas@3151
   508
        tableView.beginUpdates()
andreas@3151
   509
        tableView.deleteRows(at: [indexPath], with: .automatic)
andreas@3151
   510
        tableView.endUpdates()
andreas@3151
   511
    }
andreas@3208
   512
    
andreas@3151
   513
    func emailListViewModel(viewModel: EmailListViewModel, didUpdateDataAt indexPath: IndexPath) {
dirk@3866
   514
        Log.shared.info(component: #function, content: "\(model?.rowCount ?? 0)")
andreas@3151
   515
        tableView.beginUpdates()
andreas@3151
   516
        tableView.reloadRows(at: [indexPath], with: .none)
andreas@3151
   517
        tableView.endUpdates()
andreas@3151
   518
    }
andreas@3677
   519
    
andreas@3677
   520
    func updateView() {
andreas@3678
   521
        loadingBlocked = false
andreas@3678
   522
        tableView.dataSource = self
andreas@3678
   523
        tableView.reloadData()
andreas@3151
   524
    }
andreas@3151
   525
}
andreas@3151
   526
andreas@3151
   527
// MARK: - ActionSheet & ActionSheet Actions
andreas@3151
   528
andreas@3151
   529
extension EmailListViewController {
andreas@3166
   530
    func showMoreActionSheet(forRowAt indexPath: IndexPath) {
andreas@3151
   531
        lastSelectedIndexPath = indexPath
igor@1242
   532
        let alertControler = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
igor@1438
   533
        alertControler.view.tintColor = .pEpGreen
igor@1242
   534
        let cancelAction = createCancelAction()
andreas@3151
   535
        let replyAction = createReplyAction()
andreas@3151
   536
        let replyAllAction = createReplyAllAction()
andreas@3151
   537
        let forwardAction = createForwardAction()
igor@1242
   538
        alertControler.addAction(cancelAction)
igor@1242
   539
        alertControler.addAction(replyAction)
xavier@1620
   540
        alertControler.addAction(replyAllAction)
igor@1242
   541
        alertControler.addAction(forwardAction)
igor@1414
   542
        if let popoverPresentationController = alertControler.popoverPresentationController {
andreas@3151
   543
            popoverPresentationController.sourceView = tableView
igor@1414
   544
        }
igor@1242
   545
        present(alertControler, animated: true, completion: nil)
igor@1242
   546
    }
andreas@3208
   547
    
andreas@3151
   548
    // MARK: Action Sheet Actions
andreas@3208
   549
    
igor@1242
   550
    func createCancelAction() -> UIAlertAction {
andreas@3151
   551
        return  UIAlertAction(title: "Cancel", style: .cancel) { (action) in
andreas@3151
   552
            self.tableView.beginUpdates()
andreas@3151
   553
            self.tableView.setEditing(false, animated: true)
andreas@3151
   554
            self.tableView.endUpdates()
igor@1242
   555
        }
igor@1242
   556
    }
andreas@3208
   557
    
andreas@3151
   558
    func createReplyAction() ->  UIAlertAction {
andreas@3151
   559
        return UIAlertAction(title: "Reply", style: .default) { (action) in
andreas@3151
   560
            self.performSegue(withIdentifier: .segueReply, sender: self)
xavier@1620
   561
        }
xavier@1620
   562
    }
andreas@3208
   563
    
andreas@3151
   564
    func createReplyAllAction() ->  UIAlertAction {
andreas@3151
   565
        return UIAlertAction(title: "Reply All", style: .default) { (action) in
andreas@3151
   566
            self.performSegue(withIdentifier: .segueReplyAll, sender: self)
igor@1242
   567
        }
igor@1242
   568
    }
andreas@3208
   569
    
andreas@3151
   570
    func createForwardAction() -> UIAlertAction {
andreas@3151
   571
        return UIAlertAction(title: "Forward", style: .default) { (action) in
andreas@3151
   572
            self.performSegue(withIdentifier: .segueForward, sender: self)
xavier@2369
   573
        }
igor@1301
   574
    }
igor@1301
   575
}
igor@1338
   576
andreas@3151
   577
// MARK: - TableViewCell Actions
andreas@3151
   578
andreas@3151
   579
extension EmailListViewController {
andreas@3151
   580
    private func createRowAction(image: UIImage?,
xavier@3266
   581
                                 action: @escaping (UITableViewRowAction, IndexPath) -> Void
xavier@3266
   582
        ) -> UITableViewRowAction {
xavier@3266
   583
        let rowAction = UITableViewRowAction(style: .normal, title: nil, handler: action)
andreas@3151
   584
        if let theImage = image {
andreas@3151
   585
            let iconColor = UIColor(patternImage: theImage)
andreas@3151
   586
            rowAction.backgroundColor = iconColor
andreas@3151
   587
        }
andreas@3151
   588
        return rowAction
andreas@3151
   589
    }
andreas@3208
   590
    
xavier@3369
   591
    func flagAction(forCellAt indexPath: IndexPath) {
andreas@3151
   592
        guard let row = model?.row(for: indexPath) else {
andreas@3151
   593
            Log.shared.errorAndCrash(component: #function, errorString: "No data for indexPath!")
xavier@3369
   594
            return
andreas@3151
   595
        }
xavier@3369
   596
        if row.isFlagged {
xavier@3369
   597
            model?.unsetFlagged(forIndexPath: indexPath)
xavier@3369
   598
        } else {
xavier@3369
   599
            model?.setFlagged(forIndexPath: indexPath)
andreas@3151
   600
        }
andreas@3151
   601
    }
andreas@3208
   602
    
xavier@3369
   603
    func deleteAction(forCellAt indexPath: IndexPath) {
andreas@3911
   604
        model?.delete(forIndexPath: indexPath)
andreas@3151
   605
    }
andreas@3208
   606
    
xavier@3369
   607
    func moreAction(forCellAt indexPath: IndexPath) {
xavier@3369
   608
        self.showMoreActionSheet(forRowAt: indexPath)
andreas@3151
   609
    }
andreas@3151
   610
}
andreas@3151
   611
andreas@3151
   612
// MARK: - SegueHandlerType
igor@1338
   613
igor@1338
   614
extension EmailListViewController: SegueHandlerType {
andreas@3208
   615
    
igor@1338
   616
    enum SegueIdentifier: String {
igor@1338
   617
        case segueAddNewAccount
igor@1338
   618
        case segueShowEmail
igor@1338
   619
        case segueCompose
xavier@2715
   620
        case segueReply
xavier@1623
   621
        case segueReplyAll
xavier@1664
   622
        case segueForward
andreas@3505
   623
        case segueEditDraft
xavier@1865
   624
        case segueFilter
xavier@2248
   625
        case segueFolderViews
igor@1338
   626
        case noSegue
igor@1338
   627
    }
andreas@3208
   628
    
igor@1338
   629
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
andreas@3505
   630
        let segueId = segueIdentifier(for: segue)
andreas@3505
   631
        switch segueId {
andreas@3505
   632
        case .segueReply,
andreas@3505
   633
             .segueReplyAll,
andreas@3505
   634
             .segueForward,
andreas@3505
   635
             .segueCompose,
andreas@3505
   636
             .segueEditDraft:
andreas@3505
   637
            setupComposeViewController(for: segue)
andreas@3111
   638
        case .segueShowEmail:
andreas@2850
   639
            guard let vc = segue.destination as? EmailViewController,
andreas@3151
   640
                let indexPath = lastSelectedIndexPath,
andreas@3208
   641
                let message = model?.message(representedByRowAt: indexPath) else {
andreas@2850
   642
                    Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
andreas@2850
   643
                    return
igor@1338
   644
            }
andreas@2850
   645
            vc.appConfig = appConfig
andreas@3151
   646
            vc.message = message
andreas@3223
   647
            vc.folderShow = folderToShow
andreas@3166
   648
            vc.messageId = indexPath.row //that looks wrong
andreas@2850
   649
        case .segueFilter:
andreas@2850
   650
            guard let destiny = segue.destination as? FilterTableViewController  else {
andreas@2850
   651
                Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
andreas@2850
   652
                return
andreas@2850
   653
            }
andreas@2850
   654
            destiny.appConfig = appConfig
andreas@3163
   655
            destiny.filterDelegate = model
andreas@2850
   656
            destiny.inFolder = false
andreas@3223
   657
            destiny.filterEnabled = folderToShow?.filter
andreas@2850
   658
            destiny.hidesBottomBarWhenPushed = true
andreas@2850
   659
        case .segueAddNewAccount:
andreas@2850
   660
            guard let vc = segue.destination as? LoginTableViewController  else {
andreas@2850
   661
                Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
andreas@2850
   662
                return
andreas@2850
   663
            }
andreas@2850
   664
            vc.appConfig = appConfig
andreas@2850
   665
            vc.hidesBottomBarWhenPushed = true
xavier@1664
   666
            break
andreas@2850
   667
        case .segueFolderViews:
andreas@2850
   668
            guard let vC = segue.destination as? FolderTableViewController  else {
andreas@2850
   669
                Log.shared.errorAndCrash(component: #function, errorString: "Segue issue")
andreas@2850
   670
                return
xavier@1865
   671
            }
andreas@2850
   672
            vC.appConfig = appConfig
andreas@2850
   673
            vC.hidesBottomBarWhenPushed = true
xavier@1865
   674
            break
andreas@2850
   675
        default:
andreas@2850
   676
            Log.shared.errorAndCrash(component: #function, errorString: "Unhandled segue")
dirk@1694
   677
            break
igor@1338
   678
        }
andreas@2838
   679
    }
andreas@3208
   680
    
andreas@3166
   681
    @IBAction func segueUnwindAccountAdded(segue: UIStoryboardSegue) {
andreas@3151
   682
        // nothing to do.
andreas@3150
   683
    }
andreas@3505
   684
andreas@3505
   685
    private func setupComposeViewController(for segue: UIStoryboardSegue) {
andreas@3505
   686
        let segueId = segueIdentifier(for: segue)
andreas@3505
   687
        guard
andreas@3505
   688
            let nav = segue.destination as? UINavigationController,
andreas@3505
   689
            let composeVc = nav.topViewController as? ComposeTableViewController,
andreas@3505
   690
            let composeMode = composeMode(for: segueId) else {
andreas@3505
   691
                Log.shared.errorAndCrash(component: #function,
andreas@3505
   692
                                         errorString: "composeViewController setup issue")
andreas@3505
   693
                return
andreas@3505
   694
        }
andreas@3505
   695
        composeVc.appConfig = appConfig
andreas@3505
   696
        composeVc.composeMode = composeMode
andreas@3505
   697
        composeVc.origin = folderToShow?.account.user
andreas@3505
   698
        if composeMode != .normal {
andreas@3505
   699
            // This is not a simple compose (but reply, forward or such),
andreas@3505
   700
            // thus we have to pass the original meaasge.
andreas@3505
   701
            guard
andreas@3505
   702
                let indexPath = lastSelectedIndexPath,
andreas@3505
   703
                let message = model?.message(representedByRowAt: indexPath) else {
andreas@3505
   704
                    Log.shared.errorAndCrash(component: #function,
andreas@3505
   705
                                             errorString: "No original message")
andreas@3505
   706
                    return
andreas@3505
   707
            }
andreas@3505
   708
            composeVc.originalMessage = message
andreas@3505
   709
        }
andreas@3505
   710
    }
andreas@3505
   711
andreas@3505
   712
    private func composeMode(for segueId: SegueIdentifier) -> ComposeTableViewController.ComposeMode? {
andreas@3505
   713
        switch segueId {
andreas@3505
   714
        case .segueReply:
andreas@3505
   715
            return .replyFrom
andreas@3505
   716
        case .segueReplyAll:
andreas@3505
   717
            return .replyAll
andreas@3505
   718
        case .segueForward:
andreas@3505
   719
            return .forward
andreas@3505
   720
        case .segueCompose:
andreas@3505
   721
            return .normal
andreas@3505
   722
        case .segueEditDraft:
andreas@3505
   723
            return .draft
andreas@3505
   724
        default:
andreas@3505
   725
            return nil
andreas@3505
   726
        }
andreas@3505
   727
    }
dirk@1348
   728
}
xavier@3368
   729
xavier@3368
   730
//enums to simplify configurations
xavier@3368
   731
xavier@3368
   732
enum ActionDescriptor {
andreas@3909
   733
    case read, more, flag, trash, archive
xavier@3368
   734
xavier@3368
   735
    func title(forDisplayMode displayMode: ButtonDisplayMode) -> String? {
xavier@3368
   736
        guard displayMode != .imageOnly else { return nil }
xavier@3368
   737
xavier@3368
   738
        switch self {
andreas@3909
   739
        case .read: return NSLocalizedString("Read", comment: "read button in slidw left menu")
andreas@3909
   740
        case .more: return NSLocalizedString("More", comment: "more button in slidw left menu")
andreas@3909
   741
        case .flag: return NSLocalizedString("Flag", comment: "read button in slidw left menu")
andreas@3909
   742
        case .trash: return NSLocalizedString("Trash", comment: "Trash button in slidw left menu")
andreas@3909
   743
        case .archive: return NSLocalizedString("Archive", comment: "Archive button in slidw left menu")
xavier@3368
   744
        }
xavier@3368
   745
    }
xavier@3368
   746
xavier@3368
   747
    func image(forStyle style: ButtonStyle, displayMode: ButtonDisplayMode) -> UIImage? {
xavier@3368
   748
        guard displayMode != .titleOnly else { return nil }
xavier@3368
   749
xavier@3368
   750
        let name: String
xavier@3368
   751
        switch self {
xavier@3368
   752
        case .read: name = "read"
xavier@3368
   753
        case .more: name = "more"
xavier@3368
   754
        case .flag: name = "flag"
xavier@3368
   755
        case .trash: name = "trash"
andreas@3909
   756
        case .archive: name = "trash"
xavier@3368
   757
        }
xavier@3368
   758
xavier@3368
   759
        return UIImage(named: "swipe-" + name)
xavier@3368
   760
    }
xavier@3368
   761
xavier@3368
   762
    var color: UIColor {
xavier@3368
   763
        switch self {
xavier@3368
   764
        case .read: return #colorLiteral(red: 0.2980392157, green: 0.8509803922, blue: 0.3921568627, alpha: 1)
xavier@3368
   765
        case .more: return #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
xavier@3368
   766
        case .flag: return #colorLiteral(red: 1, green: 0.5803921569, blue: 0, alpha: 1)
xavier@3368
   767
        case .trash: return #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
andreas@3909
   768
        case .archive: return #colorLiteral(red: 0.2980392157, green: 0.8509803922, blue: 0.3921568627, alpha: 1)
xavier@3368
   769
        }
xavier@3368
   770
    }
xavier@3368
   771
}
xavier@3368
   772
xavier@3368
   773
enum ButtonDisplayMode {
xavier@3368
   774
    case titleAndImage, titleOnly, imageOnly
xavier@3368
   775
}
xavier@3368
   776
xavier@3368
   777
enum ButtonStyle {
xavier@3368
   778
    case backgroundColor, circular
xavier@3368
   779
}