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