diff --git a/.package.resolved b/.package.resolved
index e28dedf7e..d525fc03c 100644
--- a/.package.resolved
+++ b/.package.resolved
@@ -27,15 +27,6 @@
"version" : "3.8.0"
}
},
- {
- "identity" : "floatingpanel",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/SCENEE/FloatingPanel",
- "state" : {
- "revision" : "2a29cb5b3ecf4beb67cf524a030dd74a11b956c4",
- "version" : "2.6.1"
- }
- },
{
"identity" : "ios-bug-tracker",
"kind" : "remoteSourceControl",
@@ -224,6 +215,15 @@
"version" : "1.5.2"
}
},
+ {
+ "identity" : "swiftbackports",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/shaps80/SwiftBackports",
+ "state" : {
+ "revision" : "fafbeabf78b7e364abbbb7565cdfeee42af16211",
+ "version" : "1.0.2"
+ }
+ },
{
"identity" : "swiftregex",
"kind" : "remoteSourceControl",
@@ -260,6 +260,15 @@
"version" : "1.3.0"
}
},
+ {
+ "identity" : "swiftuibackports",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/shaps80/SwiftUIBackports",
+ "state" : {
+ "revision" : "556d42f391b74059a354b81b8c8e19cc7cb576f4",
+ "version" : "1.15.1"
+ }
+ },
{
"identity" : "wrappinghstack",
"kind" : "remoteSourceControl",
diff --git a/Mail/Views/SheetView.swift b/Mail/Components/ActionsPanelButton.swift
similarity index 51%
rename from Mail/Views/SheetView.swift
rename to Mail/Components/ActionsPanelButton.swift
index 912970b67..9664a72b7 100644
--- a/Mail/Views/SheetView.swift
+++ b/Mail/Components/ActionsPanelButton.swift
@@ -16,34 +16,33 @@
along with this program. If not, see .
*/
+import CocoaLumberjackSwift
import MailCore
import MailResources
import SwiftUI
-struct SheetView: View where Content: View {
- @Environment(\.dismiss) private var dismiss
+struct ActionsPanelButton: View {
+ @Environment(\.isCompactWindow) private var isCompactWindow
- @ViewBuilder let content: Content
+ @State private var actionsTarget: ActionsTarget?
- var body: some View {
- NavigationView {
- content
- .navigationBarItems(leading: Button {
- dismiss()
- } label: {
- Label(MailResourcesStrings.Localizable.buttonClose, systemImage: "xmark")
- })
- }
- .onReceive(NotificationCenter.default.publisher(for: Constants.dismissMoveSheetNotificationName)) { _ in
- dismiss()
- }
- }
-}
+ var message: Message?
+ var threads: [Thread]?
+ var isMultiSelectionEnabled = false
+ @ViewBuilder var label: () -> Content
-struct SheetView_Previews: PreviewProvider {
- static var previews: some View {
- SheetView {
- EmptyView()
+ var body: some View {
+ Button {
+ if let message {
+ actionsTarget = .message(message)
+ } else if let threads {
+ actionsTarget = .threads(threads, isMultiSelectionEnabled)
+ } else {
+ DDLogWarn("MoreButton has no action target, did you forget to set message or threads ?")
+ }
+ } label: {
+ label()
}
+ .actionsPanel(actionsTarget: $actionsTarget)
}
}
diff --git a/Mail/Components/ToolbarButton.swift b/Mail/Components/ToolbarButton.swift
index 31f9a345f..ac4810be2 100644
--- a/Mail/Components/ToolbarButton.swift
+++ b/Mail/Components/ToolbarButton.swift
@@ -20,28 +20,31 @@ import MailCore
import MailResources
import SwiftUI
-struct ToolbarButton: View {
+struct ToolbarButtonLabel: View {
@Environment(\.verticalSizeClass) private var sizeClass
let text: String
let icon: Image
- let action: () -> Void
- init(text: String, icon: Image, action: @escaping () -> Void) {
- self.text = text
- self.icon = icon
- self.action = action
+ var body: some View {
+ Label {
+ Text(text)
+ .textStyle(MailTextStyle.labelMediumAccent)
+ } icon: {
+ icon
+ }
+ .dynamicLabelStyle(sizeClass: sizeClass ?? .regular)
}
+}
+
+struct ToolbarButton: View {
+ let text: String
+ let icon: Image
+ let action: () -> Void
var body: some View {
Button(action: action) {
- Label {
- Text(text)
- .textStyle(MailTextStyle.labelMediumAccent)
- } icon: {
- icon
- }
- .dynamicLabelStyle(sizeClass: sizeClass ?? .regular)
+ ToolbarButtonLabel(text: text, icon: icon)
}
.frame(maxWidth: .infinity)
}
diff --git a/Mail/SceneDelegate.swift b/Mail/SceneDelegate.swift
index 58034ffd4..de2a6f8f4 100644
--- a/Mail/SceneDelegate.swift
+++ b/Mail/SceneDelegate.swift
@@ -101,7 +101,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate
let view = view.environment(\.window, window)
// Set root view controller
let hostingController = UIHostingController(rootView: view)
- FloatingPanelHelper.shared.attachToViewController(hostingController)
setRootViewController(hostingController)
}
diff --git a/Mail/Utils/AdaptivePanelViewModifier.swift b/Mail/Utils/AdaptivePanelViewModifier.swift
new file mode 100644
index 000000000..170e50e0a
--- /dev/null
+++ b/Mail/Utils/AdaptivePanelViewModifier.swift
@@ -0,0 +1,47 @@
+/*
+ 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 .
+ */
+
+import SwiftUI
+
+extension View {
+ func adaptivePanel(item: Binding- ,
+ @ViewBuilder content: @escaping (Item) -> Content) -> some View {
+ return modifier(AdaptivePanelViewModifier(item: item, panelContent: content))
+ }
+}
+
+struct AdaptivePanelViewModifier: ViewModifier {
+ @Environment(\.isCompactWindow) private var isCompactWindow
+
+ @Binding var item: Item?
+ @ViewBuilder var panelContent: (Item) -> PanelContent
+
+ func body(content: Content) -> some View {
+ if isCompactWindow {
+ content.floatingPanel(item: $item) { item in
+ panelContent(item)
+ }
+ } else {
+ content.popover(item: $item) { item in
+ panelContent(item)
+ .padding()
+ .frame(idealWidth: 400)
+ }
+ }
+ }
+}
diff --git a/MailCore/Utils/Environment+Extension.swift b/Mail/Utils/Environment+Extension.swift
similarity index 77%
rename from MailCore/Utils/Environment+Extension.swift
rename to Mail/Utils/Environment+Extension.swift
index fab8c86fc..cd1468be5 100644
--- a/MailCore/Utils/Environment+Extension.swift
+++ b/Mail/Utils/Environment+Extension.swift
@@ -29,3 +29,14 @@ public extension EnvironmentValues {
set { self[WindowKey.self] = newValue }
}
}
+
+public struct CompactWindowKey: EnvironmentKey {
+ public static let defaultValue = true
+}
+
+public extension EnvironmentValues {
+ var isCompactWindow: CompactWindowKey.Value {
+ get { return self[CompactWindowKey.self] }
+ set { self[CompactWindowKey.self] = newValue }
+ }
+}
diff --git a/Mail/Utils/FloatingPanelHelper.swift b/Mail/Utils/FloatingPanelHelper.swift
index f67705577..2da9c3a07 100644
--- a/Mail/Utils/FloatingPanelHelper.swift
+++ b/Mail/Utils/FloatingPanelHelper.swift
@@ -17,159 +17,100 @@
*/
import Combine
-import FloatingPanel
import MailResources
import SwiftUI
+import SwiftUIBackports
-class AdaptiveDriveFloatingPanelController: FloatingPanelController {
- private var contentSizeObservation: NSKeyValueObservation?
- private let maxPanelWidth = 800.0
- var halfOpening = false
-
- deinit {
- contentSizeObservation?.invalidate()
- }
-
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- updateMargins()
- updateLayout(size: size)
- }
-
- private func updateMargins() {
- if view.frame.size.width > maxPanelWidth {
- let insetWidth = view.frame.width - maxPanelWidth
- surfaceView.containerMargins = UIEdgeInsets(top: 0, left: insetWidth / 2, bottom: 0, right: insetWidth / 2)
- } else {
- surfaceView.containerMargins = .zero
+extension View {
+ func floatingPanel(isPresented: Binding,
+ @ViewBuilder content: @escaping () -> Content) -> some View {
+ sheet(isPresented: isPresented) {
+ if #available(iOS 16.0, *) {
+ content().modifier(SelfSizingPanelViewModifier())
+ } else {
+ content().modifier(SelfSizingPanelBackportViewModifier())
+ }
}
}
- func updateLayout(size: CGSize) {
- guard let trackingScrollView = trackingScrollView else { return }
- let fullHeight = min(
- trackingScrollView.contentSize.height + surfaceView.contentPadding.top + surfaceView.contentPadding.bottom,
- size.height - 96
- )
- let layout = AdaptiveFloatingPanelLayout(
- height: fullHeight,
- halfOpening: halfOpening && fullHeight > size.height / 2
- )
- self.layout = layout
- invalidateLayout()
- }
-
- func trackAndObserve(scrollView: UIScrollView) {
- contentSizeObservation?.invalidate()
- contentSizeObservation = scrollView.observe(\.contentSize, options: [.new, .old]) { [weak self] _, observedChanges in
- // Do not update layout if the new value is the same as the old one (to fix a bug with collectionView)
- guard observedChanges.newValue != observedChanges.oldValue,
- let window = self?.view.window else { return }
- self?.updateLayout(size: window.bounds.size)
- }
- track(scrollView: scrollView)
- if let window = view.window {
- updateMargins()
- updateLayout(size: window.bounds.size)
+ func floatingPanel(item: Binding
- ,
+ @ViewBuilder content: @escaping (Item) -> Content) -> some View {
+ sheet(item: item) { item in
+ if #available(iOS 16.0, *) {
+ content(item).modifier(SelfSizingPanelViewModifier())
+ } else {
+ content(item).modifier(SelfSizingPanelBackportViewModifier())
+ }
}
}
-}
-
-class AdaptiveFloatingPanelLayout: FloatingPanelLayout {
- let position: FloatingPanelPosition = .bottom
- let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring]
- let initialState: FloatingPanelState
- init(height: CGFloat, halfOpening: Bool) {
- initialState = .half
- if halfOpening {
- anchors = [
- .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
- .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea)
- ]
+ func ikPresentationCornerRadius(_ cornerRadius: CGFloat?) -> some View {
+ if #available(iOS 16.4, *) {
+ return presentationCornerRadius(cornerRadius)
} else {
- anchors = [
- .half: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea)
- ]
+ return introspectViewController { viewController in
+ viewController.sheetPresentationController?.preferredCornerRadius = cornerRadius
+ }
}
}
-
- func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
- return 0.2
- }
}
-class DisplayedFloatingPanelState: ObservableObject, FloatingPanelControllerDelegate {
- @Published var isOpen = false
- @Published private(set) var state: State?
+@available(iOS, introduced: 15, deprecated: 16, message: "Use native way")
+struct SelfSizingPanelBackportViewModifier: ViewModifier {
+ @State var currentDetents: Set = [.medium]
+ private let topPadding: CGFloat = 24
- private let floatingPanel: AdaptiveDriveFloatingPanelController
-
- init() {
- floatingPanel = AdaptiveDriveFloatingPanelController()
- let appearance = SurfaceAppearance()
- appearance.cornerRadius = 20
- appearance.backgroundColor = MailResourcesAsset.backgroundSecondaryColor.color
- floatingPanel.delegate = self
- floatingPanel.surfaceView.appearance = appearance
- floatingPanel.surfaceView.grabberHandlePadding = 16
- floatingPanel.surfaceView.grabberHandleSize = CGSize(width: 45, height: 5)
- floatingPanel.surfaceView.grabberHandle.barColor = MailResourcesAsset.elementsColor.color
- floatingPanel.surfaceView.contentPadding = UIEdgeInsets(top: 32, left: 0, bottom: 16, right: 0)
- floatingPanel.backdropView.dismissalTapGestureRecognizer.isEnabled = true
- floatingPanel.isRemovalInteractionEnabled = true
- }
-
- func createPanelContent(content: Content, halfOpening: Bool) {
- floatingPanel.halfOpening = halfOpening
- let content = content.introspectScrollView { [weak self] scrollView in
- self?.floatingPanel.trackAndObserve(scrollView: scrollView)
+ func body(content: Content) -> some View {
+ ScrollView {
+ content
+ .padding(.bottom, 16)
}
- let viewController = UIHostingController(rootView: content)
- viewController.view.backgroundColor = nil
- floatingPanel.set(contentViewController: viewController)
- }
-
- func open(state: State) {
- if let rootViewController = FloatingPanelHelper.shared.rootViewController {
- rootViewController.present(floatingPanel, animated: true)
- self.state = state
- isOpen = true
+ .padding(.top, topPadding)
+ .introspectScrollView { scrollView in
+ guard !currentDetents.contains(.large) else { return }
+ let totalPanelContentHeight = scrollView.contentSize.height + topPadding
+
+ scrollView.isScrollEnabled = totalPanelContentHeight > (scrollView.window?.bounds.height ?? 0)
+ if totalPanelContentHeight > (scrollView.window?.bounds.height ?? 0) / 2 {
+ currentDetents = [.medium, .large]
+ }
}
- }
-
- func close() {
- state = nil
- isOpen = false
- floatingPanel.dismiss(animated: true)
- }
-
- func floatingPanelDidRemove(_ fpc: FloatingPanelController) {
- state = nil
- isOpen = false
+ .backport.presentationDragIndicator(.visible)
+ .backport.presentationDetents(currentDetents)
+ .ikPresentationCornerRadius(20)
}
}
-class FloatingPanelHelper: FloatingPanelControllerDelegate {
- static let shared = FloatingPanelHelper()
+@available(iOS 16.0, *)
+struct SelfSizingPanelViewModifier: ViewModifier {
+ @State var currentDetents: Set = [.height(0)]
+ @State var selection: PresentationDetent = .height(0)
+ private let topPadding: CGFloat = 24
- private let sharedFloatingPanel = FloatingPanelController()
- private(set) var rootViewController: UIViewController?
- private init() {
- // Protected constructor for singleton
- }
-
- func attachToViewController(_ viewController: UIViewController) {
- rootViewController = viewController
- }
-}
-
-extension View {
- func floatingPanel(state: DisplayedFloatingPanelState,
- halfOpening: Bool = false,
- @ViewBuilder content: () -> Content) -> some View {
- state.createPanelContent(content: ScrollView { content() }.defaultAppStorage(.shared),
- halfOpening: halfOpening)
- return self
+ func body(content: Content) -> some View {
+ ScrollView {
+ content
+ .padding(.bottom, 16)
+ }
+ .padding(.top, topPadding)
+ .introspectScrollView { scrollView in
+ let totalPanelContentHeight = scrollView.contentSize.height + topPadding
+ guard selection != .height(totalPanelContentHeight) else { return }
+
+ scrollView.isScrollEnabled = totalPanelContentHeight > (scrollView.window?.bounds.height ?? 0)
+ DispatchQueue.main.async {
+ currentDetents = [.height(0), .height(totalPanelContentHeight)]
+ selection = .height(totalPanelContentHeight)
+
+ // Hack to let time for the animation to finish, after animation is complete we can modify the state again
+ // if we don't do this the animation is cut before finishing
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ currentDetents = [selection]
+ }
+ }
+ }
+ .presentationDetents(currentDetents, selection: $selection)
+ .presentationDragIndicator(.visible)
+ .ikPresentationCornerRadius(20)
}
}
diff --git a/Mail/Helpers/BottomSheetState.swift b/Mail/Utils/NavigationStore.swift
similarity index 71%
rename from Mail/Helpers/BottomSheetState.swift
rename to Mail/Utils/NavigationStore.swift
index 42803c486..9c34f259e 100644
--- a/Mail/Helpers/BottomSheetState.swift
+++ b/Mail/Utils/NavigationStore.swift
@@ -16,19 +16,11 @@
along with this program. If not, see .
*/
+import Foundation
import SwiftUI
+import MailCore
-class BottomSheetState: ObservableObject {
- @Published var isOpen = false
- @Published private(set) var state: State?
-
- func open(state: State) {
- self.state = state
- isOpen = true
- }
-
- func close() {
- state = nil
- isOpen = false
- }
+class NavigationStore: ObservableObject {
+ @Published var messageReply: MessageReply?
+ @Published var threadPath = [Thread]()
}
diff --git a/Mail/Views/Bottom sheets/Actions/ActionUtils.swift b/Mail/Views/Bottom sheets/Actions/ActionUtils.swift
new file mode 100644
index 000000000..1f1074b6d
--- /dev/null
+++ b/Mail/Views/Bottom sheets/Actions/ActionUtils.swift
@@ -0,0 +1,54 @@
+/*
+ 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 .
+ */
+
+import Foundation
+import InfomaniakCoreUI
+import MailResources
+import MailCore
+
+struct ActionUtils {
+ let actionsTarget: ActionsTarget
+ let mailboxManager: MailboxManager
+
+ func move(to folder: Folder) async throws {
+ let undoRedoAction: UndoRedoAction
+ let snackBarMessage: String
+ switch actionsTarget {
+ case .threads(let threads, _):
+ guard threads.first?.folder != folder else { return }
+ undoRedoAction = try await mailboxManager.move(threads: threads, to: folder)
+ snackBarMessage = MailResourcesStrings.Localizable.snackbarThreadsMoved(folder.localizedName)
+ case .message(let message):
+ guard message.folderId != folder.id else { return }
+ var messages = [message]
+ messages.append(contentsOf: message.duplicates)
+ undoRedoAction = try await mailboxManager.move(messages: messages, to: folder)
+ snackBarMessage = MailResourcesStrings.Localizable.snackbarMessageMoved(folder.localizedName)
+ }
+
+ await IKSnackBar.showCancelableSnackBar(message: snackBarMessage,
+ cancelSuccessMessage: MailResourcesStrings.Localizable.snackbarMoveCancelled,
+ undoRedoAction: undoRedoAction,
+ mailboxManager: mailboxManager)
+ }
+
+ func move(to folderRole: FolderRole) async throws {
+ guard let folder = mailboxManager.getFolder(with: folderRole)?.freeze() else { return }
+ try await move(to: folder)
+ }
+}
diff --git a/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift b/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift
new file mode 100644
index 000000000..ea02a4217
--- /dev/null
+++ b/Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift
@@ -0,0 +1,69 @@
+/*
+ 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 .
+ */
+
+import Foundation
+import MailCore
+import SwiftUI
+
+extension View {
+ func actionsPanel(actionsTarget: Binding, completionHandler: (() -> Void)? = nil) -> some View {
+ return modifier(ActionsPanelViewModifier(actionsTarget: actionsTarget, completionHandler: completionHandler))
+ }
+}
+
+struct ActionsPanelViewModifier: ViewModifier {
+ @EnvironmentObject private var mailboxManager: MailboxManager
+ @EnvironmentObject private var navigationStore: NavigationStore
+
+ @State private var moveAction: MoveAction?
+ @State private var reportJunkActionsTarget: ActionsTarget?
+ @State private var reportedForPhishingMessage: Message?
+ @State private var reportedForDisplayProblemMessage: Message?
+
+ @Binding var actionsTarget: ActionsTarget?
+
+ var completionHandler: (() -> Void)?
+
+ func body(content: Content) -> some View {
+ content.adaptivePanel(item: $actionsTarget) { target in
+ ActionsView(mailboxManager: mailboxManager,
+ target: target,
+ moveAction: $moveAction,
+ messageReply: $navigationStore.messageReply,
+ reportJunkActionsTarget: $reportJunkActionsTarget,
+ reportedForDisplayProblemMessage: $reportedForDisplayProblemMessage) {
+ completionHandler?()
+ }
+ }
+ .sheet(item: $moveAction) { moveAction in
+ MoveEmailView(moveAction: moveAction)
+ .sheetViewStyle()
+ }
+ .floatingPanel(item: $reportJunkActionsTarget) { target in
+ ReportJunkView(mailboxManager: mailboxManager,
+ target: target,
+ reportedForPhishingMessage: $reportedForPhishingMessage)
+ }
+ .customAlert(item: $reportedForPhishingMessage) { message in
+ ReportPhishingView(message: message)
+ }
+ .customAlert(item: $reportedForDisplayProblemMessage) { message in
+ ReportDisplayProblemView(message: message)
+ }
+ }
+}
diff --git a/Mail/Views/Bottom sheets/Actions/ActionsView.swift b/Mail/Views/Bottom sheets/Actions/ActionsView.swift
index 80ccb48cd..35eb9445e 100644
--- a/Mail/Views/Bottom sheets/Actions/ActionsView.swift
+++ b/Mail/Views/Bottom sheets/Actions/ActionsView.swift
@@ -23,30 +23,28 @@ import MailResources
import SwiftUI
struct ActionsView: View {
- @ObservedObject var viewModel: ActionsViewModel
+ @StateObject var viewModel: ActionsViewModel
init(mailboxManager: MailboxManager,
target: ActionsTarget,
- state: ThreadBottomSheet,
- globalSheet: GlobalBottomSheet,
- globalAlert: GlobalAlert? = nil,
- moveSheet: MoveSheet? = nil,
- replyHandler: ((Message, ReplyMode) -> Void)? = nil,
+ moveAction: Binding? = nil,
+ messageReply: Binding? = nil,
+ reportJunkActionsTarget: Binding? = nil,
+ reportedForDisplayProblemMessage: Binding? = nil,
completionHandler: (() -> Void)? = nil) {
var matomoCategory = MatomoUtils.EventCategory.bottomSheetMessageActions
if case .threads = target {
matomoCategory = .bottomSheetThreadActions
}
- viewModel = ActionsViewModel(mailboxManager: mailboxManager,
- target: target,
- state: state,
- globalSheet: globalSheet,
- globalAlert: globalAlert,
- moveSheet: moveSheet,
- matomoCategory: matomoCategory,
- replyHandler: replyHandler,
- completionHandler: completionHandler)
+ _viewModel = StateObject(wrappedValue: ActionsViewModel(mailboxManager: mailboxManager,
+ target: target,
+ moveAction: moveAction,
+ messageReply: messageReply,
+ reportJunkActionsTarget: reportJunkActionsTarget,
+ reportedForDisplayProblemMessage: reportedForDisplayProblemMessage,
+ matomoCategory: matomoCategory,
+ completionHandler: completionHandler))
}
var body: some View {
@@ -76,16 +74,13 @@ struct ActionsView: View {
struct ActionsView_Previews: PreviewProvider {
static var previews: some View {
- ActionsView(mailboxManager: PreviewHelper.sampleMailboxManager,
- target: .threads([PreviewHelper.sampleThread], false),
- state: ThreadBottomSheet(),
- globalSheet: GlobalBottomSheet(),
- globalAlert: GlobalAlert()) { _, _ in /* Preview */ }
+ ActionsView(mailboxManager: PreviewHelper.sampleMailboxManager, target: .threads([PreviewHelper.sampleThread], false))
.accentColor(AccentColor.pink.primary.swiftUIColor)
}
}
struct QuickActionView: View {
+ @Environment(\.dismiss) var dismiss
@ObservedObject var viewModel: ActionsViewModel
let action: Action
@@ -93,6 +88,7 @@ struct QuickActionView: View {
var body: some View {
Button {
+ dismiss()
Task {
await tryOrDisplayError {
try await viewModel.didTap(action: action)
@@ -122,6 +118,7 @@ struct QuickActionView: View {
}
struct ActionView: View {
+ @Environment(\.dismiss) var dismiss
@ObservedObject var viewModel: ActionsViewModel
let action: Action
@@ -129,6 +126,7 @@ struct ActionView: View {
var body: some View {
Button {
+ dismiss()
Task {
await tryOrDisplayError {
try await viewModel.didTap(action: action)
diff --git a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift
index 69d83470c..129e54514 100644
--- a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift
+++ b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift
@@ -173,7 +173,16 @@ struct Action: Identifiable, Equatable {
}
}
-enum ActionsTarget: Equatable {
+enum ActionsTarget: Equatable, Identifiable {
+ var id: String {
+ switch self {
+ case .threads(let threads, let isMultiSelectionEnabled):
+ return threads.map { $0.id }.joined()
+ case .message(let message):
+ return message.uid
+ }
+ }
+
case threads([Thread], Bool)
case message(Message)
@@ -199,11 +208,11 @@ enum ActionsTarget: Equatable {
@MainActor class ActionsViewModel: ObservableObject {
private let mailboxManager: MailboxManager
private let target: ActionsTarget
- private let state: ThreadBottomSheet
- private let globalSheet: GlobalBottomSheet
- private let globalAlert: GlobalAlert?
- private let moveSheet: MoveSheet?
- private let replyHandler: ((Message, ReplyMode) -> Void)?
+ private let moveAction: Binding?
+ private let messageReply: Binding?
+ private let reportJunkActionsTarget: Binding?
+ private let reportedForPhishingMessage: Binding?
+ private let reportedForDisplayProblemMessage: Binding?
private let completionHandler: (() -> Void)?
private let matomoCategory: MatomoUtils.EventCategory?
@@ -215,20 +224,20 @@ enum ActionsTarget: Equatable {
init(mailboxManager: MailboxManager,
target: ActionsTarget,
- state: ThreadBottomSheet,
- globalSheet: GlobalBottomSheet,
- globalAlert: GlobalAlert? = nil,
- moveSheet: MoveSheet? = nil,
+ moveAction: Binding? = nil,
+ messageReply: Binding? = nil,
+ reportJunkActionsTarget: Binding? = nil,
+ reportedForPhishingMessage: Binding? = nil,
+ reportedForDisplayProblemMessage: Binding? = nil,
matomoCategory: MatomoUtils.EventCategory? = nil,
- replyHandler: ((Message, ReplyMode) -> Void)? = nil,
completionHandler: (() -> Void)? = nil) {
self.mailboxManager = mailboxManager
self.target = target.freeze()
- self.state = state
- self.globalSheet = globalSheet
- self.globalAlert = globalAlert
- self.moveSheet = moveSheet
- self.replyHandler = replyHandler
+ self.moveAction = moveAction
+ self.messageReply = messageReply
+ self.reportJunkActionsTarget = reportJunkActionsTarget
+ self.reportedForPhishingMessage = reportedForPhishingMessage
+ self.reportedForDisplayProblemMessage = reportedForDisplayProblemMessage
self.completionHandler = completionHandler
self.matomoCategory = matomoCategory
setActions()
@@ -289,8 +298,6 @@ enum ActionsTarget: Equatable {
}
func didTap(action: Action) async throws {
- state.close()
- globalSheet.close()
if let matomoCategory, let matomoName = action.matomoName {
if case .threads(let threads, let isMultipleSelectionEnabled) = target, isMultipleSelectionEnabled {
matomo.trackBulkEvent(
@@ -310,7 +317,7 @@ enum ActionsTarget: Equatable {
case .replyAll:
try await reply(mode: .replyAll)
case .archive:
- try await move(to: .archive)
+ try await ActionUtils(actionsTarget: target, mailboxManager: mailboxManager).move(to: .archive)
case .forward:
try await reply(mode: .forward)
case .markAsRead, .markAsUnread:
@@ -324,9 +331,9 @@ enum ActionsTarget: Equatable {
case .reportJunk:
displayReportJunk()
case .spam:
- try await move(to: .spam)
+ try await ActionUtils(actionsTarget: target, mailboxManager: mailboxManager).move(to: .spam)
case .nonSpam:
- try await move(to: .inbox)
+ try await ActionUtils(actionsTarget: target, mailboxManager: mailboxManager).move(to: .inbox)
case .block:
try await block()
case .phishing:
@@ -334,44 +341,17 @@ enum ActionsTarget: Equatable {
case .print:
printAction()
case .report:
- report()
+ reportDisplayProblem()
case .editMenu:
editMenu()
case .moveToInbox:
- try await move(to: .inbox)
+ try await ActionUtils(actionsTarget: target, mailboxManager: mailboxManager).move(to: .inbox)
default:
print("Warning: Unhandled action!")
}
completionHandler?()
}
- private func move(to folder: Folder) async throws {
- let undoRedoAction: UndoRedoAction
- let snackBarMessage: String
- switch target {
- case .threads(let threads, _):
- guard threads.first?.folder != folder else { return }
- undoRedoAction = try await mailboxManager.move(threads: threads, to: folder)
- snackBarMessage = MailResourcesStrings.Localizable.snackbarThreadsMoved(folder.localizedName)
- case .message(let message):
- guard message.folderId != folder.id else { return }
- var messages = [message]
- messages.append(contentsOf: message.duplicates)
- undoRedoAction = try await mailboxManager.move(messages: messages, to: folder)
- snackBarMessage = MailResourcesStrings.Localizable.snackbarMessageMoved(folder.localizedName)
- }
-
- IKSnackBar.showCancelableSnackBar(message: snackBarMessage,
- cancelSuccessMessage: MailResourcesStrings.Localizable.snackbarMoveCancelled,
- undoRedoAction: undoRedoAction,
- mailboxManager: mailboxManager)
- }
-
- private func move(to folderRole: FolderRole) async throws {
- guard let folder = mailboxManager.getFolder(with: folderRole)?.freeze() else { return }
- try await move(to: folder)
- }
-
// MARK: - Actions methods
private func delete() async throws {
@@ -384,14 +364,21 @@ enum ActionsTarget: Equatable {
}
private func reply(mode: ReplyMode) async throws {
+ var displayedMessageReply: MessageReply?
switch target {
case .threads(let threads, _):
// We don't handle this action in multiple selection
guard threads.count == 1, let thread = threads.first,
let message = thread.lastMessageToExecuteAction() else { break }
- replyHandler?(message, mode)
+ displayedMessageReply = MessageReply(message: message, replyMode: mode)
case .message(let message):
- replyHandler?(message, mode)
+ displayedMessageReply = MessageReply(message: message, replyMode: mode)
+ }
+ // FIXME: There seems to be a bug where SwiftUI looses the "context" and attempts to present
+ // the view controller before waiting for the dismiss of the first one if we use a closure
+ // (this "fix" is temporary)
+ DispatchQueue.main.async { [weak self] in
+ self?.messageReply?.wrappedValue = displayedMessageReply
}
}
@@ -413,10 +400,8 @@ enum ActionsTarget: Equatable {
folderId = message.folderId
}
- moveSheet?.state = .move(folderId: folderId) { folder in
- Task {
- try await self.move(to: folder)
- }
+ DispatchQueue.main.async { [weak self, target] in
+ self?.moveAction?.wrappedValue = MoveAction(fromFolderId: folderId, target: target)
}
}
@@ -443,7 +428,7 @@ enum ActionsTarget: Equatable {
}
private func displayReportJunk() {
- globalSheet.open(state: .reportJunk(threadBottomSheet: state, target: target))
+ reportJunkActionsTarget?.wrappedValue = target
}
private func block() async throws {
@@ -458,7 +443,9 @@ enum ActionsTarget: Equatable {
private func phishing() async throws {
// This action is only available on a single message
guard case .message(let message) = target else { return }
- globalAlert?.state = .reportPhishing(message: message)
+ DispatchQueue.main.async { [weak self] in
+ self?.reportedForPhishingMessage?.wrappedValue = message
+ }
}
private func printAction() {
@@ -466,10 +453,12 @@ enum ActionsTarget: Equatable {
showWorkInProgressSnackBar()
}
- private func report() {
+ private func reportDisplayProblem() {
// This action is only available on a single message
guard case .message(let message) = target else { return }
- globalAlert?.state = .reportDisplayProblem(message: message)
+ DispatchQueue.main.async { [weak self] in
+ self?.reportedForDisplayProblemMessage?.wrappedValue = message
+ }
}
private func editMenu() {
diff --git a/Mail/Views/Bottom sheets/Actions/ReplyActionsView.swift b/Mail/Views/Bottom sheets/Actions/ReplyActionsView.swift
index 7fbf03a45..46ecb8abf 100644
--- a/Mail/Views/Bottom sheets/Actions/ReplyActionsView.swift
+++ b/Mail/Views/Bottom sheets/Actions/ReplyActionsView.swift
@@ -22,21 +22,17 @@ import MailCore
import SwiftUI
struct ReplyActionsView: View {
- @ObservedObject var viewModel: ActionsViewModel
+ @StateObject var viewModel: ActionsViewModel
var quickActions: [Action] = [.reply, .replyAll]
init(mailboxManager: MailboxManager,
- target: ActionsTarget,
- state: ThreadBottomSheet,
- globalSheet: GlobalBottomSheet,
- replyHandler: @escaping (Message, ReplyMode) -> Void) {
- viewModel = ActionsViewModel(mailboxManager: mailboxManager,
- target: target,
- state: state,
- globalSheet: globalSheet,
- matomoCategory: .replyBottomSheet,
- replyHandler: replyHandler)
+ message: Message,
+ messageReply: Binding?) {
+ _viewModel = StateObject(wrappedValue: ActionsViewModel(mailboxManager: mailboxManager,
+ target: .message(message),
+ messageReply: messageReply,
+ matomoCategory: .replyBottomSheet))
}
var body: some View {
@@ -56,9 +52,8 @@ struct ReplyActionsView: View {
struct ReplyActionsView_Previews: PreviewProvider {
static var previews: some View {
ReplyActionsView(mailboxManager: PreviewHelper.sampleMailboxManager,
- target: .threads([PreviewHelper.sampleThread], false),
- state: ThreadBottomSheet(),
- globalSheet: GlobalBottomSheet()) { _, _ in /* Preview */ }
+ message: PreviewHelper.sampleMessage,
+ messageReply: nil)
.accentColor(AccentColor.pink.primary.swiftUIColor)
}
}
diff --git a/Mail/Views/Bottom sheets/Actions/ReportJunkView.swift b/Mail/Views/Bottom sheets/Actions/ReportJunkView.swift
index b148c0f89..f940fb408 100644
--- a/Mail/Views/Bottom sheets/Actions/ReportJunkView.swift
+++ b/Mail/Views/Bottom sheets/Actions/ReportJunkView.swift
@@ -22,21 +22,17 @@ import MailCore
import SwiftUI
struct ReportJunkView: View {
- @ObservedObject var viewModel: ActionsViewModel
+ @StateObject var viewModel: ActionsViewModel
var actions: [Action] = []
init(mailboxManager: MailboxManager,
target: ActionsTarget,
- state: ThreadBottomSheet,
- globalSheet: GlobalBottomSheet,
- globalAlert: GlobalAlert) {
- viewModel = ActionsViewModel(mailboxManager: mailboxManager,
- target: target,
- state: state,
- globalSheet: globalSheet,
- globalAlert: globalAlert)
- if case let .message(message) = target {
+ reportedForPhishingMessage: Binding) {
+ _viewModel = StateObject(wrappedValue: ActionsViewModel(mailboxManager: mailboxManager,
+ target: target,
+ reportedForPhishingMessage: reportedForPhishingMessage))
+ if case .message(let message) = target {
let spam = message.folder?.role == .spam
actions.append(contentsOf: [
spam ? .nonSpam : .spam,
@@ -63,10 +59,8 @@ struct ReportJunkView: View {
struct ReportJunkView_Previews: PreviewProvider {
static var previews: some View {
ReportJunkView(mailboxManager: PreviewHelper.sampleMailboxManager,
- target: .threads([PreviewHelper.sampleThread], false),
- state: ThreadBottomSheet(),
- globalSheet: GlobalBottomSheet(),
- globalAlert: GlobalAlert())
+ target: .message(PreviewHelper.sampleMessage),
+ reportedForPhishingMessage: .constant(nil))
.accentColor(AccentColor.pink.primary.swiftUIColor)
}
}
diff --git a/Mail/Views/Bottom sheets/ContactActionsView.swift b/Mail/Views/Bottom sheets/ContactActionsView.swift
index af0cfc354..837b537e4 100644
--- a/Mail/Views/Bottom sheets/ContactActionsView.swift
+++ b/Mail/Views/Bottom sheets/ContactActionsView.swift
@@ -24,13 +24,24 @@ import MailResources
import SwiftUI
struct ContactActionsView: View {
- var recipient: Recipient
- var isRemoteContact: Bool
- @ObservedObject var bottomSheet: MessageBottomSheet
- var mailboxManager: MailboxManager
+ @EnvironmentObject var mailboxManager: MailboxManager
+ @Environment(\.dismiss) var dismiss
+ @LazyInjectService private var matomo: MatomoUtils
+
@State private var writtenToRecipient: Recipient?
- @LazyInjectService private var matomo: MatomoUtils
+ private let recipient: Recipient
+ private let actions: [ContactAction]
+
+ init(recipient: Recipient) {
+ self.recipient = recipient
+ let isRemoteContact = AccountManager.instance.currentContactManager?.getContact(for: recipient)?.remote != nil
+ if isRemoteContact {
+ actions = [.writeEmailAction, .copyEmailAction]
+ } else {
+ actions = [.writeEmailAction, .addContactsAction, .copyEmailAction]
+ }
+ }
private struct ContactAction: Hashable {
let name: String
@@ -54,13 +65,6 @@ struct ContactActionsView: View {
)
}
- private var actions: [ContactAction] {
- if isRemoteContact {
- return [.writeEmailAction, .copyEmailAction]
- }
- return [.writeEmailAction, .addContactsAction, .copyEmailAction]
- }
-
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
@@ -107,10 +111,10 @@ struct ContactActionsView: View {
case .writeEmailAction:
writeEmail()
case .addContactsAction:
- bottomSheet.close()
+ dismiss()
addToContacts()
case .copyEmailAction:
- bottomSheet.close()
+ dismiss()
copyEmail()
default:
return
@@ -138,9 +142,7 @@ struct ContactActionsView: View {
struct ContactActionsView_Previews: PreviewProvider {
static var previews: some View {
- ContactActionsView(recipient: PreviewHelper.sampleRecipient1,
- isRemoteContact: false,
- bottomSheet: MessageBottomSheet(),
- mailboxManager: PreviewHelper.sampleMailboxManager)
+ ContactActionsView(recipient: PreviewHelper.sampleRecipient1)
+ .environmentObject(PreviewHelper.sampleMailboxManager)
}
}
diff --git a/Mail/Views/Bottom sheets/RestoreEmailsView.swift b/Mail/Views/Bottom sheets/RestoreEmailsView.swift
index efc3ee1ae..0a7ff7de9 100644
--- a/Mail/Views/Bottom sheets/RestoreEmailsView.swift
+++ b/Mail/Views/Bottom sheets/RestoreEmailsView.swift
@@ -24,6 +24,8 @@ import MailResources
import SwiftUI
struct RestoreEmailsView: View {
+ @EnvironmentObject var mailboxManager: MailboxManager
+
@State private var selectedDate = ""
@State private var availableDates = [String]()
@@ -31,8 +33,6 @@ struct RestoreEmailsView: View {
@LazyInjectService private var matomo: MatomoUtils
- let mailboxManager: MailboxManager
-
var body: some View {
VStack(alignment: .leading) {
Text(MailResourcesStrings.Localizable.restoreEmailsTitle)
@@ -94,6 +94,7 @@ struct RestoreEmailsView: View {
struct RestoreEmailsView_Previews: PreviewProvider {
static var previews: some View {
- RestoreEmailsView(mailboxManager: PreviewHelper.sampleMailboxManager)
+ RestoreEmailsView()
+ .environmentObject(PreviewHelper.sampleMailboxManager)
}
}
diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift
index e53b3b761..21b1a66a9 100644
--- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift
+++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift
@@ -79,9 +79,8 @@ struct MailboxesManagementView: View {
}
}
.sheet(isPresented: $isShowingSwitchAccount) {
- SheetView {
- AccountListView()
- }
+ AccountListView()
+ .sheetViewStyle()
}
.sheet(isPresented: $isShowingManageAccount) {
AccountView(mailboxes: mailboxes)
diff --git a/Mail/Views/Menu Drawer/MailboxQuotaView.swift b/Mail/Views/Menu Drawer/MailboxQuotaView.swift
index bed7bda6f..e0eba554c 100644
--- a/Mail/Views/Menu Drawer/MailboxQuotaView.swift
+++ b/Mail/Views/Menu Drawer/MailboxQuotaView.swift
@@ -22,8 +22,6 @@ import MailResources
import SwiftUI
struct MailboxQuotaView: View {
- @EnvironmentObject var globalSheet: GlobalBottomSheet
-
let quotas: Quotas
var progressString: String {
return MailResourcesStrings.Localizable.menuDrawerMailboxStorage(
diff --git a/Mail/Views/Menu Drawer/MenuDrawerView.swift b/Mail/Views/Menu Drawer/MenuDrawerView.swift
index 9fad5c2cd..092589854 100644
--- a/Mail/Views/Menu Drawer/MenuDrawerView.swift
+++ b/Mail/Views/Menu Drawer/MenuDrawerView.swift
@@ -129,7 +129,6 @@ struct NavigationDrawer: View {
struct MenuDrawerView: View {
@EnvironmentObject var splitViewManager: SplitViewManager
- @EnvironmentObject var bottomSheet: GlobalBottomSheet
@StateObject var viewModel: MenuDrawerViewModel
@@ -192,17 +191,16 @@ struct MenuDrawerView: View {
}
.background(MailResourcesAsset.backgroundSecondaryColor.swiftUIColor.ignoresSafeArea())
.environment(\.folderCellType, .link)
- .onAppear {
- viewModel.createMenuItems(bottomSheet: bottomSheet)
- }
.sheet(isPresented: $viewModel.isShowingHelp) {
- SheetView {
- HelpView()
- }
+ HelpView()
+ .sheetViewStyle()
}
.sheet(isPresented: $viewModel.isShowingBugTracker) {
BugTrackerView(isPresented: $viewModel.isShowingBugTracker)
}
+ .floatingPanel(isPresented: $viewModel.isShowingRestoreMails) {
+ RestoreEmailsView()
+ }
}
}
@@ -217,6 +215,5 @@ struct MenuDrawerView_Previews: PreviewProvider {
static var previews: some View {
MenuDrawerView(mailboxManager: PreviewHelper.sampleMailboxManager, isCompact: false)
.environmentObject(NavigationDrawerState())
- .environmentObject(GlobalBottomSheet())
}
}
diff --git a/Mail/Views/Menu Drawer/MenuDrawerViewModel.swift b/Mail/Views/Menu Drawer/MenuDrawerViewModel.swift
index d523c65c6..ae1a519f7 100644
--- a/Mail/Views/Menu Drawer/MenuDrawerViewModel.swift
+++ b/Mail/Views/Menu Drawer/MenuDrawerViewModel.swift
@@ -60,8 +60,8 @@ class MenuDrawerViewModel: ObservableObject {
@Published var actionsMenuItems = [MenuItem]()
@Published var isShowingHelp = false
@Published var isShowingBugTracker = false
+ @Published var isShowingRestoreMails = false
- private var bottomSheet: GlobalBottomSheet?
private var foldersObservationToken: NotificationToken?
private var mailboxesObservationToken: NotificationToken?
@@ -103,6 +103,8 @@ class MenuDrawerViewModel: ObservableObject {
break
}
}
+
+ createMenuItems()
}
private func handleFoldersUpdate(_ folders: Results) {
@@ -110,9 +112,7 @@ class MenuDrawerViewModel: ObservableObject {
userFolders = NestableFolder.createFoldersHierarchy(from: Array(folders.where { $0.role == nil }))
}
- func createMenuItems(bottomSheet: GlobalBottomSheet) {
- self.bottomSheet = bottomSheet
-
+ func createMenuItems() {
helpMenuItems = [
MenuItem(icon: MailResourcesAsset.feedback,
label: MailResourcesStrings.Localizable.buttonFeedback,
@@ -159,6 +159,6 @@ class MenuDrawerViewModel: ObservableObject {
}
private func restoreMails() {
- bottomSheet?.open(state: .restoreEmails)
+ isShowingRestoreMails = true
}
}
diff --git a/Mail/Views/Menu Drawer/MenuHeaderView.swift b/Mail/Views/Menu Drawer/MenuHeaderView.swift
index bd141013b..96ae784ba 100644
--- a/Mail/Views/Menu Drawer/MenuHeaderView.swift
+++ b/Mail/Views/Menu Drawer/MenuHeaderView.swift
@@ -53,9 +53,8 @@ struct MenuHeaderView: View {
.clipped()
.shadow(color: MailResourcesAsset.menuDrawerShadowColor.swiftUIColor, radius: 1, x: 0, y: 2)
.sheet(isPresented: $isShowingSettings) {
- SheetView {
- SettingsView()
- }
+ SettingsView()
+ .sheetViewStyle()
}
}
}
diff --git a/Mail/Views/Search/SearchView.swift b/Mail/Views/Search/SearchView.swift
index ef3430a60..8b69b28e9 100644
--- a/Mail/Views/Search/SearchView.swift
+++ b/Mail/Views/Search/SearchView.swift
@@ -23,27 +23,19 @@ import RealmSwift
import SwiftUI
struct SearchView: View {
- @StateObject var viewModel: SearchViewModel
-
@EnvironmentObject var splitViewManager: SplitViewManager
- @EnvironmentObject var globalBottomSheet: GlobalBottomSheet
- @StateObject var bottomSheet: ThreadBottomSheet
+ @StateObject var viewModel: SearchViewModel
@Binding private var editedMessageDraft: Draft?
- @Binding private var messageReply: MessageReply?
let isCompact: Bool
init(mailboxManager: MailboxManager,
folder: Folder,
editedMessageDraft: Binding,
- messageReply: Binding,
isCompact: Bool) {
- let threadBottomSheet = ThreadBottomSheet()
_editedMessageDraft = editedMessageDraft
- _messageReply = messageReply
- _bottomSheet = StateObject(wrappedValue: threadBottomSheet)
_viewModel = StateObject(wrappedValue: SearchViewModel(mailboxManager: mailboxManager, folder: folder))
self.isCompact = isCompact
}
@@ -94,16 +86,6 @@ struct SearchView: View {
.emptyState(isEmpty: viewModel.searchState == .noResults) {
EmptyStateView.emptySearch
}
- .floatingPanel(state: bottomSheet, halfOpening: true) {
- if case .actions(let target) = bottomSheet.state, !target.isInvalidated {
- ActionsView(mailboxManager: viewModel.mailboxManager,
- target: target,
- state: bottomSheet,
- globalSheet: globalBottomSheet) { message, replyMode in
- messageReply = MessageReply(message: message, replyMode: replyMode)
- }
- }
- }
.refreshable {
await viewModel.fetchThreads()
}
@@ -146,7 +128,6 @@ struct SearchView_Previews: PreviewProvider {
SearchView(mailboxManager: PreviewHelper.sampleMailboxManager,
folder: PreviewHelper.sampleFolder,
editedMessageDraft: .constant(nil),
- messageReply: .constant(nil),
isCompact: true)
}
}
diff --git a/Mail/Views/SheetViewModifier.swift b/Mail/Views/SheetViewModifier.swift
new file mode 100644
index 000000000..c538c6fde
--- /dev/null
+++ b/Mail/Views/SheetViewModifier.swift
@@ -0,0 +1,67 @@
+/*
+ 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 .
+ */
+
+import MailCore
+import MailResources
+import SwiftUI
+
+typealias DismissModalAction = () -> Void
+
+struct DismissModalKey: EnvironmentKey {
+ static let defaultValue: DismissModalAction = { /* dismiss nothing by default */ }
+}
+
+extension EnvironmentValues {
+ var dismissModal: DismissModalAction {
+ get {
+ return self[DismissModalKey.self]
+ }
+ set {
+ self[DismissModalKey.self] = newValue
+ }
+ }
+}
+
+extension View {
+ func sheetViewStyle() -> some View {
+ modifier(SheetViewModifier())
+ }
+}
+
+struct SheetViewModifier: ViewModifier {
+ @Environment(\.dismiss) private var dismiss
+
+ func body(content: Content) -> some View {
+ NavigationView {
+ content
+ .environment(\.dismissModal) {
+ dismiss()
+ }
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button {
+ dismiss()
+ } label: {
+ Label(MailResourcesStrings.Localizable.buttonClose, systemImage: "xmark")
+ }
+ }
+ }
+ }
+ .navigationViewStyle(.stack)
+ }
+}
diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift
index 0344e45d7..6c292ce56 100644
--- a/Mail/Views/SplitView.swift
+++ b/Mail/Views/SplitView.swift
@@ -25,32 +25,6 @@ import NavigationBackport
import RealmSwift
import SwiftUI
-struct MailNavigationPathKey: EnvironmentKey {
- static var defaultValue: Binding<[Thread]>?
-}
-
-extension EnvironmentValues {
- var mailNavigationPath: Binding<[Thread]>? {
- get { self[MailNavigationPathKey.self] }
- set { self[MailNavigationPathKey.self] = newValue }
- }
-}
-
-class GlobalBottomSheet: DisplayedFloatingPanelState {
- enum State {
- case getMoreStorage
- case restoreEmails
- case reportJunk(threadBottomSheet: ThreadBottomSheet, target: ActionsTarget)
- }
-}
-
-class GlobalAlert: SheetState {
- enum State {
- case reportPhishing(message: Message)
- case reportDisplayProblem(message: Message)
- }
-}
-
public class SplitViewManager: ObservableObject {
@Published var showSearch = false
@Published var selectedFolder: Folder?
@@ -62,22 +36,20 @@ public class SplitViewManager: ObservableObject {
}
struct SplitView: View {
- var mailboxManager: MailboxManager
- @State var splitViewController: UISplitViewController?
- @StateObject private var navigationDrawerController = NavigationDrawerState()
-
- @Environment(\.horizontalSizeClass) var sizeClass
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
@Environment(\.window) var window
- @StateObject private var bottomSheet = GlobalBottomSheet()
- @StateObject private var alert = GlobalAlert()
+ @State var splitViewController: UISplitViewController?
+ @StateObject private var navigationDrawerController = NavigationDrawerState()
+ @StateObject private var navigationStore = NavigationStore()
@StateObject private var splitViewManager: SplitViewManager
- @State private var path = [Thread]()
- var isCompact: Bool {
- sizeClass == .compact || verticalSizeClass == .compact
+ let mailboxManager: MailboxManager
+
+ private var isCompact: Bool {
+ UIConstants.isCompact(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)
}
init(mailboxManager: MailboxManager) {
@@ -90,7 +62,7 @@ struct SplitView: View {
Group {
if isCompact {
ZStack {
- NBNavigationStack(path: $path) {
+ NBNavigationStack(path: $navigationStore.threadPath) {
ThreadListManagerView(isCompact: isCompact)
.accessibilityHidden(navigationDrawerController.isOpen)
.nbNavigationDestination(for: Thread.self) { thread in
@@ -111,7 +83,7 @@ struct SplitView: View {
ThreadListManagerView(isCompact: isCompact)
- if let thread = path.last {
+ if let thread = navigationStore.threadPath.last {
ThreadView(thread: thread)
} else {
EmptyStateView.emptyThread(from: splitViewManager.selectedFolder)
@@ -119,6 +91,9 @@ struct SplitView: View {
}
}
}
+ .sheet(item: $navigationStore.messageReply) { messageReply in
+ ComposeMessageView.replyOrForwardMessage(messageReply: messageReply, mailboxManager: mailboxManager)
+ }
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
Task {
try await mailboxManager.folders()
@@ -148,41 +123,12 @@ struct SplitView: View {
splitViewManager.splitViewController = splitViewController
setupBehaviour(orientation: interfaceOrientation)
}
- .floatingPanel(state: bottomSheet) {
- switch bottomSheet.state {
- case .getMoreStorage:
- MoreStorageView()
- case .restoreEmails:
- RestoreEmailsView(mailboxManager: mailboxManager)
- case .reportJunk(let threadBottomSheet, let target):
- ReportJunkView(
- mailboxManager: mailboxManager,
- target: target,
- state: threadBottomSheet,
- globalSheet: bottomSheet,
- globalAlert: alert
- )
- case .none:
- EmptyView()
- }
- }
- .customAlert(isPresented: $alert.isShowing) {
- switch alert.state {
- case .reportPhishing(let message):
- ReportPhishingView(message: message)
- case .reportDisplayProblem(let message):
- ReportDisplayProblemView(message: message)
- case .none:
- EmptyView()
- }
- }
- .environment(\.mailNavigationPath, $path)
.environment(\.realmConfiguration, mailboxManager.realmConfiguration)
+ .environment(\.isCompactWindow, horizontalSizeClass == .compact || verticalSizeClass == .compact)
.environmentObject(mailboxManager)
.environmentObject(splitViewManager)
.environmentObject(navigationDrawerController)
- .environmentObject(bottomSheet)
- .environmentObject(alert)
+ .environmentObject(navigationStore)
.defaultAppStorage(.shared)
}
diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift
index e791ba606..97201fd5b 100644
--- a/Mail/Views/Thread List/ThreadListCell.swift
+++ b/Mail/Views/Thread List/ThreadListCell.swift
@@ -25,12 +25,11 @@ import SwiftUI
struct ThreadListCell: View {
@EnvironmentObject var splitViewManager: SplitViewManager
- @Environment(\.mailNavigationPath) private var path
let thread: Thread
- @ObservedObject var viewModel: ThreadListViewModel
- @ObservedObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel
+ let viewModel: ThreadListViewModel
+ let multipleSelectionViewModel: ThreadListMultipleSelectionViewModel
let threadDensity: ThreadDensity
@@ -39,8 +38,6 @@ struct ThreadListCell: View {
@Binding var editedMessageDraft: Draft?
- @State private var shouldNavigateToThreadList = false
-
private var selectionType: SelectionBackgroundKind {
if multipleSelectionViewModel.isEnabled {
return isMultiSelected ? .multiple : .none
@@ -106,8 +103,6 @@ struct ThreadListCell_Previews: PreviewProvider {
thread: PreviewHelper.sampleThread,
viewModel: ThreadListViewModel(mailboxManager: PreviewHelper.sampleMailboxManager,
folder: PreviewHelper.sampleFolder,
- bottomSheet: ThreadBottomSheet(),
- moveSheet: MoveSheet(),
isCompact: false),
multipleSelectionViewModel: ThreadListMultipleSelectionViewModel(mailboxManager: PreviewHelper.sampleMailboxManager),
threadDensity: .large,
diff --git a/Mail/Views/Thread List/ThreadListSwipeAction.swift b/Mail/Views/Thread List/ThreadListSwipeAction.swift
index 0876d41fe..cdab919a1 100644
--- a/Mail/Views/Thread List/ThreadListSwipeAction.swift
+++ b/Mail/Views/Thread List/ThreadListSwipeAction.swift
@@ -24,24 +24,23 @@ import MailResources
import SwiftUI
private struct SwipeActionView: View {
- private let thread: Thread
- private let viewModel: ThreadListViewModel
- private let action: SwipeAction
-
@LazyInjectService private var matomo: MatomoUtils
- init(thread: Thread, viewModel: ThreadListViewModel, action: SwipeAction) {
- self.thread = thread
- self.viewModel = viewModel
- self.action = action.fallback(for: thread) ?? action
- }
+ @EnvironmentObject private var mailboxManager: MailboxManager
+
+ @Binding var moveAction: MoveAction?
+ @Binding var actionsTarget: ActionsTarget?
+
+ let thread: Thread
+ let viewModel: ThreadListViewModel
+ let action: SwipeAction
var body: some View {
Button(role: action.isDestructive ? .destructive : nil) {
matomo.track(eventWithCategory: .swipeActions, name: action.matomoName)
Task {
await tryOrDisplayError {
- try await viewModel.handleSwipeAction(action, thread: thread)
+ try await handleSwipeAction(action, thread: thread)
}
}
} label: {
@@ -50,19 +49,66 @@ private struct SwipeActionView: View {
}
.tint(action.swipeTint)
}
+
+ func handleSwipeAction(_ action: SwipeAction, thread: Thread) async throws {
+ switch action {
+ case .delete:
+ try await mailboxManager.moveOrDelete(threads: [thread])
+ case .archive:
+ try await move(thread: thread, to: .archive)
+ case .readUnread:
+ try await mailboxManager.toggleRead(threads: [thread])
+ case .move:
+ moveAction = MoveAction(fromFolderId: viewModel.folder.id, target: .threads([thread], false))
+ case .favorite:
+ try await mailboxManager.toggleStar(threads: [thread])
+ case .postPone:
+ // TODO: Report action
+ showWorkInProgressSnackBar()
+ case .spam:
+ try await toggleSpam(thread: thread)
+ case .quickAction:
+ actionsTarget = .threads([thread.thaw() ?? thread], false)
+ case .none:
+ break
+ case .moveToInbox:
+ try await move(thread: thread, to: .inbox)
+ }
+ }
+
+ private func toggleSpam(thread: Thread) async throws {
+ let destination: FolderRole = viewModel.folder.role == .spam ? .inbox : .spam
+ try await move(thread: thread, to: destination)
+ }
+
+ private func move(thread: Thread, to folderRole: FolderRole) async throws {
+ guard let folder = mailboxManager.getFolder(with: folderRole)?.freeze() else { return }
+ try await move(thread: thread, to: folder)
+ }
+
+ private func move(thread: Thread, to folder: Folder) async throws {
+ let response = try await mailboxManager.move(threads: [thread], to: folder)
+ IKSnackBar.showCancelableSnackBar(message: MailResourcesStrings.Localizable.snackbarThreadMoved(folder.localizedName),
+ cancelSuccessMessage: MailResourcesStrings.Localizable.snackbarMoveCancelled,
+ undoRedoAction: response,
+ mailboxManager: mailboxManager)
+ }
}
struct ThreadListSwipeActions: ViewModifier {
- let thread: Thread
- let viewModel: ThreadListViewModel
- let multipleSelectionViewModel: ThreadListMultipleSelectionViewModel
-
@AppStorage(UserDefaults.shared.key(.swipeFullLeading)) private var swipeFullLeading = DefaultPreferences.swipeFullLeading
@AppStorage(UserDefaults.shared.key(.swipeLeading)) private var swipeLeading = DefaultPreferences.swipeLeading
@AppStorage(UserDefaults.shared.key(.swipeFullTrailing)) private var swipeFullTrailing = DefaultPreferences.swipeFullTrailing
@AppStorage(UserDefaults.shared.key(.swipeTrailing)) private var swipeTrailing = DefaultPreferences.swipeTrailing
+ @State private var moveAction: MoveAction?
+ @State private var actionsTarget: ActionsTarget?
+
+ let thread: Thread
+ let viewModel: ThreadListViewModel
+ let multipleSelectionViewModel: ThreadListMultipleSelectionViewModel
+
func body(content: Content) -> some View {
if viewModel.folder.role == .draft {
content
@@ -77,6 +123,11 @@ struct ThreadListSwipeActions: ViewModifier {
.swipeActions(edge: .trailing) {
edgeActions([swipeFullTrailing, swipeTrailing])
}
+ .actionsPanel(actionsTarget: $actionsTarget)
+ .sheet(item: $moveAction) { moveAction in
+ MoveEmailView(moveAction: moveAction)
+ .sheetViewStyle()
+ }
}
}
@@ -84,7 +135,11 @@ struct ThreadListSwipeActions: ViewModifier {
private func edgeActions(_ actions: [SwipeAction]) -> some View {
if !multipleSelectionViewModel.isEnabled {
ForEach(actions.filter { $0 != .none }, id: \.rawValue) { action in
- SwipeActionView(thread: thread, viewModel: viewModel, action: action)
+ SwipeActionView(moveAction: $moveAction,
+ actionsTarget: $actionsTarget,
+ thread: thread,
+ viewModel: viewModel,
+ action: action)
}
}
}
@@ -102,11 +157,11 @@ extension View {
struct ThreadListSwipeAction_Previews: PreviewProvider {
static var previews: some View {
- SwipeActionView(thread: PreviewHelper.sampleThread,
+ SwipeActionView(moveAction: .constant(nil),
+ actionsTarget: .constant(nil),
+ thread: PreviewHelper.sampleThread,
viewModel: ThreadListViewModel(mailboxManager: PreviewHelper.sampleMailboxManager,
folder: PreviewHelper.sampleFolder,
- bottomSheet: ThreadBottomSheet(),
- moveSheet: MoveSheet(),
isCompact: false),
action: .delete)
}
diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift
index f91aaf5a8..d16fed9c7 100644
--- a/Mail/Views/Thread List/ThreadListView.swift
+++ b/Mail/Views/Thread List/ThreadListView.swift
@@ -24,18 +24,6 @@ import MailResources
import RealmSwift
import SwiftUI
-class ThreadBottomSheet: DisplayedFloatingPanelState {
- enum State: Equatable {
- case actions(ActionsTarget)
- }
-}
-
-class MoveSheet: SheetState {
- enum State {
- case move(folderId: String?, moveHandler: MoveEmailView.MoveHandler)
- }
-}
-
class FlushAlertState: Identifiable {
let id = UUID()
let deletedMessages: Int?
@@ -52,15 +40,12 @@ struct ThreadListView: View {
@StateObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel
@EnvironmentObject var splitViewManager: SplitViewManager
- @EnvironmentObject var globalBottomSheet: GlobalBottomSheet
- @Environment(\.mailNavigationPath) private var path
+ @EnvironmentObject var navigationStore: NavigationStore
@AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity
@AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
@State private var isShowingComposeNewMessageView = false
- @StateObject var bottomSheet: ThreadBottomSheet
- @StateObject var moveSheet: MoveSheet
@StateObject private var networkMonitor = NetworkMonitor()
@Binding private var editedMessageDraft: Draft?
@Binding private var messageReply: MessageReply?
@@ -86,16 +71,10 @@ struct ThreadListView: View {
editedMessageDraft: Binding,
messageReply: Binding,
isCompact: Bool) {
- let threadBottomSheet = ThreadBottomSheet()
- let moveEmailSheet = MoveSheet()
_editedMessageDraft = editedMessageDraft
_messageReply = messageReply
- _bottomSheet = StateObject(wrappedValue: threadBottomSheet)
- _moveSheet = StateObject(wrappedValue: moveEmailSheet)
_viewModel = StateObject(wrappedValue: ThreadListViewModel(mailboxManager: mailboxManager,
folder: folder,
- bottomSheet: threadBottomSheet,
- moveSheet: moveEmailSheet,
isCompact: isCompact))
_multipleSelectionViewModel =
StateObject(wrappedValue: ThreadListMultipleSelectionViewModel(mailboxManager: mailboxManager))
@@ -200,7 +179,6 @@ struct ThreadListView: View {
}
.modifier(ThreadListToolbar(isCompact: isCompact,
flushAlert: $flushAlert,
- bottomSheet: bottomSheet,
viewModel: viewModel,
multipleSelectionViewModel: multipleSelectionViewModel) {
withAnimation(.default.speed(2)) {
@@ -213,22 +191,8 @@ struct ThreadListView: View {
matomo.track(eventWithCategory: .newMessage, name: "openFromFab")
isShowingComposeNewMessageView.toggle()
}
- .floatingPanel(state: bottomSheet, halfOpening: true) {
- if case let .actions(target) = bottomSheet.state, !target.isInvalidated {
- ActionsView(mailboxManager: viewModel.mailboxManager,
- target: target,
- state: bottomSheet,
- globalSheet: globalBottomSheet, moveSheet: moveSheet) { message, replyMode in
- messageReply = MessageReply(message: message, replyMode: replyMode)
- } completionHandler: {
- bottomSheet.close()
- multipleSelectionViewModel.isEnabled = false
- }
- }
- }
.onAppear {
networkMonitor.start()
- viewModel.globalBottomSheet = globalBottomSheet
viewModel.selectedThread = nil
}
.onChange(of: splitViewManager.selectedFolder) { newFolder in
@@ -236,9 +200,9 @@ struct ThreadListView: View {
}
.onChange(of: viewModel.selectedThread) { newThread in
if let newThread {
- path?.wrappedValue = [newThread]
+ navigationStore.threadPath = [newThread]
} else {
- path?.wrappedValue = []
+ navigationStore.threadPath = []
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
@@ -253,11 +217,6 @@ struct ThreadListView: View {
.sheet(isPresented: $isShowingComposeNewMessageView) {
ComposeMessageView.newMessage(mailboxManager: viewModel.mailboxManager)
}
- .sheet(isPresented: $moveSheet.isShowing) {
- if case let .move(folderId, handler) = moveSheet.state {
- MoveEmailView.sheetView(mailboxManager: viewModel.mailboxManager, from: folderId, moveHandler: handler)
- }
- }
.customAlert(item: $flushAlert) { item in
FlushFolderAlertView(flushAlert: item, folder: viewModel.folder)
}
@@ -294,11 +253,11 @@ private struct ThreadListToolbar: ViewModifier {
@Binding var flushAlert: FlushAlertState?
- @ObservedObject var bottomSheet: ThreadBottomSheet
@ObservedObject var viewModel: ThreadListViewModel
@ObservedObject var multipleSelectionViewModel: ThreadListMultipleSelectionViewModel
@State private var isShowingSwitchAccount = false
+ @State private var multipleSelectionActionsTarget: ActionsTarget?
@EnvironmentObject var splitViewManager: SplitViewManager
@EnvironmentObject var navigationDrawerState: NavigationDrawerState
@@ -389,16 +348,22 @@ private struct ThreadListToolbar: ViewModifier {
.disabled(action == .archive && splitViewManager.selectedFolder?.role == .archive)
}
- ToolbarButton(text: MailResourcesStrings.Localizable.buttonMore,
- icon: MailResourcesAsset.plusActions.swiftUIImage) {
- bottomSheet
- .open(state: .actions(.threads(Array(multipleSelectionViewModel.selectedItems), true)))
+ ToolbarButton(
+ text: MailResourcesStrings.Localizable.buttonMore,
+ icon: MailResourcesAsset.plusActions.swiftUIImage
+ ) {
+ multipleSelectionActionsTarget = .threads(Array(multipleSelectionViewModel.selectedItems), true)
}
}
.disabled(multipleSelectionViewModel.selectedItems.isEmpty)
}
}
}
+ .actionsPanel(actionsTarget: $multipleSelectionActionsTarget) {
+ withAnimation {
+ multipleSelectionViewModel.isEnabled = false
+ }
+ }
.navigationTitle(
multipleSelectionViewModel.isEnabled
? MailResourcesStrings.Localizable.multipleSelectionCount(multipleSelectionViewModel.selectedItems.count)
diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift
index 5f3fe1bf7..85f170636 100644
--- a/Mail/Views/Thread List/ThreadListViewModel.swift
+++ b/Mail/Views/Thread List/ThreadListViewModel.swift
@@ -110,10 +110,6 @@ class DateSection: Identifiable {
}
}
- let moveSheet: MoveSheet
- let bottomSheet: ThreadBottomSheet
- var globalBottomSheet: GlobalBottomSheet?
-
var scrollViewProxy: ScrollViewProxy?
var isCompact: Bool
@@ -148,15 +144,11 @@ class DateSection: Identifiable {
init(
mailboxManager: MailboxManager,
folder: Folder,
- bottomSheet: ThreadBottomSheet,
- moveSheet: MoveSheet,
isCompact: Bool
) {
self.mailboxManager = mailboxManager
self.folder = folder
lastUpdate = folder.lastUpdate
- self.bottomSheet = bottomSheet
- self.moveSheet = moveSheet
self.isCompact = isCompact
observeChanges()
}
@@ -287,55 +279,4 @@ class DateSection: Identifiable {
return newSections
}
}
-
- // MARK: - Swipe actions
-
- func handleSwipeAction(_ action: SwipeAction, thread: Thread) async throws {
- switch action {
- case .delete:
- try await mailboxManager.moveOrDelete(threads: [thread])
- case .archive:
- try await move(thread: thread, to: .archive)
- case .readUnread:
- try await mailboxManager.toggleRead(threads: [thread])
- case .move:
- moveSheet.state = .move(folderId: folder.id) { folder in
- guard thread.folder != folder else { return }
- Task {
- try await self.move(thread: thread, to: folder)
- }
- }
- case .favorite:
- try await mailboxManager.toggleStar(threads: [thread])
- case .postPone:
- // TODO: Report action
- showWorkInProgressSnackBar()
- case .spam:
- try await toggleSpam(thread: thread)
- case .quickAction:
- bottomSheet.open(state: .actions(.threads([thread.thaw() ?? thread], false)))
- case .none:
- break
- case .moveToInbox:
- try await move(thread: thread, to: .inbox)
- }
- }
-
- private func toggleSpam(thread: Thread) async throws {
- let destination: FolderRole = folder.role == .spam ? .inbox : .spam
- try await move(thread: thread, to: destination)
- }
-
- private func move(thread: Thread, to folderRole: FolderRole) async throws {
- guard let folder = mailboxManager.getFolder(with: folderRole)?.freeze() else { return }
- try await move(thread: thread, to: folder)
- }
-
- private func move(thread: Thread, to folder: Folder) async throws {
- let response = try await mailboxManager.move(threads: [thread], to: folder)
- IKSnackBar.showCancelableSnackBar(message: MailResourcesStrings.Localizable.snackbarThreadMoved(folder.localizedName),
- cancelSuccessMessage: MailResourcesStrings.Localizable.snackbarMoveCancelled,
- undoRedoAction: response,
- mailboxManager: mailboxManager)
- }
}
diff --git a/Mail/Views/Thread/MessageHeaderDetailView.swift b/Mail/Views/Thread/MessageHeaderDetailView.swift
index 0f4606c87..58a26abdd 100644
--- a/Mail/Views/Thread/MessageHeaderDetailView.swift
+++ b/Mail/Views/Thread/MessageHeaderDetailView.swift
@@ -44,7 +44,6 @@ struct ViewGeometry: View {
struct MessageHeaderDetailView: View {
@ObservedRealmObject var message: Message
- let recipientTapped: (Recipient) -> Void
@State private var labelWidth: CGFloat = 100
@@ -53,29 +52,25 @@ struct MessageHeaderDetailView: View {
RecipientLabel(
labelWidth: $labelWidth,
title: MailResourcesStrings.Localizable.fromTitle,
- recipients: message.from,
- recipientTapped: recipientTapped
+ recipients: message.from
)
RecipientLabel(
labelWidth: $labelWidth,
title: MailResourcesStrings.Localizable.toTitle,
- recipients: message.to,
- recipientTapped: recipientTapped
+ recipients: message.to
)
if !message.cc.isEmpty {
RecipientLabel(
labelWidth: $labelWidth,
title: MailResourcesStrings.Localizable.ccTitle,
- recipients: message.cc,
- recipientTapped: recipientTapped
+ recipients: message.cc
)
}
if !message.bcc.isEmpty {
RecipientLabel(
labelWidth: $labelWidth,
title: MailResourcesStrings.Localizable.bccTitle,
- recipients: message.bcc,
- recipientTapped: recipientTapped
+ recipients: message.bcc
)
}
HStack {
@@ -97,7 +92,7 @@ struct MessageHeaderDetailView: View {
struct MessageHeaderDetailView_Previews: PreviewProvider {
static var previews: some View {
- MessageHeaderDetailView(message: PreviewHelper.sampleMessage) { _ in /* Preview */ }
+ MessageHeaderDetailView(message: PreviewHelper.sampleMessage)
}
}
@@ -105,7 +100,8 @@ struct RecipientLabel: View {
@Binding var labelWidth: CGFloat
let title: String
let recipients: RealmSwift.List
- let recipientTapped: (Recipient) -> Void
+
+ @State private var contactViewRecipient: Recipient?
@LazyInjectService private var matomo: MatomoUtils
@@ -120,13 +116,17 @@ struct RecipientLabel: View {
WrappingHStack(lineSpacing: 2) {
Button {
matomo.track(eventWithCategory: .message, name: "selectRecipient")
- recipientTapped(recipient)
+ contactViewRecipient = recipient
} label: {
Text(recipient.name.isEmpty ? recipient.email : recipient.name)
.textStyle(.bodySmallAccent)
.lineLimit(1)
.layoutPriority(1)
}
+ .adaptivePanel(item: $contactViewRecipient) { recipient in
+ ContactActionsView(recipient: recipient)
+ }
+
if !recipient.name.isEmpty {
Text(recipient.email)
.textStyle(.labelSecondary)
diff --git a/Mail/Views/Thread/MessageHeaderSummaryView.swift b/Mail/Views/Thread/MessageHeaderSummaryView.swift
index a4d0365de..b652537d1 100644
--- a/Mail/Views/Thread/MessageHeaderSummaryView.swift
+++ b/Mail/Views/Thread/MessageHeaderSummaryView.swift
@@ -25,13 +25,18 @@ import RealmSwift
import SwiftUI
struct MessageHeaderSummaryView: View {
+ @EnvironmentObject private var mailboxManager: MailboxManager
+ @EnvironmentObject private var navigationStore: NavigationStore
+
@ObservedRealmObject var message: Message
+
+ @State private var replyOrReplyAllMessage: Message?
+ @State private var contactViewRecipient: Recipient?
+
@Binding var isMessageExpanded: Bool
@Binding var isHeaderExpanded: Bool
+
let deleteDraftTapped: () -> Void
- let replyButtonTapped: () -> Void
- let moreButtonTapped: () -> Void
- let recipientTapped: (Recipient) -> Void
@LazyInjectService private var matomo: MatomoUtils
@@ -41,10 +46,13 @@ struct MessageHeaderSummaryView: View {
if let recipient = message.from.first {
Button {
matomo.track(eventWithCategory: .message, name: "selectAvatar")
- recipientTapped(recipient)
+ contactViewRecipient = recipient
} label: {
AvatarView(avatarDisplayable: recipient, size: 40)
}
+ .adaptivePanel(item: $contactViewRecipient) { recipient in
+ ContactActionsView(recipient: recipient)
+ }
}
VStack(alignment: .leading, spacing: 4) {
@@ -101,13 +109,26 @@ struct MessageHeaderSummaryView: View {
if isMessageExpanded {
HStack(spacing: 20) {
- Button(action: replyButtonTapped) {
+ Button {
+ matomo.track(eventWithCategory: .messageActions, name: "reply")
+ if message.canReplyAll {
+ replyOrReplyAllMessage = message
+ } else {
+ navigationStore.messageReply = MessageReply(message: message, replyMode: .reply)
+ }
+
+ } label: {
MailResourcesAsset.emailActionReply.swiftUIImage
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
}
- Button(action: moreButtonTapped) {
+ .adaptivePanel(item: $replyOrReplyAllMessage) { message in
+ ReplyActionsView(mailboxManager: mailboxManager,
+ message: message,
+ messageReply: $navigationStore.messageReply)
+ }
+ ActionsPanelButton(message: message) {
MailResourcesAsset.plusActions.swiftUIImage
.resizable()
.scaledToFit()
@@ -127,23 +148,11 @@ struct MessageHeaderSummaryView_Previews: PreviewProvider {
isMessageExpanded: .constant(false),
isHeaderExpanded: .constant(false)) {
// Preview
- } replyButtonTapped: {
- // Preview
- } moreButtonTapped: {
- // Preview
- } recipientTapped: { _ in
- // Preview
}
MessageHeaderSummaryView(message: PreviewHelper.sampleMessage,
isMessageExpanded: .constant(true),
isHeaderExpanded: .constant(false)) {
// Preview
- } replyButtonTapped: {
- // Preview
- } moreButtonTapped: {
- // Preview
- } recipientTapped: { _ in
- // Preview
}
}
.previewLayout(.sizeThatFits)
diff --git a/Mail/Views/Thread/MessageHeaderView.swift b/Mail/Views/Thread/MessageHeaderView.swift
index 916909028..0d6eec308 100644
--- a/Mail/Views/Thread/MessageHeaderView.swift
+++ b/Mail/Views/Thread/MessageHeaderView.swift
@@ -25,38 +25,27 @@ import RealmSwift
import SwiftUI
struct MessageHeaderView: View {
+ @LazyInjectService private var matomo: MatomoUtils
+
+ @EnvironmentObject private var navigationStore: NavigationStore
+ @EnvironmentObject private var mailboxManager: MailboxManager
+
@State private var editedDraft: Draft?
- @State var messageReply: MessageReply?
+
@ObservedRealmObject var message: Message
+
@Binding var isHeaderExpanded: Bool
@Binding var isMessageExpanded: Bool
- @EnvironmentObject var mailboxManager: MailboxManager
- @EnvironmentObject var bottomSheet: MessageBottomSheet
- @EnvironmentObject var threadBottomSheet: ThreadBottomSheet
-
- @LazyInjectService private var matomo: MatomoUtils
-
var body: some View {
VStack(spacing: 12) {
MessageHeaderSummaryView(message: message,
isMessageExpanded: $isMessageExpanded,
isHeaderExpanded: $isHeaderExpanded,
- deleteDraftTapped: deleteDraft) {
- matomo.track(eventWithCategory: .messageActions, name: "reply")
- if message.canReplyAll {
- bottomSheet.open(state: .replyOption(message, isThread: false))
- } else {
- messageReply = MessageReply(message: message, replyMode: .reply)
- }
- } moreButtonTapped: {
- threadBottomSheet.open(state: .actions(.message(message.thaw() ?? message)))
- } recipientTapped: { recipient in
- openContact(recipient: recipient)
- }
+ deleteDraftTapped: deleteDraft)
if isHeaderExpanded {
- MessageHeaderDetailView(message: message, recipientTapped: openContact(recipient:))
+ MessageHeaderDetailView(message: message)
}
}
.contentShape(Rectangle())
@@ -74,16 +63,6 @@ struct MessageHeaderView: View {
.sheet(item: $editedDraft) { editedDraft in
ComposeMessageView.editDraft(draft: editedDraft, mailboxManager: mailboxManager)
}
- .sheet(item: $messageReply) { messageReply in
- ComposeMessageView.replyOrForwardMessage(messageReply: messageReply, mailboxManager: mailboxManager)
- }
- }
-
- private func openContact(recipient: Recipient) {
- let isRemoteContact = AccountManager.instance.currentContactManager?.getContact(for: recipient)?.remote != nil
- bottomSheet.open(
- state: .contact(recipient, isRemote: isRemoteContact)
- )
}
private func deleteDraft() {
diff --git a/Mail/Views/Thread/MoveEmailView.swift b/Mail/Views/Thread/MoveEmailView.swift
index f2384bd2b..b91fe5da1 100644
--- a/Mail/Views/Thread/MoveEmailView.swift
+++ b/Mail/Views/Thread/MoveEmailView.swift
@@ -24,19 +24,29 @@ import MailResources
import RealmSwift
import SwiftUI
+struct MoveAction: Identifiable {
+ var id: String {
+ return "\(target.id)\(fromFolderId ?? "")"
+ }
+
+ let fromFolderId: String?
+ let target: ActionsTarget
+}
+
struct MoveEmailView: View {
- typealias MoveHandler = (Folder) -> Void
+ @LazyInjectService private var matomo: MatomoUtils
+
+ @Environment(\.dismissModal) var dismissModal
@EnvironmentObject private var mailboxManager: MailboxManager
+ typealias MoveHandler = (Folder) -> Void
+
// swiftlint:disable empty_count
@ObservedResults(Folder.self, where: { $0.role != .draft && $0.parents.count == 0 && $0.toolType == nil }) var folders
@State private var isShowingCreateFolderAlert = false
- @LazyInjectService private var matomo: MatomoUtils
-
- let currentFolderId: String?
- let moveHandler: MoveEmailView.MoveHandler
+ let moveAction: MoveAction
var body: some View {
ScrollView {
@@ -63,32 +73,31 @@ struct MoveEmailView: View {
.environment(\.folderCellType, .indicator)
.matomoView(view: ["MoveEmailView"])
.customAlert(isPresented: $isShowingCreateFolderAlert) {
- CreateFolderView(mode: .move(moveHandler: moveHandler))
+ CreateFolderView(mode: .move { newFolder in
+ Task {
+ try await ActionUtils(actionsTarget: moveAction.target, mailboxManager: mailboxManager).move(to: newFolder)
+ }
+ dismissModal()
+ })
}
}
private func listOfFolders(nestableFolders: [NestableFolder]) -> some View {
ForEach(nestableFolders) { nestableFolder in
- FolderCell(folder: nestableFolder, currentFolderId: currentFolderId) { folder in
- moveHandler(folder)
- NotificationCenter.default.post(Notification(name: Constants.dismissMoveSheetNotificationName))
+ FolderCell(folder: nestableFolder, currentFolderId: moveAction.fromFolderId) { folder in
+ Task {
+ try await ActionUtils(actionsTarget: moveAction.target, mailboxManager: mailboxManager).move(to: folder)
+ }
+ dismissModal()
}
}
}
}
-extension MoveEmailView {
- static func sheetView(mailboxManager: MailboxManager, from folderId: String?,
- moveHandler: @escaping MoveEmailView.MoveHandler) -> some View {
- SheetView {
- MoveEmailView(currentFolderId: folderId, moveHandler: moveHandler)
- }
- }
-}
-
struct MoveMessageView_Previews: PreviewProvider {
static var previews: some View {
- MoveEmailView(currentFolderId: nil) { _ in /* Preview */ }
+ MoveEmailView(moveAction: MoveAction(fromFolderId: PreviewHelper.sampleFolder.id,
+ target: .message(PreviewHelper.sampleMessage)))
.environmentObject(PreviewHelper.sampleMailboxManager)
}
}
diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift
index 479462b98..f85c8562c 100644
--- a/Mail/Views/Thread/ThreadView.swift
+++ b/Mail/Views/Thread/ThreadView.swift
@@ -32,41 +32,21 @@ private struct ScrollOffsetPreferenceKey: PreferenceKey {
}
}
-class MessageBottomSheet: DisplayedFloatingPanelState {
- enum State: Equatable {
- case contact(Recipient, isRemote: Bool)
- case replyOption(Message, isThread: Bool)
- }
-}
-
struct ThreadView: View {
+ @LazyInjectService private var matomo: MatomoUtils
+
+ @Environment(\.isCompactWindow) private var isCompactWindow
+ @Environment(\.dismiss) private var dismiss
+
@EnvironmentObject private var splitViewManager: SplitViewManager
- @Environment(\.mailNavigationPath) private var path
@EnvironmentObject private var mailboxManager: MailboxManager
-
- @ObservedRealmObject var thread: Thread
+ @EnvironmentObject private var navigationStore: NavigationStore
@State private var headerHeight: CGFloat = 0
@State private var displayNavigationTitle = false
- @State private var messageReply: MessageReply?
-
- @StateObject private var moveSheet = MoveSheet()
- @StateObject private var bottomSheet = MessageBottomSheet()
- @StateObject private var threadBottomSheet = ThreadBottomSheet()
-
- @State private var showEmptyView = false
-
- @EnvironmentObject var globalBottomSheet: GlobalBottomSheet
- @EnvironmentObject var globalAlert: GlobalAlert
- @Environment(\.horizontalSizeClass) private var sizeClass
- @Environment(\.verticalSizeClass) private var verticalSizeClass
- @Environment(\.dismiss) var dismiss
+ @State private var replyOrReplyAllMessage: Message?
- var isCompact: Bool {
- sizeClass == .compact || verticalSizeClass == .compact
- }
-
- @LazyInjectService private var matomo: MatomoUtils
+ @ObservedRealmObject var thread: Thread
private let toolbarActions: [Action] = [.reply, .forward, .archive, .delete]
@@ -122,70 +102,37 @@ struct ThreadView: View {
}
ToolbarItemGroup(placement: .bottomBar) {
ForEach(toolbarActions) { action in
- ToolbarButton(text: action.title, icon: action.icon) {
- didTap(action: action)
+ if action == .reply {
+ ToolbarButton(text: action.title, icon: action.icon) {
+ didTap(action: action)
+ }
+ .adaptivePanel(item: $replyOrReplyAllMessage) { message in
+ ReplyActionsView(
+ mailboxManager: mailboxManager,
+ message: message,
+ messageReply: $navigationStore.messageReply
+ )
+ }
+ } else {
+ ToolbarButton(text: action.title, icon: action.icon) {
+ didTap(action: action)
+ }
+ .disabled(action == .archive && thread.folder?.role == .archive)
}
- .disabled(action == .archive && thread.folder?.role == .archive)
Spacer()
}
- ToolbarButton(text: MailResourcesStrings.Localizable.buttonMore,
- icon: MailResourcesAsset.plusActions.swiftUIImage) {
- threadBottomSheet.open(state: .actions(.threads([thread.thaw() ?? thread], false)))
- }
- }
- }
- .environmentObject(mailboxManager)
- .environmentObject(bottomSheet)
- .environmentObject(threadBottomSheet)
- .sheet(item: $messageReply) { messageReply in
- ComposeMessageView.replyOrForwardMessage(messageReply: messageReply, mailboxManager: mailboxManager)
- }
- .sheet(isPresented: $moveSheet.isShowing) {
- if case .move(let folderId, let handler) = moveSheet.state {
- MoveEmailView.sheetView(mailboxManager: mailboxManager, from: folderId, moveHandler: handler)
- }
- }
- .floatingPanel(state: bottomSheet) {
- switch bottomSheet.state {
- case .contact(let recipient, let isRemote):
- ContactActionsView(
- recipient: recipient,
- isRemoteContact: isRemote,
- bottomSheet: bottomSheet,
- mailboxManager: mailboxManager
- )
- case .replyOption(let message, let isThread):
- ReplyActionsView(
- mailboxManager: mailboxManager,
- target: isThread ? .threads([thread], false) : .message(message),
- state: threadBottomSheet,
- globalSheet: globalBottomSheet
- ) { message, replyMode in
- bottomSheet.close()
- messageReply = MessageReply(message: message, replyMode: replyMode)
- }
- case .none:
- EmptyView()
- }
- }
- .floatingPanel(state: threadBottomSheet, halfOpening: true) {
- if case .actions(let target) = threadBottomSheet.state, !target.isInvalidated {
- ActionsView(mailboxManager: mailboxManager,
- target: target,
- state: threadBottomSheet,
- globalSheet: globalBottomSheet,
- globalAlert: globalAlert,
- moveSheet: moveSheet) { message, replyMode in
- messageReply = MessageReply(message: message, replyMode: replyMode)
+ ActionsPanelButton(threads: [thread]) {
+ ToolbarButtonLabel(text: MailResourcesStrings.Localizable.buttonMore,
+ icon: MailResourcesAsset.plusActions.swiftUIImage)
}
}
}
.onChange(of: thread.messages) { newMessagesList in
if newMessagesList.isEmpty || thread.messageInFolderCount == 0 {
- if isCompact {
+ if isCompactWindow {
dismiss() // For iPhone
} else {
- path?.wrappedValue = [] // For iPad
+ navigationStore.threadPath = [] // For iPad
}
}
}
@@ -200,13 +147,13 @@ struct ThreadView: View {
case .reply:
guard let message = thread.messages.last else { return }
if message.canReplyAll {
- bottomSheet.open(state: .replyOption(message, isThread: true))
+ replyOrReplyAllMessage = message
} else {
- messageReply = MessageReply(message: message, replyMode: .reply)
+ navigationStore.messageReply = MessageReply(message: message, replyMode: .reply)
}
case .forward:
guard let message = thread.messages.last else { return }
- messageReply = MessageReply(message: message, replyMode: .forward)
+ navigationStore.messageReply = MessageReply(message: message, replyMode: .forward)
case .archive:
Task {
await tryOrDisplayError {
diff --git a/Mail/Views/ThreadListManagerView.swift b/Mail/Views/ThreadListManagerView.swift
index 9a872e0fb..6e663c797 100644
--- a/Mail/Views/ThreadListManagerView.swift
+++ b/Mail/Views/ThreadListManagerView.swift
@@ -48,7 +48,6 @@ struct ThreadListManagerView: View {
mailboxManager: mailboxManager,
folder: selectedFolder,
editedMessageDraft: $editedMessageDraft,
- messageReply: $messageReply,
isCompact: isCompact
)
} else {
@@ -80,9 +79,6 @@ struct ThreadListManagerView: View {
.sheet(item: $editedMessageDraft) { draft in
ComposeMessageView.editDraft(draft: draft, mailboxManager: mailboxManager)
}
- .sheet(item: $messageReply) { messageReply in
- ComposeMessageView.replyOrForwardMessage(messageReply: messageReply, mailboxManager: mailboxManager)
- }
}
}
diff --git a/MailCore/UI/UIConstants.swift b/MailCore/UI/UIConstants.swift
index 3881adc45..038036d45 100644
--- a/MailCore/UI/UIConstants.swift
+++ b/MailCore/UI/UIConstants.swift
@@ -18,6 +18,7 @@
import Foundation
import MailResources
+import SwiftUI
import UIKit
public enum BarAppearanceConstants {
@@ -98,4 +99,8 @@ public enum UIConstants {
public static let componentsMaxWidth: CGFloat = 496
public static let chipInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
+
+ public static func isCompact(horizontalSizeClass: UserInterfaceSizeClass?, verticalSizeClass: UserInterfaceSizeClass?) -> Bool {
+ return horizontalSizeClass == .compact || verticalSizeClass == .compact
+ }
}
diff --git a/Project.swift b/Project.swift
index b48e01952..95eb35404 100644
--- a/Project.swift
+++ b/Project.swift
@@ -44,12 +44,12 @@ let project = Project(name: "Mail",
.package(url: "https://github.com/Ambrdctr/SQRichTextEditor", .branch("master")),
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", .upToNextMajor(from: "1.0.1")),
.package(url: "https://github.com/dkk/WrappingHStack", .upToNextMajor(from: "2.0.0")),
- .package(url: "https://github.com/SCENEE/FloatingPanel", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.0.0")),
.package(url: "https://github.com/airbnb/lottie-ios", .exact("3.5.0")),
.package(url: "https://github.com/valentinperignon/SwiftSoup", .branch("try-headcleaner")),
.package(url: "https://github.com/johnpatrickmorgan/NavigationBackport", .upToNextMajor(from: "0.7.2")),
- .package(url: "https://github.com/aheze/Popovers", .upToNextMajor(from: "1.3.2"))
+ .package(url: "https://github.com/aheze/Popovers", .upToNextMajor(from: "1.3.2")),
+ .package(url: "https://github.com/shaps80/SwiftUIBackports", .upToNextMajor(from: "1.15.1"))
],
targets: [
Target(name: "Mail",
@@ -78,10 +78,10 @@ let project = Project(name: "Mail",
.package(product: "SQRichTextEditor"),
.package(product: "Shimmer"),
.package(product: "WrappingHStack"),
- .package(product: "FloatingPanel"),
.package(product: "Lottie"),
.package(product: "NavigationBackport"),
- .package(product: "Popovers")
+ .package(product: "Popovers"),
+ .package(product: "SwiftUIBackports")
],
settings: .settings(base: baseSettings),
environment: ["hostname": "\(ProcessInfo.processInfo.hostName)."]),