diff --git a/Mail/Views/Menu Drawer/Folders/FolderCell.swift b/Mail/Views/Menu Drawer/Folders/FolderCell.swift
index fe72c187e..6bb6719ec 100644
--- a/Mail/Views/Menu Drawer/Folders/FolderCell.swift
+++ b/Mail/Views/Menu Drawer/Folders/FolderCell.swift
@@ -131,8 +131,6 @@ struct FolderCellContent: View {
@LazyInjectService private var matomo: MatomoUtils
- @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
-
@Environment(\.folderCellType) var cellType
private let frozenFolder: Folder
@@ -166,7 +164,8 @@ struct FolderCellContent: View {
ChevronIcon(direction: frozenFolder.isExpanded ? .up : .down)
.padding(value: .medium)
}
- .accessibilityLabel(MailResourcesStrings.Localizable.contentDescriptionButtonExpandFolder(frozenFolder.name))
+ .accessibilityLabel(MailResourcesStrings.Localizable
+ .contentDescriptionButtonExpandFolder(frozenFolder.name))
.opacity(level == 0 && !frozenFolder.children.isEmpty ? 1 : 0)
}
@@ -186,7 +185,8 @@ struct FolderCellContent: View {
}
.padding(.leading, IKPadding.menuDrawerSubFolder * CGFloat(level))
.padding(canHaveChevron ? IKPadding.menuDrawerCellWithChevron : IKPadding.menuDrawerCell)
- .background(background)
+ .background(FolderCellBackground(isCurrentFolder: isCurrentFolder))
+ .dropThreadDestination(destinationFolder: frozenFolder)
}
@ViewBuilder
@@ -207,13 +207,6 @@ struct FolderCellContent: View {
}
}
- @ViewBuilder
- private var background: some View {
- if cellType == .menuDrawer {
- SelectionBackground(selectionType: isCurrentFolder ? .folder : .none, paddingLeading: 0, accentColor: accentColor)
- }
- }
-
private func collapseFolder() {
matomo.track(eventWithCategory: .menuDrawer, name: "collapseFolder", value: !frozenFolder.isExpanded)
@@ -224,6 +217,24 @@ struct FolderCellContent: View {
}
}
+struct FolderCellBackground: View {
+ @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
+ @Environment(\.isHovered) private var isHovered
+ @Environment(\.folderCellType) private var cellType
+
+ let isCurrentFolder: Bool
+
+ var body: some View {
+ if cellType == .menuDrawer {
+ SelectionBackground(
+ selectionType: (isCurrentFolder || isHovered) ? .folder : .none,
+ paddingLeading: 0,
+ accentColor: accentColor
+ )
+ }
+ }
+}
+
#Preview {
FolderCell(folder: NestableFolder(content: PreviewHelper.sampleFolder, children: []), currentFolderId: nil)
.environmentObject(PreviewHelper.sampleMailboxManager)
diff --git a/Mail/Views/Thread List/DragAndDropModifier.swift b/Mail/Views/Thread List/DragAndDropModifier.swift
new file mode 100644
index 000000000..ecb70561a
--- /dev/null
+++ b/Mail/Views/Thread List/DragAndDropModifier.swift
@@ -0,0 +1,127 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2024 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 .
+ */
+
+import MailCore
+import SwiftUI
+
+@available(macCatalyst 16.0, iOS 16.0, *)
+struct DraggedThread: Transferable, Codable {
+ let threadIds: [String]
+
+ static var transferRepresentation: some TransferRepresentation {
+ CodableRepresentation(contentType: .plainText)
+ }
+}
+
+extension EnvironmentValues {
+ @Entry
+ var isHovered = false
+}
+
+extension Notification.Name {
+ static let dropThreadSuccess = Notification.Name("dropThreadSuccess")
+}
+
+struct DropThreadViewModifier: ViewModifier {
+ @EnvironmentObject private var mailboxManager: MailboxManager
+ @EnvironmentObject private var actionsManager: ActionsManager
+
+ @State private var isDropTargeted = false
+
+ let destinationFolder: Folder
+
+ func body(content: Content) -> some View {
+ if #available(macCatalyst 16.0, iOS 16.0, *) {
+ content
+ .contentShape(Rectangle())
+ .dropDestination(for: DraggedThread.self) { draggedThreads, _ in
+ let flatThreads = draggedThreads.flatMap(\.threadIds)
+ handleDroppedThreads(flatThreads)
+
+ return true
+ } isTargeted: {
+ isDropTargeted = $0
+ }
+ .environment(\.isHovered, isDropTargeted)
+ } else {
+ content
+ }
+ }
+
+ private func handleDroppedThreads(_ draggedThreadsIds: [String]) {
+ let threads = draggedThreadsIds.compactMap {
+ mailboxManager.getThread(from: $0)
+ }
+
+ guard let originFolder = threads.first(where: { $0.folder != nil })?.folder,
+ originFolder != destinationFolder else {
+ return
+ }
+
+ let movedMessages = threads.flatMap(\.messages)
+
+ Task {
+ await tryOrDisplayError {
+ try await actionsManager.performMove(
+ messages: movedMessages,
+ from: originFolder,
+ to: destinationFolder
+ )
+ }
+ NotificationCenter.default.post(name: .dropThreadSuccess, object: nil)
+ }
+ }
+}
+
+struct DraggableThreadViewModifier: ViewModifier {
+ let draggedThreadId: [String]
+ let onSuccess: () -> Void
+
+ func body(content: Content) -> some View {
+ if #available(macCatalyst 16.0, iOS 16.0, *) {
+ content
+ .onAppear {
+ NotificationCenter.default.addObserver(forName: .dropThreadSuccess, object: nil, queue: .main) { _ in
+ onSuccess()
+ }
+ }
+ .onDisappear {
+ NotificationCenter.default.removeObserver(self, name: .dropThreadSuccess, object: nil)
+ }
+ #if os(macOS) || targetEnvironment(macCatalyst)
+ .draggable(DraggedThread(threadIds: draggedThreadId)) {
+ DraggedEnvelopeView(amount: draggedThreadId.count)
+ }
+ #else
+ .draggable(DraggedThread(threadIds: draggedThreadId))
+ #endif
+ } else {
+ content
+ }
+ }
+}
+
+extension View {
+ func dropThreadDestination(destinationFolder: Folder) -> some View {
+ modifier(DropThreadViewModifier(destinationFolder: destinationFolder))
+ }
+
+ func draggableThread(_ draggedThreadIds: [String], onSuccess: @escaping () -> Void) -> some View {
+ modifier(DraggableThreadViewModifier(draggedThreadId: draggedThreadIds, onSuccess: onSuccess))
+ }
+}
diff --git a/Mail/Views/Thread List/DraggedEnvelopeView.swift b/Mail/Views/Thread List/DraggedEnvelopeView.swift
new file mode 100644
index 000000000..0d34f59ff
--- /dev/null
+++ b/Mail/Views/Thread List/DraggedEnvelopeView.swift
@@ -0,0 +1,40 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2024 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 .
+ */
+
+import MailCore
+import SwiftUI
+
+struct DraggedEnvelopeView: View {
+ let amount: Int
+
+ var body: some View {
+ Image(systemName: "envelope.fill")
+ .font(.title)
+ .padding(value: .small)
+ .overlay(alignment: .bottomTrailing) {
+ if amount > 1 {
+ Text("\(amount)")
+ .padding(.horizontal, value: .small)
+ .padding(.vertical, value: .extraSmall)
+ .background(.red, in: .circle)
+ .foregroundStyle(.primary)
+ .font(MailTextStyle.bodySmall.font)
+ }
+ }
+ }
+}
diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift
index 58eafa6d3..de3d63943 100644
--- a/Mail/Views/Thread List/ThreadListView.swift
+++ b/Mail/Views/Thread List/ThreadListView.swift
@@ -123,6 +123,10 @@ struct ThreadListView: View {
isMultiSelected: multipleSelectionViewModel.selectedItems.ids
.contains(thread.id),
flushAlert: $flushAlert)
+ .draggableThread(multipleSelectionViewModel.selectedItems.ids
+ .isEmpty ? [thread.uid] : multipleSelectionViewModel.selectedItems.ids) {
+ multipleSelectionViewModel.selectedItems.removeAll()
+ }
}
.threadListCellAppearance()
.tag(thread)
diff --git a/MailCore/Cache/MailboxManager/MailboxManager.swift b/MailCore/Cache/MailboxManager/MailboxManager.swift
index 2ce10d429..b4bf111ee 100644
--- a/MailCore/Cache/MailboxManager/MailboxManager.swift
+++ b/MailCore/Cache/MailboxManager/MailboxManager.swift
@@ -208,6 +208,11 @@ public final class MailboxManager: ObservableObject, MailboxManageable {
}
return result
}
+
+ public func getThread(from threadId: String) -> Thread? {
+ guard let thread = transactionExecutor.fetchObject(ofType: Thread.self, forPrimaryKey: threadId) else { return nil }
+ return thread.freezeIfNeeded()
+ }
}
// MARK: - Equatable conformance