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