Skip to content

Commit

Permalink
Merge pull request #784 from Infomaniak/unreadStaticList
Browse files Browse the repository at this point in the history
The mail does not disappear on reading an unread message with a filter activated
  • Loading branch information
PhilippeWeidmann committed Jun 9, 2023
2 parents 08887f7 + d38b883 commit 03b9eb5
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 73 deletions.
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ opt_in_rules:
excluded:
- .tuist-bin/*
- Derived/Sources/*
- DerivedData/
- Derived/
4 changes: 2 additions & 2 deletions Mail/Views/Thread List/ThreadListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ struct ThreadListView: View {

@Binding private var editedMessageDraft: Draft?
@Binding private var messageReply: MessageReply?

private var shouldDisplayEmptyView: Bool {
viewModel.folder.lastUpdate != nil && viewModel.sections.isEmpty && !viewModel.isLoadingPage
}
Expand Down Expand Up @@ -144,7 +144,7 @@ struct ThreadListView: View {
ProgressView()
.id(UUID())
.frame(maxWidth: .infinity)
} else if displayLoadMoreButton {
} else if displayLoadMoreButton && !viewModel.filterUnreadOn {
MailButton(label: MailResourcesStrings.Localizable.buttonLoadMore) {
withAnimation {
isLoadingMore = true
Expand Down
208 changes: 208 additions & 0 deletions Mail/Views/Thread List/ThreadListViewModel+Observation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
Infomaniak Mail - iOS App
Copyright (C) 2022 Infomaniak Network SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import MailCore
import RealmSwift
import SwiftUI

extension ThreadListViewModel {
private func threadResults() -> Results<Thread>? {
guard let folder = folder.thaw() else {
sections = []
return nil
}

let threadResults: Results<Thread>
if let predicate = filter.predicate {
threadResults = folder.threads
.filter(predicate + " OR uid == %@", selectedThread?.uid ?? "")
.sorted(by: \.date, ascending: false)
} else {
threadResults = folder.threads.sorted(by: \.date, ascending: false)
}

return threadResults
}

// MARK: - Observe global changes

func observeChanges(animateInitialThreadChanges: Bool = false) {
stopObserveChanges()

observationThreadToken = threadResults()?.observe(on: observeQueue) { [weak self] changes in
guard let self = self else {
return
}

switch changes {
case .initial(let results):
let filteredThreads = Array(results.freezeIfNeeded())
guard let newSections = self.sortThreadsIntoSections(threads: filteredThreads) else { return }

DispatchQueue.main.sync {
self.filteredThreads = filteredThreads
withAnimation(animateInitialThreadChanges ? .default : nil) {
self.sections = newSections
}
}
case .update(let results, _, _, _):
let filteredThreads = Array(results.freezeIfNeeded())
guard let newSections = self.sortThreadsIntoSections(threads: filteredThreads) else { return }

DispatchQueue.main.sync {
self.nextThreadIfNeeded(from: filteredThreads)
self.filteredThreads = filteredThreads
if self.filter != .all,
filteredThreads.count == 1,
!self.filter.accepts(thread: filteredThreads[0]) {
self.filter = .all
}
withAnimation {
self.sections = newSections
}
}
case .error:
break
}

// We only apply the first update when in "unread" mode
if self.filter == .unseen {
self.stopObserveChanges()
}
}
observationLastUpdateToken = folder.observe(keyPaths: [\Folder.lastUpdate], on: .main) { [weak self] changes in
switch changes {
case .change(let folder, _):
withAnimation {
self?.lastUpdate = folder.lastUpdate
}
default:
break
}
}
}

func stopObserveChanges() {
observationThreadToken?.invalidate()
observationLastUpdateToken?.invalidate()
}

// MARK: - Observe filtered results

static let containAnyOfUIDs = "uid IN %@"

/// Observe filtered threads, when global observation is disabled.
func observeFilteredResults() {
stopObserveFilteredThreads()

let allThreadsUIDs = threadResults()?.reduce([String]()) { partialResult, thread in
partialResult + [thread.uid]
}

guard let allThreadsUIDs = allThreadsUIDs else {
return
}

let containAnyOf = NSPredicate(format: Self.containAnyOfUIDs, allThreadsUIDs)
let realm = mailboxManager.getRealm()
let allThreads = realm.objects(Thread.self).filter(containAnyOf)

observeFilteredThreadsToken = allThreads.observe(on: observeQueue) { [weak self] changes in
guard let self = self else {
return
}

switch changes {
case .initial:
break
case .update(let all, _, _, let modificationIndexes):
refreshInFilterMode(all: all, changes: modificationIndexes)
case .error:
break
}
}
}

func stopObserveFilteredThreads() {
observeFilteredThreadsToken?.invalidate()
}

/// Update filtered threads on observation change.
private func refreshInFilterMode(all: Results<Thread>, changes: [Int]) {
for index in changes {
let updatedThread = all[index]
let uid = updatedThread.uid

let threadToUpdate: Thread? = sections.reduce(nil as Thread?) { partialResult, section in
partialResult ?? section.threads.first { $0.uid == uid }
}

let sectionToUpdate = sections.first { section in
section.threads.contains { $0.uid == uid }
}

guard let threadToUpdate = threadToUpdate,
let sectionToUpdate = sectionToUpdate else {
return
}

let threadToUpdateIndex = sectionToUpdate.threads.firstIndex(of: threadToUpdate)
guard let threadToUpdateIndex = threadToUpdateIndex else {
return
}

sectionToUpdate.threads[threadToUpdateIndex] = updatedThread.freeze()

Task {
await MainActor.run {
objectWillChange.send()
}
}
}
}

// MARK: - Observe unread count

/// Observe the unread count to disable filtering when it reaches 0
func observeUnreadCount() {
stopObserveUnread()

observationUnreadToken = threadResults()?.observe(on: observeQueue) { [weak self] changes in
guard let self = self else {
return
}

switch changes {
case .initial(let all), .update(let all, _, _, _):
let unreadCount = all.where { $0.unseenMessages > 0 }.count
Task {
await MainActor.run {
self.unreadCount = unreadCount
}
}

case .error:
break
}
}
}

func stopObserveUnread() {
observationUnreadToken?.invalidate()
}
}
Loading

0 comments on commit 03b9eb5

Please sign in to comment.