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