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