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