Skip to content

feat(Snooze): Display error messages when possible #1791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Mail/Views/Schedule/SnoozedFloatingPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ struct SnoozedFloatingPanel: ViewModifier {
guard let messages else { return }

Task {
let action = try await actionsManager.performSnooze(messages: messages, date: date, originFolder: folder)
completionHandler?(action)
await tryOrDisplayError {
let action = try await actionsManager.performSnooze(messages: messages, date: date, originFolder: folder)
completionHandler?(action)
}
}
}
}
24 changes: 20 additions & 4 deletions MailCore/API/MailApiError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ public class MailApiError: MailError {
shouldDisplay: true
)

public static let apiMessageNotSnoozed = MailApiError(code: "mail__message_not_snoozed")
public static let apiMessageNotSnoozed = MailApiError(
code: "mail__message_not_snoozed",
localizedDescription: MailResourcesStrings.Localizable.errorMessageNotSnoozed,
shouldDisplay: true
)

static let allErrors: [MailApiError] = [
// General
Expand Down Expand Up @@ -171,9 +175,21 @@ public class MailApiError: MailError {

// Snooze
apiMessageNotSnoozed,
MailApiError(code: "mail__message_snooze_already_scheduled"),
MailApiError(code: "mail__message_max_number_of_scheduled_snooze_reached"),
MailApiError(code: "mail__message_cannot_be_snooze")
MailApiError(
code: "mail__message_snooze_already_scheduled",
localizedDescription: MailResourcesStrings.Localizable.errorMessageSnoozeAlreadyScheduled,
shouldDisplay: true
),
MailApiError(
code: "mail__message_max_number_of_scheduled_snooze_reached",
localizedDescription: MailResourcesStrings.Localizable.errorMaxNumberOfScheduledSnoozeReached,
shouldDisplay: true
),
MailApiError(
code: "mail__message_cannot_be_snooze",
localizedDescription: MailResourcesStrings.Localizable.errorMessageCannotBeSnoozed,
shouldDisplay: true
)
]

static func mailApiErrorFromCode(_ code: String) -> MailApiError? {
Expand Down
16 changes: 13 additions & 3 deletions MailCore/API/MailApiFetcher/MailApiFetcher+Snooze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ public extension MailApiFetcher {
}
}

func updateSnooze(message: Message, until date: Date, mailbox: Mailbox) async throws {
guard let snoozeUuid = message.snoozeUuid else { throw MailError.missingSnoozeUUID }

let _: Empty = try await perform(
request: authenticatedRequest(
.snoozeAction(mailboxUuid: mailbox.uuid, snoozeUuid: snoozeUuid),
method: .put,
parameters: ["end_date": date]
)
)
}

func updateSnooze(messages: [Message], until date: Date, mailbox: Mailbox) async throws -> [SnoozeUpdatedAPIResponse] {
return try await batchOver(values: messages, chunkSize: Self.editSnoozeAPILimit) { chunk in
return try await self.perform(request: self.authenticatedRequest(
Expand All @@ -45,9 +57,7 @@ public extension MailApiFetcher {
}

func deleteSnooze(message: Message, mailbox: Mailbox) async throws {
guard let snoozeUuid = message.snoozeUuid else {
throw MailError.missingSnoozeUUID
}
guard let snoozeUuid = message.snoozeUuid else { throw MailError.missingSnoozeUUID }

let _: Empty = try await perform(
request: authenticatedRequest(
Expand Down
6 changes: 5 additions & 1 deletion MailCore/API/MailError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ public class MailError: LocalizedError, Encodable {

public static let tooShortScheduleDelay = MailError(code: "tooShortScheduleDelay")

public static let missingSnoozeUUID = MailError(code: "missingSnoozeUUID")
public static let missingSnoozeUUID = MailError(
code: "missingSnoozeUUID",
localizedDescription: MailResourcesStrings.Localizable.errorMessageNotSnoozed,
shouldDisplay: true
)
}

extension MailError: Identifiable {
Expand Down
122 changes: 82 additions & 40 deletions MailCore/Cache/Actions/ActionsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
// Needed to be sure that the bottomView is dismissed before we try to show the printPanel
DispatchQueue.main.asyncAfter(deadline: UIConstants.modalCloseDelay) {
let nc = NotificationCenter.default
nc.post(name: Notification.Name.printNotification, object: message)

Check warning on line 168 in MailCore/Cache/Actions/ActionsManager.swift

View workflow job for this annotation

GitHub Actions / UI Tests

capture of 'message' with non-sendable type 'Message' in a `@Sendable` closure

Check warning on line 168 in MailCore/Cache/Actions/ActionsManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

capture of 'message' with non-sendable type 'Message' in a `@Sendable` closure
}
case .moveToInbox, .nonSpam:
try await performMove(messages: messagesWithDuplicates, from: origin.frozenFolder, to: .inbox)
Expand Down Expand Up @@ -306,46 +306,6 @@
}
}

public func performSnooze(messages: [Message], date: Date, originFolder: Folder?) async throws -> Action {
let messagesToExecuteAction = messages.lastMessagesToExecuteAction(
currentMailboxEmail: mailboxManager.mailbox.email,
currentFolder: originFolder
)

var snoozeCount = 0
let allMessagesAreSnoozed = messagesToExecuteAction.allSatisfy(\.isSnoozed)
if allMessagesAreSnoozed {
let response = try await mailboxManager.updateSnooze(messages: messagesToExecuteAction, until: date)
snoozeCount = response.reduce(0) { $0 + $1.updated.count }
} else {
let response = try await mailboxManager.snooze(messages: messagesToExecuteAction, until: date)
snoozeCount = response.reduce(0) { $0 + $1.snoozeActions.count }
}

if snoozeCount == 0 {
snackbarPresenter.show(message: MailResourcesStrings.Localizable.errorUnknown)
} else {
snackbarPresenter
.show(message: MailResourcesStrings.Localizable.snackbarSnoozeSuccess(date.formatted(.snoozeSnackbar)))
}

return allMessagesAreSnoozed ? .modifiedSnoozed : .snoozed
}

private func performDeleteSnooze(messages: [Message]) async throws {
let response = try await mailboxManager.deleteSnooze(messages: messages)
let deletedSnoozeCount = response.reduce(0) { $0 + $1.cancelled.count }

Task { @MainActor in
if deletedSnoozeCount == 0 {
snackbarPresenter.show(message: MailResourcesStrings.Localizable.errorUnknown)
} else {
snackbarPresenter
.show(message: MailResourcesStrings.Localizable.snackbarUnsnoozeSuccess(deletedSnoozeCount))
}
}
}

@MainActor
private func displayResultSnackbar(message: String?, undoAction: UndoAction?) {
guard let message else { return }
Expand Down Expand Up @@ -483,3 +443,85 @@
return uniqueThreads.count == 1
}
}

// MARK: - Snooze

extension ActionsManager {
public func performSnooze(messages: [Message], date: Date, originFolder: Folder?) async throws -> Action {
let messagesToExecuteAction = messages.lastMessagesToExecuteAction(
currentMailboxEmail: mailboxManager.mailbox.email,
currentFolder: originFolder
)

let allMessagesAreSnoozed = messagesToExecuteAction.allSatisfy(\.isSnoozed)
if allMessagesAreSnoozed {
if messagesToExecuteAction.count == 1, let message = messagesToExecuteAction.first {
try await modifySnooze(message: message, date: date)
} else {
try await modifySnooze(messages: messagesToExecuteAction, date: date)
}
return .modifiedSnoozed
} else {
try await snooze(messages: messagesToExecuteAction, date: date)
return .snoozed
}
}

private func performDeleteSnooze(messages: [Message]) async throws {
if messages.count == 1, let message = messages.first {
try await deleteSnooze(message: message)
} else {
try await deleteSnooze(messages: messages)
}
}

private func snooze(messages: [Message], date: Date) async throws {
let response = try await mailboxManager.snooze(messages: messages, until: date)

let snoozeCount = response.reduce(0) { $0 + $1.snoozeActions.count }
showSnoozeCompletedSnackar(messagesSnoozed: snoozeCount, date: date)
}

private func modifySnooze(message: Message, date: Date) async throws {
try await mailboxManager.updateSnooze(message: message, until: date)
showSnoozeCompletedSnackar(messagesSnoozed: 1, date: date)
}

private func modifySnooze(messages: [Message], date: Date) async throws {
let response = try await mailboxManager.updateSnooze(messages: messages, until: date)

let snoozeCount = response.reduce(0) { $0 + $1.updated.count }
showSnoozeCompletedSnackar(messagesSnoozed: snoozeCount, date: date)
}

private func deleteSnooze(message: Message) async throws {
try await mailboxManager.deleteSnooze(message: message)
showDeleteSnoozeCompletedSnackar(snoozedDeleted: 1)
}

private func deleteSnooze(messages: [Message]) async throws {
let response = try await mailboxManager.deleteSnooze(messages: messages)

let deletedSnoozeCount = response.reduce(0) { $0 + $1.cancelled.count }
showDeleteSnoozeCompletedSnackar(snoozedDeleted: deletedSnoozeCount)
}

private func showSnoozeCompletedSnackar(messagesSnoozed: Int, date: Date) {
if messagesSnoozed == 0 {
snackbarPresenter.show(message: MailResourcesStrings.Localizable.errorUnknown)
} else {
snackbarPresenter.show(
message: MailResourcesStrings.Localizable.snackbarSnoozeSuccess(date.formatted(.snoozeSnackbar))
)
}
}

private func showDeleteSnoozeCompletedSnackar(snoozedDeleted: Int) {
if snoozedDeleted == 0 {
snackbarPresenter.show(message: MailResourcesStrings.Localizable.errorUnknown)
} else {
snackbarPresenter
.show(message: MailResourcesStrings.Localizable.snackbarUnsnoozeSuccess(snoozedDeleted))
}
}
}
14 changes: 14 additions & 0 deletions MailCore/Cache/MailboxManager/MailboxManager+Snooze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public extension MailboxManager {
return response
}

func updateSnooze(message: Message, until date: Date) async throws {
try await apiFetcher.updateSnooze(message: message, until: date, mailbox: mailbox)
Task {
try await refreshFolder(from: [message], additionalFolder: nil)
}
}

func updateSnooze(messages: [Message], until date: Date) async throws -> [SnoozeUpdatedAPIResponse] {
let response = try await apiFetcher.updateSnooze(messages: messages, until: date, mailbox: mailbox)
Task {
Expand All @@ -37,6 +44,13 @@ public extension MailboxManager {
return response
}

func deleteSnooze(message: Message) async throws {
try await apiFetcher.deleteSnooze(message: message, mailbox: mailbox)
Task {
try await refreshFolder(from: [message], additionalFolder: nil)
}
}

func deleteSnooze(messages: [Message]) async throws -> [SnoozeCancelledAPIResponse] {
let response = try await apiFetcher.deleteSnooze(messages: messages, mailbox: mailbox)
Task {
Expand Down
14 changes: 13 additions & 1 deletion MailResources/Localizable/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Locale: de, German
* Tagged: ios
* Exported by: Valentin Perignon
* Exported at: Fri, 25 Apr 2025 11:19:29 +0200
* Exported at: Thu, 08 May 2025 08:09:40 +0200
*/

/* loco:62bb154d7513e127cb2e9c94 */
Expand Down Expand Up @@ -820,9 +820,21 @@
/* loco:65d4ae8ef9b6570a070b3e92 */
"errorMailboxUnavailable" = "Diese E-Mail Adresse ist nicht verfügbar";

/* loco:680b69620fe68fbfc80ac583 */
"errorMaxNumberOfScheduledSnoozeReached" = "Sie haben die maximale Anzahl geplanter Schlummerzeiten erreicht";

/* loco:680b8d7f855ff2c2e708b282 */
"errorMessageCannotBeSnoozed" = "Diese Nachricht kann nicht in den Schlummermodus versetzt werden";

/* loco:645c9af969688c1b200ad4a2 */
"errorMessageNotFound" = "Nachricht nicht gefunden";

/* loco:680b6895a9a8ec84c3020a73 */
"errorMessageNotSnoozed" = "Diese Nachricht ist derzeit nicht im Schlummermodus";

/* loco:680b692fe83f3ba1df0a2895 */
"errorMessageSnoozeAlreadyScheduled" = "Für diese Nachricht ist bereits ein Schlummern geplant";

/* loco:6494050cd62813225a066fb2 */
"errorMoveDestinationNotFound" = "Der Zielordner besteht nicht";

Expand Down
14 changes: 13 additions & 1 deletion MailResources/Localizable/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Locale: en, English
* Tagged: ios
* Exported by: Valentin Perignon
* Exported at: Fri, 25 Apr 2025 11:19:29 +0200
* Exported at: Thu, 08 May 2025 08:09:40 +0200
*/

/* loco:62bb154d7513e127cb2e9c94 */
Expand Down Expand Up @@ -820,9 +820,21 @@
/* loco:65d4ae8ef9b6570a070b3e92 */
"errorMailboxUnavailable" = "This email address is not available";

/* loco:680b69620fe68fbfc80ac583 */
"errorMaxNumberOfScheduledSnoozeReached" = "You’ve reached the maximum number of scheduled snoozes";

/* loco:680b8d7f855ff2c2e708b282 */
"errorMessageCannotBeSnoozed" = "This message cannot be snoozed";

/* loco:645c9af969688c1b200ad4a2 */
"errorMessageNotFound" = "Message not found";

/* loco:680b6895a9a8ec84c3020a73 */
"errorMessageNotSnoozed" = "This message is not currently snoozed";

/* loco:680b692fe83f3ba1df0a2895 */
"errorMessageSnoozeAlreadyScheduled" = "A snooze is already scheduled for this message";

/* loco:6494050cd62813225a066fb2 */
"errorMoveDestinationNotFound" = "The destination folder does not exist";

Expand Down
14 changes: 13 additions & 1 deletion MailResources/Localizable/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Locale: es, Spanish
* Tagged: ios
* Exported by: Valentin Perignon
* Exported at: Fri, 25 Apr 2025 11:19:29 +0200
* Exported at: Thu, 08 May 2025 08:09:40 +0200
*/

/* loco:62bb154d7513e127cb2e9c94 */
Expand Down Expand Up @@ -820,9 +820,21 @@
/* loco:65d4ae8ef9b6570a070b3e92 */
"errorMailboxUnavailable" = "Esta dirección de correo electrónico no está disponible";

/* loco:680b69620fe68fbfc80ac583 */
"errorMaxNumberOfScheduledSnoozeReached" = "Has alcanzado el número máximo de aplazamientos programados";

/* loco:680b8d7f855ff2c2e708b282 */
"errorMessageCannotBeSnoozed" = "Este mensaje no se puede aplazar";

/* loco:645c9af969688c1b200ad4a2 */
"errorMessageNotFound" = "Mensaje no encontrado";

/* loco:680b6895a9a8ec84c3020a73 */
"errorMessageNotSnoozed" = "Este mensaje no está aplazado actualmente";

/* loco:680b692fe83f3ba1df0a2895 */
"errorMessageSnoozeAlreadyScheduled" = "Este mensaje ya tiene un aplazamiento programado";

/* loco:6494050cd62813225a066fb2 */
"errorMoveDestinationNotFound" = "La carpeta de destino no existe";

Expand Down
14 changes: 13 additions & 1 deletion MailResources/Localizable/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Locale: fr, French
* Tagged: ios
* Exported by: Valentin Perignon
* Exported at: Fri, 25 Apr 2025 11:19:29 +0200
* Exported at: Thu, 08 May 2025 08:09:40 +0200
*/

/* loco:62bb154d7513e127cb2e9c94 */
Expand Down Expand Up @@ -820,9 +820,21 @@
/* loco:65d4ae8ef9b6570a070b3e92 */
"errorMailboxUnavailable" = "Cette adresse mail n’est pas disponible";

/* loco:680b69620fe68fbfc80ac583 */
"errorMaxNumberOfScheduledSnoozeReached" = "Vous avez atteint le nombre maximal de mises en attente programmées";

/* loco:680b8d7f855ff2c2e708b282 */
"errorMessageCannotBeSnoozed" = "Ce message ne peut pas être mis en attente";

/* loco:645c9af969688c1b200ad4a2 */
"errorMessageNotFound" = "Message introuvable";

/* loco:680b6895a9a8ec84c3020a73 */
"errorMessageNotSnoozed" = "Ce message n’est pas mis en attente";

/* loco:680b692fe83f3ba1df0a2895 */
"errorMessageSnoozeAlreadyScheduled" = "Une mise en attente est déjà programmée pour ce message";

/* loco:6494050cd62813225a066fb2 */
"errorMoveDestinationNotFound" = "Le dossier de destination n’existe pas";

Expand Down
Loading
Loading