Skip to content

Commit 9842749

Browse files
feat(Snooze): Snooze actions (#3) (#1736)
2 parents 741af47 + f1ae762 commit 9842749

25 files changed

+902
-44
lines changed

Mail/Components/ActionsPanelButton.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ struct ActionsPanelButton<Content: View>: View {
4242
panelSource: panelSource,
4343
popoverArrowEdge: popoverArrowEdge
4444
) { action in
45-
if action == .markAsUnread {
45+
if action == .markAsUnread || action == .snooze || action == .modifySnooze {
4646
dismiss()
4747
}
4848
}

Mail/Views/Bottom sheets/Actions/ActionsPanelViewModifier.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct ActionsPanelViewModifier: ViewModifier {
5353
@ModalState private var messagesToMove: [Message]?
5454
@ModalState private var flushAlert: FlushAlertState?
5555
@ModalState private var shareMailLink: ShareMailLinkResult?
56+
@ModalState private var messagesToSnooze: [Message]?
5657
@ModalState private var messagesToDownload: [Message]?
5758

5859
@Binding var messages: [Message]?
@@ -73,10 +74,20 @@ struct ActionsPanelViewModifier: ViewModifier {
7374
nearestReportedForPhishingMessagesAlert: $reportedForPhishingMessages,
7475
nearestReportedForDisplayProblemMessageAlert: $reportedForDisplayProblemMessage,
7576
nearestShareMailLinkPanel: $shareMailLink,
77+
nearestMessagesToSnooze: $messagesToSnooze,
7678
messagesToDownload: $messagesToDownload
7779
)
7880
}
7981

82+
private var initialSnoozedDate: Date? {
83+
guard let messages,
84+
let initialDate = messages.first?.snoozeEndDate,
85+
messages.allSatisfy({ $0.isSnoozed && $0.snoozeEndDate == initialDate })
86+
else { return nil }
87+
88+
return initialDate
89+
}
90+
8091
func body(content: Content) -> some View {
8192
content.adaptivePanel(item: $messages, popoverArrowEdge: popoverArrowEdge) { messages in
8293
ActionsView(
@@ -136,5 +147,10 @@ struct ActionsPanelViewModifier: ViewModifier {
136147
.backport.presentationDetents([.medium, .large])
137148
}
138149
}
150+
.snoozedFloatingPanel(
151+
messages: messagesToSnooze,
152+
initialDate: initialSnoozedDate,
153+
folder: originFolder?.freezeIfNeeded()
154+
) { completionHandler?($0) }
139155
}
140156
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Infomaniak Mail - iOS App
3+
Copyright (C) 2025 Infomaniak Network SA
4+
5+
This program is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import MailCore
20+
import SwiftUI
21+
22+
extension View {
23+
func snoozedFloatingPanel(
24+
messages: [Message]?,
25+
initialDate: Date?,
26+
folder: Folder?,
27+
completionHandler: ((Action) -> Void)? = nil
28+
) -> some View {
29+
modifier(
30+
SnoozedFloatingPanel(
31+
messages: messages,
32+
initialDate: initialDate,
33+
folder: folder,
34+
completionHandler: completionHandler
35+
)
36+
)
37+
}
38+
}
39+
40+
struct SnoozedFloatingPanel: ViewModifier {
41+
@EnvironmentObject private var actionsManager: ActionsManager
42+
43+
@State private var isShowingPanel = false
44+
45+
let messages: [Message]?
46+
let initialDate: Date?
47+
let folder: Folder?
48+
let completionHandler: ((Action) -> Void)?
49+
50+
func body(content: Content) -> some View {
51+
content
52+
.onChange(of: messages) { newValue in
53+
isShowingPanel = newValue != nil
54+
}
55+
.scheduleFloatingPanel(
56+
isPresented: $isShowingPanel,
57+
type: .snooze,
58+
initialDate: initialDate,
59+
completionHandler: handleSelectedDate
60+
)
61+
}
62+
63+
private func handleSelectedDate(_ date: Date) {
64+
guard let messages else { return }
65+
66+
Task {
67+
let action = try await actionsManager.performSnooze(messages: messages, date: date, originFolder: folder)
68+
completionHandler?(action)
69+
}
70+
}
71+
}

Mail/Views/Thread/SnoozedThreadHeaderView.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,55 @@
1616
along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19+
import MailCore
1920
import MailResources
2021
import SwiftUI
2122

2223
struct SnoozedThreadHeaderView: View {
24+
@Environment(\.dismiss) private var dismiss
25+
@EnvironmentObject private var actionsManager: ActionsManager
26+
27+
@State private var messagesToSnooze: [Message]?
28+
2329
let date: Date
24-
let shouldDisplayActions: Bool
30+
let messages: [Message]
31+
let folder: Folder?
32+
33+
private var origin: ActionOrigin {
34+
return .threadHeader(originFolder: folder, nearestMessagesToSnooze: $messagesToSnooze)
35+
}
2536

2637
var body: some View {
2738
MessageHeaderActionView(
2839
icon: MailResourcesAsset.alarmClockFilled.swiftUIImage,
2940
message: MailResourcesStrings.Localizable.snoozeAlertTitle(date.formatted(.messageHeader)),
3041
isFirst: true,
31-
shouldDisplayActions: shouldDisplayActions
42+
shouldDisplayActions: folder?.canAccessSnoozeActions ?? false
3243
) {
3344
Button(MailResourcesStrings.Localizable.buttonModify, action: edit)
3445
MessageHeaderDivider()
3546
Button(MailResourcesStrings.Localizable.buttonCancelReminder, action: cancel)
3647
}
48+
.snoozedFloatingPanel(
49+
messages: messagesToSnooze,
50+
initialDate: date,
51+
folder: folder
52+
) { _ in dismiss() }
3753
}
3854

39-
private func edit() {}
55+
private func edit() {
56+
Task {
57+
try await actionsManager.performAction(target: messages, action: .modifySnooze, origin: origin)
58+
}
59+
}
4060

41-
private func cancel() {}
61+
private func cancel() {
62+
Task {
63+
try await actionsManager.performAction(target: messages, action: .cancelSnooze, origin: origin)
64+
}
65+
}
4266
}
4367

4468
#Preview {
45-
SnoozedThreadHeaderView(date: .now, shouldDisplayActions: true)
69+
SnoozedThreadHeaderView(date: .now, messages: [], folder: nil)
4670
}

Mail/Views/Thread/ThreadView.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,7 @@ struct ThreadView: View {
5656
.padding(.horizontal, value: .medium)
5757

5858
if thread.isSnoozed, let snoozeEndDate = thread.snoozeEndDate {
59-
SnoozedThreadHeaderView(
60-
date: snoozeEndDate,
61-
shouldDisplayActions: thread.folder?.canAccessSnoozeActions ?? false
62-
)
59+
SnoozedThreadHeaderView(date: snoozeEndDate, messages: thread.messages.toArray(), folder: thread.folder)
6360
}
6461

6562
MessageListView(messages: thread.messages.toArray(), mailboxManager: mailboxManager)

MailCore/Cache/Actions/Action+List.swift

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,26 @@ extension Action: CaseIterable {
7979
}
8080

8181
public var shouldDisableMultipleSelection: Bool {
82-
return ![.openMovePanel, .saveThreadInkDrive, .shareMailLink, .reportJunk, .phishing, .block, .blockList].contains(self)
82+
return ![
83+
.openMovePanel,
84+
.saveThreadInkDrive,
85+
.shareMailLink,
86+
.reportJunk,
87+
.phishing,
88+
.block,
89+
.blockList,
90+
.snooze,
91+
.modifySnooze
92+
].contains(self)
8393
}
8494

8595
private static func actionsForMessage(_ message: Message, origin: ActionOrigin,
8696
userIsStaff: Bool,
8797
userEmail: String) -> (quickActions: [Action], listActions: [Action]) {
8898
@LazyInjectService var platformDetector: PlatformDetectable
8999

100+
let snoozedActions = snoozedActions([message], folder: origin.frozenFolder)
101+
90102
var spamAction: Action? {
91103
guard !message.fromMe(currentMailboxEmail: userEmail) else { return nil }
92104
return message.folder?.role == .spam ? .nonSpam : .reportJunk
@@ -107,7 +119,9 @@ extension Action: CaseIterable {
107119
userIsStaff ? .reportDisplayProblem : nil
108120
]
109121

110-
return (Action.quickActions, tempListActions.compactMap { $0 })
122+
let listActions = snoozedActions + tempListActions.compactMap { $0 }
123+
124+
return (Action.quickActions, listActions)
111125
}
112126

113127
private static func actionsForMessagesInDifferentThreads(_ messages: [Message], originFolder: Folder?, userEmail: String)
@@ -121,6 +135,8 @@ extension Action: CaseIterable {
121135
.delete
122136
]
123137

138+
let snoozedActions = snoozedActions(messages, folder: originFolder)
139+
124140
var spamAction: Action? {
125141
let selfThread = messages.flatMap(\.from).allSatisfy { $0.isMeOrPlusMe(currentMailboxEmail: userEmail) }
126142
guard !selfThread else { return nil }
@@ -134,7 +150,9 @@ extension Action: CaseIterable {
134150
.saveThreadInkDrive
135151
]
136152

137-
return (quickActions, tempListActions.compactMap { $0 })
153+
let listActions = snoozedActions + tempListActions.compactMap { $0 }
154+
155+
return (quickActions, listActions)
138156
}
139157

140158
private static func actionsForMessagesInSameThreads(_ messages: [Message], originFolder: Folder?, userEmail: String)
@@ -149,6 +167,7 @@ extension Action: CaseIterable {
149167
return originFolder?.role == .spam ? .nonSpam : .reportJunk
150168
}
151169

170+
let snoozedActions = snoozedActions(messages, folder: originFolder)
152171
let tempListActions: [Action?] = [
153172
.openMovePanel,
154173
spamAction,
@@ -157,8 +176,22 @@ extension Action: CaseIterable {
157176
showUnstar ? .unstar : .star,
158177
.saveThreadInkDrive
159178
]
179+
let listActions = snoozedActions + tempListActions.compactMap { $0 }
160180

161-
return (Action.quickActions, tempListActions.compactMap { $0 })
181+
return (Action.quickActions, listActions)
182+
}
183+
184+
private static func snoozedActions(_ messages: [Message], folder: Folder?) -> [Action] {
185+
guard folder?.canAccessSnoozeActions == true else { return [] }
186+
187+
let messagesFromFolder = messages.filter { $0.folder?.remoteId == folder?.remoteId }
188+
guard !messagesFromFolder.isEmpty else { return [] }
189+
190+
if messagesFromFolder.allSatisfy(\.isSnoozed) {
191+
return [.modifySnooze, .cancelSnooze]
192+
} else {
193+
return [.snooze]
194+
}
162195
}
163196

164197
public static func actionsForMessages(_ messages: [Message],
@@ -193,6 +226,27 @@ extension Action: RawRepresentable {
193226
}
194227

195228
public extension Action {
229+
// MARK: Thread actions
230+
231+
static let snooze = Action(
232+
id: "snooze",
233+
title: MailResourcesStrings.Localizable.actionSnooze,
234+
iconResource: MailResourcesAsset.alarmClock,
235+
matomoName: "snooze"
236+
)
237+
static let modifySnooze = Action(
238+
id: "modifySnooze",
239+
title: MailResourcesStrings.Localizable.actionModifySnooze,
240+
iconResource: MailResourcesAsset.alarmClock,
241+
matomoName: "modifySnooze"
242+
)
243+
static let cancelSnooze = Action(
244+
id: "cancelSnooze",
245+
title: MailResourcesStrings.Localizable.actionCancelSnooze,
246+
iconResource: MailResourcesAsset.circleCross,
247+
matomoName: "cancelSnooze"
248+
)
249+
196250
// MARK: Mail actions
197251

198252
static let delete = Action(

MailCore/Cache/Actions/ActionOrigin.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public struct ActionOrigin {
2626
case toolbar
2727
case multipleSelection
2828
case shortcut
29+
case threadHeader
2930
}
3031

3132
public enum FloatingPanelSource {
@@ -45,6 +46,7 @@ public struct ActionOrigin {
4546
private(set) var nearestReportedForPhishingMessagesAlert: Binding<[Message]?>?
4647
private(set) var nearestReportedForDisplayProblemMessageAlert: Binding<Message?>?
4748
private(set) var nearestShareMailLinkPanel: Binding<ShareMailLinkResult?>?
49+
private(set) var nearestMessagesToSnooze: Binding<[Message]?>?
4850
private(set) var messagesToDownload: Binding<[Message]?>?
4951

5052
init(
@@ -59,6 +61,7 @@ public struct ActionOrigin {
5961
nearestReportedForPhishingMessagesAlert: Binding<[Message]?>? = nil,
6062
nearestReportedForDisplayProblemMessageAlert: Binding<Message?>? = nil,
6163
nearestShareMailLinkPanel: Binding<ShareMailLinkResult?>? = nil,
64+
nearestMessagesToSnooze: Binding<[Message]?>? = nil,
6265
messagesToDownload: Binding<[Message]?>? = nil
6366
) {
6467
self.type = type
@@ -72,6 +75,7 @@ public struct ActionOrigin {
7275
self.nearestReportedForPhishingMessagesAlert = nearestReportedForPhishingMessagesAlert
7376
self.nearestReportedForDisplayProblemMessageAlert = nearestReportedForDisplayProblemMessageAlert
7477
self.nearestShareMailLinkPanel = nearestShareMailLinkPanel
78+
self.nearestMessagesToSnooze = nearestMessagesToSnooze
7579
self.messagesToDownload = messagesToDownload
7680
}
7781

@@ -90,6 +94,7 @@ public struct ActionOrigin {
9094
nearestReportedForPhishingMessagesAlert: Binding<[Message]?>? = nil,
9195
nearestReportedForDisplayProblemMessageAlert: Binding<Message?>? = nil,
9296
nearestShareMailLinkPanel: Binding<ShareMailLinkResult?>? = nil,
97+
nearestMessagesToSnooze: Binding<[Message]?>? = nil,
9398
messagesToDownload: Binding<[Message]?>? = nil) -> ActionOrigin {
9499
return ActionOrigin(
95100
type: .floatingPanel(source: source),
@@ -102,6 +107,7 @@ public struct ActionOrigin {
102107
nearestReportedForPhishingMessagesAlert: nearestReportedForPhishingMessagesAlert,
103108
nearestReportedForDisplayProblemMessageAlert: nearestReportedForDisplayProblemMessageAlert,
104109
nearestShareMailLinkPanel: nearestShareMailLinkPanel,
110+
nearestMessagesToSnooze: nearestMessagesToSnooze,
105111
messagesToDownload: messagesToDownload
106112
)
107113
}
@@ -134,4 +140,8 @@ public struct ActionOrigin {
134140
nearestFlushAlert: Binding<FlushAlertState?>? = nil) -> ActionOrigin {
135141
ActionOrigin(type: .shortcut, folder: originFolder, nearestFlushAlert: nearestFlushAlert)
136142
}
143+
144+
public static func threadHeader(originFolder: Folder? = nil, nearestMessagesToSnooze: Binding<[Message]?>? = nil) -> ActionOrigin {
145+
return ActionOrigin(type: .threadHeader, folder: originFolder, nearestMessagesToSnooze: nearestMessagesToSnooze)
146+
}
137147
}

0 commit comments

Comments
 (0)