Skip to content

Commit

Permalink
feat: Drag and drop mail (#1556)
Browse files Browse the repository at this point in the history
  • Loading branch information
lebojo authored Oct 4, 2024
2 parents 275745e + ee057a1 commit 7fe86ab
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 11 deletions.
33 changes: 22 additions & 11 deletions Mail/Views/Menu Drawer/Folders/FolderCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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)
Expand Down
127 changes: 127 additions & 0 deletions Mail/Views/Thread List/DragAndDropModifier.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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))
}
}
40 changes: 40 additions & 0 deletions Mail/Views/Thread List/DraggedEnvelopeView.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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)
}
}
}
}
4 changes: 4 additions & 0 deletions Mail/Views/Thread List/ThreadListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions MailCore/Cache/MailboxManager/MailboxManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7fe86ab

Please sign in to comment.