Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Single message mode setting added #917

Merged
merged 13 commits into from
Aug 14, 2023
103 changes: 103 additions & 0 deletions Mail/Views/Settings/General/SettingsThreadModeView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
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 MailResources
import SwiftUI

struct ThreadModeSettingUpdate: Identifiable {
let id = UUID()
let newSetting: ThreadMode
}

struct SettingsThreadModeView: View {
@State private var selectedValue: ThreadMode
@State private var threadModeSettingUpdate: ThreadModeSettingUpdate?

init() {
_selectedValue = State(wrappedValue: UserDefaults.shared.threadMode)
}

var body: some View {
List {
Section {
ForEach(ThreadMode.allCases, id: \.rawValue) { value in
Button {
if value != selectedValue {
threadModeSettingUpdate = ThreadModeSettingUpdate(newSetting: value)
}
} label: {
VStack(spacing: 0) {
HStack(spacing: 16) {
value.image?
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.foregroundColor(MailResourcesAsset.textTertiaryColor)
Text(value.title)
.textStyle(.body)
Spacer()
if value == selectedValue {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
.padding(.vertical, 16)
.padding(.horizontal, 24)

if value != ThreadMode.allCases.last {
IKDivider()
.padding(.horizontal, 8)
}
}
}
}
.listRowSeparator(.hidden)
.listRowInsets(.init())
.background(MailResourcesAsset.backgroundColor.swiftUIColor)
} header: {
Text(MailResourcesStrings.Localizable.settingsSelectDisplayModeDescription)
.textStyle(.bodySmallSecondary)
}
}
.listStyle(.plain)
.background(MailResourcesAsset.backgroundColor.swiftUIColor)
.navigationBarTitle(MailResourcesStrings.Localizable.settingsThreadModeTitle, displayMode: .inline)
.customAlert(item: $threadModeSettingUpdate) { threadModeUpdate in
VStack(alignment: .leading, spacing: 24) {
Text(MailResourcesStrings.Localizable.settingsThreadModeWarningTitle(threadModeUpdate.newSetting.title))
.textStyle(.bodyMedium)
Text(MailResourcesStrings.Localizable.settingsThreadModeWarningDescription)
.textStyle(.bodySecondary)
ModalButtonsView(
primaryButtonTitle: MailResourcesStrings.Localizable.buttonConfirm,
secondaryButtonTitle: MailResourcesStrings.Localizable.buttonCancel
) {
selectedValue = threadModeUpdate.newSetting
UserDefaults.shared.threadMode = selectedValue
}
}
}
}
}

struct SettingsThreadModeView_Previews: PreviewProvider {
static var previews: some View {
SettingsThreadModeView()
}
}
12 changes: 12 additions & 0 deletions Mail/Views/Settings/General/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct SettingsView: View {
@AppStorage(UserDefaults.shared.key(.theme)) private var theme = DefaultPreferences.theme
@AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
@AppStorage(UserDefaults.shared.key(.externalContent)) private var externalContent = DefaultPreferences.externalContent
@AppStorage(UserDefaults.shared.key(.threadMode)) private var threadMode = DefaultPreferences.threadMode

var body: some View {
ScrollView {
Expand Down Expand Up @@ -120,6 +121,14 @@ struct SettingsView: View {
SettingsSwipeActionsView()
}

// Thread mode
SettingsSubMenuCell(
title: MailResourcesStrings.Localizable.settingsThreadModeTitle,
subtitle: threadMode.title
) {
SettingsThreadModeView()
}

// External content
SettingsSubMenuCell(
title: MailResourcesStrings.Localizable.settingsExternalContentTitle,
Expand All @@ -137,6 +146,9 @@ struct SettingsView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
}
.onChange(of: threadMode) { _ in
AccountManager.instance.updateConversationSettings()
}
.background(MailResourcesAsset.backgroundColor.swiftUIColor)
.navigationBarTitle(MailResourcesStrings.Localizable.settingsTitle, displayMode: .inline)
.backButtonDisplayMode(.minimal)
Expand Down
3 changes: 3 additions & 0 deletions Mail/Views/ThreadListManagerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import SwiftUI
struct ThreadListManagerView: View {
@Environment(\.isCompactWindow) private var isCompactWindow

@AppStorage(UserDefaults.shared.key(.threadMode)) private var threadMode = DefaultPreferences.threadMode

@EnvironmentObject private var splitViewManager: SplitViewManager
@EnvironmentObject private var mailboxManager: MailboxManager

Expand All @@ -34,6 +36,7 @@ struct ThreadListManagerView: View {
SearchView(mailboxManager: mailboxManager, folder: selectedFolder)
} else {
ThreadListView(mailboxManager: mailboxManager, folder: selectedFolder, isCompact: isCompactWindow)
.id(threadMode)
Ambrdctr marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions MailCore/Cache/AccountManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,14 @@ public final class AccountManager: RefreshTokenDelegate, ObservableObject {
bugTracker.stopActivatingOnScreenshot()
}
}

public func updateConversationSettings() {
for account in accounts {
for mailbox in MailboxInfosManager.instance.getMailboxes(for: account.userId) {
if let mailboxManager = getMailboxManager(for: mailbox) {
mailboxManager.cleanRealm()
}
}
}
}
}
71 changes: 48 additions & 23 deletions MailCore/Cache/MailboxManager/MailboxManager+Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ public extension MailboxManager {

await backgroundRealm.execute { [self] realm in
if let folder = folder.fresh(using: realm) {
createMultiMessagesThreads(messageByUids: messageByUidsResult, folder: folder, using: realm)
createThreads(messageByUids: messageByUidsResult, folder: folder, using: realm)
}
SentryDebug.sendMissingMessagesSentry(
sentUids: uniqueUids,
Expand All @@ -343,7 +343,7 @@ public extension MailboxManager {
}
}

private func createMultiMessagesThreads(messageByUids: MessageByUidsResult, folder: Folder, using realm: Realm) {
private func createThreads(messageByUids: MessageByUidsResult, folder: Folder, using realm: Realm) {
var threadsToUpdate = Set<Thread>()
try? realm.safeWrite {
for message in messageByUids.messages {
Expand All @@ -360,28 +360,14 @@ public extension MailboxManager {
}
message.inTrash = folder.role == .trash
message.computeReference()
let existingThreads = Array(realm.objects(Thread.self)
.where { $0.messageIds.containsAny(in: message.linkedUids) })

if let newThread = createNewThreadIfRequired(
for: message,
folder: folder,
existingThreads: existingThreads
) {
threadsToUpdate.insert(newThread)
}

var allExistingMessages = Set(existingThreads.flatMap(\.messages))
allExistingMessages.insert(message)

for thread in existingThreads {
for existingMessage in allExistingMessages {
if !thread.messages.map(\.uid).contains(existingMessage.uid) {
thread.addMessageIfNeeded(newMessage: message.fresh(using: realm) ?? message)
}
}

threadsToUpdate.insert(thread)
let isThreadMode = UserDefaults.shared.threadMode == .conversation
if isThreadMode {
let updatedThreads = createConversationThread(message: message, folder: folder, using: realm)
threadsToUpdate.formUnion(updatedThreads)
} else {
let createdThread = createSingleMessageThread(message: message, folder: folder)
threadsToUpdate.insert(createdThread)
}

if let message = realm.objects(Message.self).first(where: { $0.uid == message.uid }) {
Expand All @@ -392,6 +378,45 @@ public extension MailboxManager {
}
}

private func createConversationThread(
message: Message,
folder: Folder,
using realm: Realm
) -> Set<Thread> {
var threadsToUpdate = Set<Thread>()

let existingThreads = Array(realm.objects(Thread.self)
.where { $0.messageIds.containsAny(in: message.linkedUids) /* && $0.isConversationThread == true */ })

if let newThread = createNewThreadIfRequired(
for: message,
folder: folder,
existingThreads: existingThreads
) {
threadsToUpdate.insert(newThread)
}

var allExistingMessages = Set(existingThreads.flatMap(\.messages))
allExistingMessages.insert(message)

for thread in existingThreads {
for existingMessage in allExistingMessages {
if !thread.messages.map(\.uid).contains(existingMessage.uid) {
thread.addMessageIfNeeded(newMessage: message.fresh(using: realm) ?? message)
}
}

threadsToUpdate.insert(thread)
}
return threadsToUpdate
}

private func createSingleMessageThread(message: Message, folder: Folder) -> Thread {
let thread = message.toThread().detached()
folder.threads.insert(thread)
return thread
}

private func createNewThreadIfRequired(for message: Message, folder: Folder, existingThreads: [Thread]) -> Thread? {
guard !existingThreads.contains(where: { $0.folder == folder }) else { return nil }

Expand Down
21 changes: 21 additions & 0 deletions MailCore/Cache/MailboxManager/MailboxManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,27 @@ public final class MailboxManager: ObservableObject, MailboxManageable {
let realm = getRealm()
return realm.objects(Folder.self).contains { $0.unreadCount > 0 }
}

public func cleanRealm() {
Task {
await backgroundRealm.execute { realm in

let folders = realm.objects(Folder.self)
let threads = realm.objects(Thread.self)
let messages = realm.objects(Message.self)

try? realm.safeWrite {
realm.delete(threads)
realm.delete(messages)
for folder in folders {
folder.cursor = nil
folder.resetHistoryInfo()
folder.computeUnreadCount()
}
}
}
}
}
}

// MARK: - Equatable conformance
Expand Down
1 change: 1 addition & 0 deletions MailCore/Models/Settings/DefaultPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ public enum DefaultPreferences {
public static let forwardMode = ForwardMode.inline
public static let acknowledgement = false
public static let includeOriginalInReply = false
public static let threadMode = ThreadMode.conversation
}
44 changes: 44 additions & 0 deletions MailCore/Models/Settings/ThreadMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
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 Foundation
import MailResources
import SwiftUI

public enum ThreadMode: String, CaseIterable, SettingsOptionEnum {
case conversation
case message

public var title: String {
switch self {
case .conversation:
return MailResourcesStrings.Localizable.settingsOptionThreadModeConversation
case .message:
return MailResourcesStrings.Localizable.settingsOptionThreadModeMessage
}
}

public var image: Image? {
switch self {
case .conversation:
return MailResourcesAsset.conversationEmail.swiftUIImage
case .message:
return MailResourcesAsset.singleEmail.swiftUIImage
}
}
}
10 changes: 10 additions & 0 deletions MailCore/Utils/MailUserDefaults+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public extension UserDefaults.Keys {
static let forwardMode = UserDefaults.Keys(rawValue: "forwardMode")
static let acknowledgement = UserDefaults.Keys(rawValue: "acknowledgement")
static let includeOriginalInReply = UserDefaults.Keys(rawValue: "includeOriginalInReply")
static let threadMode = UserDefaults.Keys(rawValue: "threadMode")
}

public extension UserDefaults {
Expand Down Expand Up @@ -202,4 +203,13 @@ public extension UserDefaults {
set(newValue, forKey: key(.acknowledgement))
}
}

var threadMode: ThreadMode {
get {
return ThreadMode(rawValue: string(forKey: key(.threadMode)) ?? "") ?? DefaultPreferences.threadMode
}
set {
set(newValue.rawValue, forKey: key(.threadMode))
}
}
}
Loading
Loading