From ac812d8954bda0941b978bd9564fa193df24c047 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 30 Mar 2023 14:19:05 +0200 Subject: [PATCH 1/6] refactor(ThreadListViewModel): Guard folder --- Mail/Views/Thread List/ThreadListViewModel.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index cd9569226..6be3b8386 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -205,7 +205,11 @@ class DateSection: Identifiable { func observeChanges(animateInitialThreadChanges: Bool = false) { observationThreadToken?.invalidate() observationLastUpdateToken?.invalidate() - if let folder = folder.thaw() { + guard let folder = folder.thaw() else { + sections = [] + return + } + let threadResults = folder.threads.sorted(by: \.date, ascending: false) observationThreadToken = threadResults.observe(on: .main) { [weak self] changes in switch changes { @@ -234,9 +238,6 @@ class DateSection: Identifiable { break } } - } else { - sections = [] - } } func sortThreadsIntoSections(threads: [Thread]) { From 9aba7de5f71397b8ecf5e54fb5cc458e756e52f4 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 30 Mar 2023 14:49:41 +0200 Subject: [PATCH 2/6] feat(ThreadListViewModel): Filter using realm --- .../Thread List/ThreadListViewModel.swift | 64 +++++++++++-------- MailCore/Models/Thread.swift | 17 ++++- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index 6be3b8386..4d577ee3a 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -210,45 +210,55 @@ class DateSection: Identifiable { return } - let threadResults = folder.threads.sorted(by: \.date, ascending: false) - observationThreadToken = threadResults.observe(on: .main) { [weak self] changes in - switch changes { - case .initial(let results): - withAnimation(animateInitialThreadChanges ? .default : nil) { - self?.sortThreadsIntoSections(threads: Array(results.freezeIfNeeded())) - } - case .update(let results, _, _, _): - if self?.filter != .all && results.count == 1 && self?.filter.accepts(thread: results[0]) != true { - self?.filter = .all - } - withAnimation { - self?.sortThreadsIntoSections(threads: Array(results.freezeIfNeeded())) - } - case .error: - break + let threadResults: Results + 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) + } + + observationThreadToken = threadResults.observe(on: .main) { [weak self] changes in + switch changes { + case .initial(let results): + let filteredThreads = Array(results.freezeIfNeeded()) + self?.filteredThreads = filteredThreads + withAnimation(animateInitialThreadChanges ? .default : nil) { + self?.sortThreadsIntoSections(threads: filteredThreads) } + case .update(let results, _, _, _): + let filteredThreads = Array(results.freezeIfNeeded()) + self?.filteredThreads = filteredThreads + if self?.filter != .all && results.count == 1 && self?.filter.accepts(thread: results[0]) != true { + self?.filter = .all + } + withAnimation { + self?.sortThreadsIntoSections(threads: filteredThreads) + } + case .error: + break } - 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 + } + 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 sortThreadsIntoSections(threads: [Thread]) { var newSections = [DateSection]() var currentSection: DateSection? - filteredThreads = threads.filter { $0.id == selectedThread?.id || filter.accepts(thread: $0) } - if filteredThreads.isEmpty && filterUnreadOn { + if threads.isEmpty && filterUnreadOn { filterUnreadOn.toggle() } else { - for thread in filteredThreads { + for thread in threads { if currentSection?.threadBelongsToSection(thread: thread) != true { currentSection = DateSection(thread: thread) newSections.append(currentSection!) diff --git a/MailCore/Models/Thread.swift b/MailCore/Models/Thread.swift index 77e33a6f6..f05c69875 100644 --- a/MailCore/Models/Thread.swift +++ b/MailCore/Models/Thread.swift @@ -89,7 +89,7 @@ public class Thread: Object, Decodable, Identifiable { return fromArray[0].formattedName default: let fromCount = min(fromArray.count, Constants.threadCellMaxRecipients) - return fromArray[0.. 0" + case .starred: + return "flagged == TRUE" + case .unstarred: + return "flagged == FALSE" + } + } + public func accepts(thread: Thread) -> Bool { switch self { case .all: From 7c331096ac525544e37561d16c8b4e5ff80c8df6 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 30 Mar 2023 15:54:08 +0200 Subject: [PATCH 3/6] perf(ThreadListViewModel): Sort in the background --- .../Thread List/ThreadListViewModel.swift | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index 4d577ee3a..dbb0039cb 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -123,6 +123,7 @@ class DateSection: Identifiable { private var observationThreadToken: NotificationToken? private var observationLastUpdateToken: NotificationToken? + private let observeQueue = DispatchQueue(label: "com.infomaniak.thread-results", qos: .userInteractive) @Published var filter = Filter.all { didSet { @@ -218,22 +219,30 @@ class DateSection: Identifiable { threadResults = folder.threads.sorted(by: \.date, ascending: false) } - observationThreadToken = threadResults.observe(on: .main) { [weak self] changes in + observationThreadToken = threadResults.observe(on: observeQueue) { [weak self] changes in switch changes { case .initial(let results): let filteredThreads = Array(results.freezeIfNeeded()) - self?.filteredThreads = filteredThreads - withAnimation(animateInitialThreadChanges ? .default : nil) { - self?.sortThreadsIntoSections(threads: filteredThreads) + 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()) - self?.filteredThreads = filteredThreads - if self?.filter != .all && results.count == 1 && self?.filter.accepts(thread: results[0]) != true { - self?.filter = .all - } - withAnimation { - self?.sortThreadsIntoSections(threads: filteredThreads) + guard let newSections = self?.sortThreadsIntoSections(threads: filteredThreads) else { return } + + DispatchQueue.main.sync { + self?.filteredThreads = filteredThreads + if self?.filter != .all && results.count == 1 && self?.filter.accepts(thread: results[0]) != true { + self?.filter = .all + } + withAnimation { + self?.sections = newSections + } } case .error: break @@ -251,12 +260,15 @@ class DateSection: Identifiable { } } - func sortThreadsIntoSections(threads: [Thread]) { + private func sortThreadsIntoSections(threads: [Thread]) -> [DateSection]? { var newSections = [DateSection]() var currentSection: DateSection? if threads.isEmpty && filterUnreadOn { - filterUnreadOn.toggle() + DispatchQueue.main.sync { + filterUnreadOn.toggle() + } + return nil } else { for thread in threads { if currentSection?.threadBelongsToSection(thread: thread) != true { @@ -266,7 +278,7 @@ class DateSection: Identifiable { currentSection?.threads.append(thread) } - sections = newSections + return newSections } } From 13e376dc42e2314410bb07c525f2cb386b818e04 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 30 Mar 2023 15:54:31 +0200 Subject: [PATCH 4/6] perf: Index thread date --- MailCore/Models/Thread.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MailCore/Models/Thread.swift b/MailCore/Models/Thread.swift index f05c69875..df6436081 100644 --- a/MailCore/Models/Thread.swift +++ b/MailCore/Models/Thread.swift @@ -42,7 +42,7 @@ public class Thread: Object, Decodable, Identifiable { @Persisted public var cc: List @Persisted public var bcc: List @Persisted public var subject: String? - @Persisted public var date: Date + @Persisted(indexed: true) public var date: Date @Persisted public var hasAttachments: Bool @Persisted public var hasSwissTransferAttachments: Bool @Persisted public var hasDrafts: Bool From 21bd72aa4b7aec23db39ed59062b2a6e1178be64 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 30 Mar 2023 16:10:26 +0200 Subject: [PATCH 5/6] refactor: Stop using computed properties for DateSection --- .../Thread List/ThreadListViewModel.swift | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index dbb0039cb..5bd83b00b 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -45,31 +45,31 @@ class DateSection: Identifiable { return .init(start: date.startOfMonth, end: date.endOfMonth) } } - } - var id: DateInterval { referenceDate.dateInterval } - - var title: String { - switch referenceDate { - case .today: - return MailResourcesStrings.Localizable.threadListSectionToday - case .yesterday: - return MailResourcesStrings.Localizable.messageDetailsYesterday - case .thisWeek: - return MailResourcesStrings.Localizable.threadListSectionThisWeek - case .lastWeek: - return MailResourcesStrings.Localizable.threadListSectionLastWeek - case .thisMonth: - return MailResourcesStrings.Localizable.threadListSectionThisMonth - case .older(let date): - var formatStyle = Date.FormatStyle.dateTime.month(.wide) - if !Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) { - formatStyle = formatStyle.year() + public var title: String { + switch self { + case .today: + return MailResourcesStrings.Localizable.threadListSectionToday + case .yesterday: + return MailResourcesStrings.Localizable.messageDetailsYesterday + case .thisWeek: + return MailResourcesStrings.Localizable.threadListSectionThisWeek + case .lastWeek: + return MailResourcesStrings.Localizable.threadListSectionLastWeek + case .thisMonth: + return MailResourcesStrings.Localizable.threadListSectionThisMonth + case .older(let date): + var formatStyle = Date.FormatStyle.dateTime.month(.wide) + if !Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) { + formatStyle = formatStyle.year() + } + return date.formatted(formatStyle).capitalized } - return date.formatted(formatStyle).capitalized } } + let id: DateInterval + let title: String var threads = [Thread]() private let referenceDate: ReferenceDate @@ -77,6 +77,8 @@ class DateSection: Identifiable { init(thread: Thread) { let sections: [ReferenceDate] = [.today, .yesterday, .thisWeek, .lastWeek, .thisMonth] referenceDate = sections.first { $0.dateInterval.contains(thread.date) } ?? .older(thread.date) + id = referenceDate.dateInterval + title = referenceDate.title } func threadBelongsToSection(thread: Thread) -> Bool { From 735be3b9ead905a0950f99df7d8bb76929620393 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Mon, 3 Apr 2023 08:48:14 +0200 Subject: [PATCH 6/6] fix(ThreadListViewModel): Correct thread access --- Mail/Views/Thread List/ThreadListViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index 5bd83b00b..54961c349 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -239,7 +239,8 @@ class DateSection: Identifiable { DispatchQueue.main.sync { self?.filteredThreads = filteredThreads - if self?.filter != .all && results.count == 1 && self?.filter.accepts(thread: results[0]) != true { + if self?.filter != .all && filteredThreads.count == 1 + && self?.filter.accepts(thread: filteredThreads[0]) != true { self?.filter = .all } withAnimation {