pEpForiOS/UI/EmailDisplayList/EmailListViewModel.swift
author Xavier Algarra <xavier@pep-project.org>
Mon, 07 May 2018 11:11:24 +0200
branchIOS-1064
changeset 4550 c04fe6d5c76b
parent 4472 dc539372d7ef
permissions -rw-r--r--
IOS-1064 some changes
     1 //
     2 //  EmailListViewModel.swift
     3 //  pEpForiOS
     4 //
     5 //  Created by Xavier Algarra on 23/06/2017.
     6 //  Copyright © 2017 p≡p Security S.A. All rights reserved.
     7 //
     8 
     9 import Foundation
    10 import MessageModel
    11 
    12 protocol EmailListViewModelDelegate: TableViewUpdate {
    13     func emailListViewModel(viewModel: EmailListViewModel, didInsertDataAt indexPath: IndexPath)
    14     func emailListViewModel(viewModel: EmailListViewModel, didUpdateDataAt indexPath: IndexPath)
    15     func emailListViewModel(viewModel: EmailListViewModel, didRemoveDataAt indexPath: IndexPath)
    16 }
    17 
    18 // MARK: - FilterUpdateProtocol
    19 
    20 extension EmailListViewModel: FilterUpdateProtocol {
    21     public func addFilter(_ filter: CompositeFilter<FilterBase>) {
    22         setFilterViewFilter(filter: filter)
    23     }
    24 }
    25 
    26 // MARK: - EmailListViewModel
    27 
    28 class EmailListViewModel {
    29     let messageFolderDelegateHandlingQueue = DispatchQueue(label:
    30         "net.pep-security-EmailListViewModel-MessageFolderDelegateHandling")
    31     let contactImageTool = IdentityImageTool()
    32     let messageSyncService: MessageSyncServiceProtocol
    33     class Row {
    34         var senderContactImage: UIImage?
    35         var ratingImage: UIImage?
    36         var showAttchmentIcon: Bool = false
    37         let from: String
    38         let to: String
    39         let subject: String
    40         let bodyPeek: String
    41         var isFlagged: Bool = false
    42         var isSeen: Bool = false
    43         var dateText: String
    44         
    45         init(withPreviewMessage pvmsg: PreviewMessage, senderContactImage: UIImage? = nil) {
    46             self.senderContactImage = senderContactImage
    47             showAttchmentIcon = pvmsg.hasAttachments
    48             from = pvmsg.from.userNameOrAddress
    49             to = pvmsg.to
    50             subject = pvmsg.subject
    51             bodyPeek = pvmsg.bodyPeek
    52             isFlagged = pvmsg.isFlagged
    53             isSeen = pvmsg.isSeen
    54             dateText = pvmsg.dateSent.smartString()
    55         }
    56     }
    57     
    58     private var messages: SortedSet<PreviewMessage>?
    59     private let queue: OperationQueue = {
    60         let createe = OperationQueue()
    61         createe.qualityOfService = .userInteractive
    62         return createe
    63     }()
    64     public var delegate: EmailListViewModelDelegate?
    65     private var folderToShow: Folder?
    66     
    67     let sortByDateSentAscending: SortedSet<PreviewMessage>.SortBlock =
    68     { (pvMsg1: PreviewMessage, pvMsg2: PreviewMessage) -> ComparisonResult in
    69         if pvMsg1.dateSent > pvMsg2.dateSent {
    70             return .orderedAscending
    71         } else if pvMsg1.dateSent < pvMsg2.dateSent {
    72             return .orderedDescending
    73         } else if pvMsg1.uid > pvMsg2.uid {
    74             return .orderedAscending
    75         } else if pvMsg1.uid < pvMsg2.uid {
    76             return .orderedDescending
    77         } else {
    78             return .orderedSame
    79         }
    80     }
    81     
    82     // MARK: Life Cycle
    83     
    84     init(delegate: EmailListViewModelDelegate? = nil, messageSyncService: MessageSyncServiceProtocol,
    85          folderToShow: Folder? = nil) {
    86         self.messages = SortedSet(array: [], sortBlock: sortByDateSentAscending)
    87         self.delegate = delegate
    88         self.messageSyncService = messageSyncService
    89         self.folderToShow = folderToShow
    90         resetViewModel()
    91     }
    92 
    93     private func startListeningToChanges() {
    94         MessageModelConfig.messageFolderDelegate = self
    95     }
    96 
    97     private func stopListeningToChanges() {
    98         MessageModelConfig.messageFolderDelegate = nil
    99     }
   100     
   101     private func resetViewModel() {
   102         guard let folder = folderToShow else {
   103             Log.shared.errorAndCrash(component: #function, errorString: "No data, no cry.")
   104             return
   105         }
   106         // Ignore MessageModelConfig.messageFolderDelegate while reloading.
   107         self.stopListeningToChanges()
   108         queue.addOperation {
   109             let messagesToDisplay = folder.allMessages()
   110             let previewMessages = messagesToDisplay.map { PreviewMessage(withMessage: $0) }
   111 
   112             self.messages = SortedSet(array: previewMessages, sortBlock: self.sortByDateSentAscending)
   113             DispatchQueue.main.async {
   114                 self.delegate?.updateView()
   115                 self.startListeningToChanges()
   116             }
   117         }
   118     }
   119     
   120     // MARK: Internal
   121     
   122     private func indexOfPreviewMessage(forMessage msg:Message) -> Int? {
   123         guard let previewMessages = messages else {
   124             Log.shared.errorAndCrash(component: #function, errorString: "No data.")
   125             return nil
   126         }
   127         for i in 0..<previewMessages.count {
   128             guard let pvMsg = previewMessages.object(at: i) else {
   129                 Log.shared.errorAndCrash(component: #function, errorString: "Inconsistant data")
   130                 return nil
   131             }
   132             if pvMsg == msg {
   133                 return i
   134             }
   135         }
   136         return nil
   137     }
   138     
   139     // MARK: Public Data Access & Manipulation
   140     
   141     func row(for indexPath: IndexPath) -> Row? {
   142         guard let previewMessage = messages?.object(at: indexPath.row) else {
   143             Log.shared.errorAndCrash(component: #function,
   144                                      errorString: "InconsistencyviewModel vs. model")
   145             return nil
   146         }
   147         if let cachedSenderImage = contactImageTool.cachedIdentityImage(forIdentity: previewMessage.from) {
   148             return Row(withPreviewMessage: previewMessage, senderContactImage: cachedSenderImage)
   149         } else {
   150             return Row(withPreviewMessage: previewMessage)
   151         }
   152     }
   153     
   154     var rowCount: Int {
   155         return messages?.count ?? 0
   156     }
   157 
   158     func updateLastLookAt() {}
   159     
   160     /// Returns the senders contact image to display.
   161     /// This is a possibly time consuming process and shold not be called from the main thread.
   162     ///
   163     /// - Parameter indexPath: row indexpath to get the contact image for
   164     /// - Returns: contact image to display
   165     func senderImage(forCellAt indexPath:IndexPath) -> UIImage? {
   166         guard let previewMessage = messages?.object(at: indexPath.row) else {
   167             Log.shared.errorAndCrash(component: #function,
   168                                      errorString: "InconsistencyviewModel vs. model")
   169             return nil
   170         }
   171         return contactImageTool.identityImage(for: previewMessage.from)
   172     }
   173     
   174     private func cachedSenderImage(forCellAt indexPath:IndexPath) -> UIImage? {
   175         guard
   176             let msgs = messages,
   177             indexPath.row < msgs.count,
   178             let previewMessage = messages?.object(at: indexPath.row)
   179             else {
   180             // The model has been updated.
   181             return nil
   182         }
   183         return contactImageTool.cachedIdentityImage(forIdentity: previewMessage.from)
   184     }
   185     
   186     func pEpRatingColorImage(forCellAt indexPath: IndexPath) -> UIImage? {
   187         guard
   188             let msgs = messages,
   189             indexPath.row < msgs.count,
   190             let previewMessage = messages?.object(at: indexPath.row),
   191             let message = previewMessage.message()
   192             else {
   193                 // The model has been updated.
   194                 return nil
   195         }
   196         let color = PEPUtil.pEpColor(pEpRating: message.pEpRating())
   197         let result = color.statusIcon()
   198         return result
   199     }
   200     
   201     func setFlagged(forIndexPath indexPath: IndexPath) {
   202         setFlaggedValue(forIndexPath: indexPath, newValue: true)
   203     }
   204     
   205     func unsetFlagged(forIndexPath indexPath: IndexPath) {
   206         setFlaggedValue(forIndexPath: indexPath, newValue: false)
   207     }
   208     
   209     func markRead(forIndexPath indexPath: IndexPath) {
   210         guard let previewMessage = messages?.object(at: indexPath.row) else {
   211             return
   212         }
   213         DispatchQueue.main.async { [weak self] in
   214             guard let me = self else {
   215                 Log.shared.errorAndCrash(component: #function, errorString: "Lost myself")
   216                 return
   217             }
   218             previewMessage.isSeen = true
   219             me.delegate?.emailListViewModel(viewModel: me, didUpdateDataAt: indexPath)
   220         }
   221     }
   222     
   223     func delete(forIndexPath indexPath: IndexPath) {
   224         guard let previewMessage = messages?.object(at: indexPath.row),
   225             let message = previewMessage.message() else {
   226                 return
   227         }
   228         messages?.remove(object: previewMessage)
   229         message.imapDelete()
   230     }
   231     
   232     func message(representedByRowAt indexPath: IndexPath) -> Message? {
   233         return messages?.object(at: indexPath.row)?.message()
   234     }
   235     
   236     func freeMemory() {
   237         contactImageTool.clearCache()
   238     }
   239     
   240     private func setFlaggedValue(forIndexPath indexPath: IndexPath, newValue flagged: Bool) {
   241         guard let previewMessage = messages?.object(at: indexPath.row),
   242             let message = previewMessage.message() else {
   243                 return
   244         }
   245         message.imapFlags?.flagged = flagged
   246         didUpdate(messageFolder: message)
   247         DispatchQueue.main.async {
   248             message.save()
   249         }
   250     }
   251 
   252     public func reloadData() {
   253         resetViewModel()
   254     }
   255 
   256     // MARK: Filter
   257     
   258     public var isFilterEnabled = false {
   259         didSet {
   260             handleFilterEnabledSwitch()
   261         }
   262     }
   263     public var activeFilter : CompositeFilter<FilterBase>? {
   264         get {
   265             guard let folder = folderToShow else {
   266                 return nil
   267             }
   268             return folder.filter
   269         }
   270     }
   271 
   272     static let defaultFilterViewFilter = CompositeFilter<FilterBase>.defaultFilter()
   273     private var _filterViewFilter: CompositeFilter = defaultFilterViewFilter
   274     private var filterViewFilter: CompositeFilter<FilterBase> {
   275         get {
   276             if _filterViewFilter.isEmpty() {
   277                 _filterViewFilter = EmailListViewModel.defaultFilterViewFilter
   278             }
   279             return _filterViewFilter
   280         }
   281         set {
   282             _filterViewFilter = newValue
   283         }
   284     }
   285 
   286     private func setFilterViewFilter(filter: CompositeFilter<FilterBase>) {
   287         if isFilterEnabled {
   288             let folderFilter = assuredFilterOfFolderToShow()
   289             folderFilter.without(filters: filterViewFilter)
   290             folderFilter.with(filters: filter)
   291             resetViewModel()
   292         }
   293         filterViewFilter = filter
   294     }
   295 
   296     private func handleFilterEnabledSwitch() {
   297         let folderFilter = assuredFilterOfFolderToShow()
   298         if isFilterEnabled {
   299             folderFilter.with(filters: filterViewFilter)
   300         } else {
   301             folderFilter.without(filters: filterViewFilter)
   302         }
   303         resetViewModel()
   304     }
   305     
   306     public func setSearchFilter(forSearchText txt: String = "") {
   307         if txt == "" {
   308             assuredFilterOfFolderToShow().removeSearchFilter()
   309         } else {
   310             let folderFilter = assuredFilterOfFolderToShow()
   311             folderFilter.removeSearchFilter()
   312             let searchFilter = SearchFilter(searchTerm: txt)
   313             folderFilter.add(filter: searchFilter)
   314         }
   315         resetViewModel()
   316     }
   317     
   318     public func removeSearchFilter() {
   319         guard let filter = folderToShow?.filter else {
   320             Log.shared.errorAndCrash(component: #function, errorString: "No folder.")
   321             return
   322         }
   323         filter.removeSearchFilter()
   324         resetViewModel()
   325     }
   326 
   327     private func assuredFilterOfFolderToShow() -> CompositeFilter<FilterBase> {
   328         guard let folder = folderToShow else {
   329             Log.shared.errorAndCrash(component: #function, errorString: "No folder.")
   330             return CompositeFilter<FilterBase>.defaultFilter()
   331         }
   332         if folder.filter == nil{
   333             folder.resetFilter()
   334         }
   335 
   336         guard let folderFilter = folder.filter else {
   337             Log.shared.errorAndCrash(component: #function,
   338                                      errorString: "We just set the filter but do not have one?")
   339             return CompositeFilter<FilterBase>.defaultFilter()
   340         }
   341         return folderFilter
   342     }
   343 
   344     // MARK: - Fetch Older Messages
   345 
   346     /// The number of rows (not yet displayed to the user) before we want to fetch older messages.
   347     /// A balance between good user experience (have data in time, ideally before the user has scrolled
   348     /// to the last row) and memory usage has to be found.
   349     private let numRowsBeforeLastToTriggerFetchOder = 1
   350 
   351     /// Figures out whether or not fetching of older messages should be requested.
   352     /// Takes numRowsBeforeLastToTriggerFetchOder into account,
   353     ///
   354     /// - Parameter row: number of displayed tableView row to base computation on
   355     /// - Returns: true if fetch older messages should be requested, false otherwize
   356     private func triggerFetchOlder(lastDisplayedRow row: Int) -> Bool {
   357         return row >= rowCount - numRowsBeforeLastToTriggerFetchOder
   358     }
   359 
   360     // Implemented to get informed about the currently visible cells.
   361     // If the user has scrolled down (almost) to the end, we ask for older emails.
   362 
   363     /// Get informed about the new visible cells.
   364     /// If the user has scrolled down (almost) to the end, we ask for older emails.
   365     ///
   366     /// - Parameter indexPath: indexpath to check need for fetch older for
   367     public func fetchOlderMessagesIfRequired(forIndexPath indexPath: IndexPath) {
   368         guard let folder = folderToShow else {
   369             return
   370         }
   371         if !triggerFetchOlder(lastDisplayedRow: indexPath.row) {
   372             return
   373         }
   374         if folder is UnifiedInbox {
   375             guard let unified = folder as? UnifiedInbox else {
   376                 Log.shared.errorAndCrash(component: #function, errorString: "Error casting")
   377                 return
   378             }
   379             requestFetchOlder(forFolders: unified.folders)
   380         } else {
   381             requestFetchOlder(forFolders: [folder])
   382         }
   383     }
   384 
   385     private func requestFetchOlder(forFolders folders: [Folder]) {
   386         DispatchQueue.main.async { [weak self] in
   387             for folder in folders {
   388                 self?.messageSyncService.requestFetchOlderMessages(inFolder: folder)
   389             }
   390         }
   391     }
   392 }
   393 
   394 // MARK: - MessageFolderDelegate
   395 
   396 extension EmailListViewModel: MessageFolderDelegate {
   397 
   398     func didCreate(messageFolder: MessageFolder) {
   399         messageFolderDelegateHandlingQueue.async {
   400             self.didCreateInternal(messageFolder: messageFolder)
   401         }
   402     }
   403     
   404     func didUpdate(messageFolder: MessageFolder) {
   405         messageFolderDelegateHandlingQueue.async {
   406             self.didUpdateInternal(messageFolder: messageFolder)
   407         }
   408     }
   409     
   410     func didDelete(messageFolder: MessageFolder) {
   411         messageFolderDelegateHandlingQueue.async {
   412             self.didDeleteInternal(messageFolder: messageFolder)
   413         }
   414     }
   415     
   416     private func didCreateInternal(messageFolder: MessageFolder) {
   417         guard let message = messageFolder as? Message else {
   418             // The createe is no message. Ignore.
   419             return
   420         }
   421         if !shouldBeDisplayed(message: message){
   422             return
   423         }
   424         // Is a Message (not a Folder)
   425         if let filter = folderToShow?.filter,
   426             !filter.fulfillsFilter(message: message) {
   427             // The message does not fit in current filter criteria. Ignore- and do not show it.
   428             return
   429         }
   430         let previewMessage = PreviewMessage(withMessage: message)
   431 
   432         DispatchQueue.main.async { [weak self] in
   433             if let theSelf = self {
   434                 guard let index = theSelf.messages?.insert(object: previewMessage) else {
   435                     Log.shared.errorAndCrash(component: #function,
   436                                              errorString: "We should be able to insert.")
   437                     return
   438                 }
   439                 let indexPath = IndexPath(row: index, section: 0)
   440                 theSelf.delegate?.emailListViewModel(viewModel: theSelf, didInsertDataAt: indexPath)
   441             }
   442         }
   443     }
   444     
   445     private func didDeleteInternal(messageFolder: MessageFolder) {
   446         // Make sure it is a Message (not a Folder). Flag must have changed
   447         guard let message = messageFolder as? Message else {
   448             // It is not a Message (probably it is a Folder).
   449             return
   450         }
   451         if !shouldBeDisplayed(message: message){
   452             return
   453         }
   454         // Is a Message (not a Folder)
   455         guard let indexExisting = indexOfPreviewMessage(forMessage: message) else {
   456             // We do not have this message in our model, so we do not have to remove it
   457             return
   458         }
   459         guard let pvMsgs = messages else {
   460             Log.shared.errorAndCrash(component: #function, errorString: "Missing data")
   461             return
   462         }
   463         DispatchQueue.main.async { [weak self] in
   464             if let me = self {
   465                 pvMsgs.removeObject(at: indexExisting)
   466                 let indexPath = IndexPath(row: indexExisting, section: 0)
   467                 me.delegate?.emailListViewModel(viewModel: me, didRemoveDataAt: indexPath)
   468             }
   469         }
   470     }
   471     
   472     private func didUpdateInternal(messageFolder: MessageFolder) {
   473         // Make sure it is a Message (not a Folder). Flag must have changed
   474         guard let message = messageFolder as? Message else {
   475             // It is not a Message (probably it is a Folder).
   476             return
   477         }
   478         if !shouldBeDisplayed(message: message){
   479             return
   480         }
   481         guard let pvMsgs = messages else {
   482             Log.shared.errorAndCrash(component: #function, errorString: "Missing data")
   483             return
   484         }
   485 
   486         if indexOfPreviewMessage(forMessage: message) == nil {
   487             // We do not have this updated message in our model yet. It might have been updated in
   488             // a way, that fulfills the current filters now but did not before the update.
   489             // Or it has just been decrypted.
   490             // Forward to didCreateInternal to figure out if we want to display it.
   491             self.didCreateInternal(messageFolder: messageFolder)
   492             return
   493         }
   494 
   495         // We do have this message in our model, so we do have to update it
   496         guard let indexExisting = indexOfPreviewMessage(forMessage: message),
   497             let existingMessage = pvMsgs.object(at: indexExisting) else {
   498                 Log.shared.errorAndCrash(component: #function,
   499                                          errorString: "We should have the message at this point")
   500                 return
   501         }
   502 
   503         let previewMessage = PreviewMessage(withMessage: message)
   504         if !previewMessage.flagsDiffer(previewMessage: existingMessage) {
   505             // The only message properties displayed in this view that might be updated are flagged and seen.
   506             // We got called even the flaggs did not change. Ignore.
   507             return
   508         }
   509         
   510         let indexToRemove = pvMsgs.index(of: existingMessage)
   511         DispatchQueue.main.async { [weak self] in
   512             if let me = self {
   513                 pvMsgs.removeObject(at: indexToRemove)
   514 
   515                 if let filter = me.folderToShow?.filter,
   516                     !filter.fulfillsFilter(message: message) {
   517                     // The message was included in the model, but does not fulfil the filter criteria
   518                     // anymore after it has been updated.
   519                     // Remove it.
   520                     let indexPath = IndexPath(row: indexToRemove, section: 0)
   521                     me.delegate?.emailListViewModel(viewModel: me, didRemoveDataAt: indexPath)
   522                     return
   523                 }
   524                 // The updated message has to be shown. Add it to the model ...
   525                 let indexInserted = pvMsgs.insert(object: previewMessage)
   526                 if indexToRemove != indexInserted  {Log.shared.warn(component: #function,
   527                                                                     content:
   528                     """
   529 When updating a message, the the new index of the message must be the same as the old index.
   530 Something is fishy here.
   531 """
   532                     )
   533                 }
   534                 // ...  and inform the delegate.
   535                 let indexPath = IndexPath(row: indexInserted, section: 0)
   536                 me.delegate?.emailListViewModel(viewModel: me, didUpdateDataAt: indexPath)
   537             }
   538         }
   539     }
   540 
   541     private func shouldBeDisplayed(message: Message) -> Bool {
   542         if !isInFolderToShow(message: message) {
   543             return false
   544         }
   545         if message.isEncrypted {
   546             return false
   547         }
   548         return true
   549     }
   550 
   551     private func isInFolderToShow(message: Message) -> Bool {
   552         if folderToShow is UnifiedInbox {
   553             if message.parent.folderType == .inbox {
   554                 return true
   555             }
   556         } else {
   557             return message.parent == folderToShow
   558         }
   559         return false
   560     }
   561 }