Skip to content

feat: Update Schedule options #1796

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 2 commits into from
May 13, 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
4 changes: 4 additions & 0 deletions Mail/Utils/Date+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ extension Date {
Calendar.current.date(byAdding: .day, value: -1, to: .now)!
}

static var tomorrow: Date {
Calendar.current.date(byAdding: .day, value: 1, to: .now)!
}

static var lastWeek: Date {
Calendar.current.date(byAdding: .weekOfYear, value: -1, to: .now)!
}
Expand Down
1 change: 1 addition & 0 deletions Mail/Views/Schedule/ScheduleFloatingPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ struct ScheduleFloatingPanel: ViewModifier {
isShowingCustomScheduleAlert: $isShowingCustomScheduleAlert,
isShowingMyKSuiteUpgrade: $isShowingMyKSuiteUpgrade,
type: type,
initialDate: initialDate,
completionHandler: completionHandler
)
.environmentObject(mailboxManager)
Expand Down
27 changes: 23 additions & 4 deletions Mail/Views/Schedule/ScheduleFloatingPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,32 @@ struct ScheduleFloatingPanelView: View {
@Binding var isShowingMyKSuiteUpgrade: Bool

let type: ScheduleType
let initialDate: Date?
let completionHandler: (Date) -> Void

private var scheduleOptions: [ScheduleOption] {
var seenDateOptions = Set<Date>()
if let initialDate {
seenDateOptions.insert(initialDate)
}

var filteredOptions = ScheduleOption.allPresetOptions.filter { option in
guard option.canBeDisplayed, let date = option.date else { return false }
if seenDateOptions.contains(date) {
return false
} else {
seenDateOptions.insert(date)
return true
}
}

let lastScheduledDate = UserDefaults.shared[keyPath: type.lastCustomScheduleDateKeyPath]
let lastScheduledOption = ScheduleOption.lastSchedule(value: lastScheduledDate)
if lastScheduledOption.canBeDisplayed, !seenDateOptions.contains(lastScheduledDate) {
filteredOptions.insert(.lastSchedule(value: lastScheduledDate), at: 0)
}

var allSimpleCases = ScheduleOption.allSimpleCases
allSimpleCases.insert(ScheduleOption.lastSchedule(value: lastScheduledDate), at: 0)
return allSimpleCases.filter { $0.shouldBeDisplayedNow }
return filteredOptions
}

var body: some View {
Expand All @@ -58,6 +76,7 @@ struct ScheduleFloatingPanelView: View {
ScheduleFloatingPanelView(
isShowingCustomScheduleAlert: .constant(false),
isShowingMyKSuiteUpgrade: .constant(false),
type: .scheduledDraft
type: .scheduledDraft,
initialDate: nil
) { _ in }
}
113 changes: 50 additions & 63 deletions Mail/Views/Schedule/ScheduleOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
import MailResources
import SwiftUI

extension ScheduleOption {
static var allPresetOptions: [ScheduleOption] = [
.laterThisMorning,
.thisAfternoon,
.thisEvening,
.tomorrowMorning,
.nextMonday,
.nextMondayMorning,
.nextMondayAfternoon
]
}

enum ScheduleOption: Identifiable, Equatable {
case laterThisMorning
case thisAfternoon
Expand All @@ -31,25 +43,6 @@ enum ScheduleOption: Identifiable, Equatable {

var id: String { title }

var date: Date? {
switch self {
case .laterThisMorning:
return dateFromNow(setHour: 8)
case .thisAfternoon:
return dateFromNow(setHour: 14)
case .thisEvening:
return dateFromNow(setHour: 18)
case .tomorrowMorning:
return dateFromNow(setHour: 8, tomorrow: true)
case .nextMondayMorning, .nextMonday:
return nextMonday(setHour: 8)
case .nextMondayAfternoon:
return nextMonday(setHour: 14)
case .lastSchedule(let value):
return value
}
}

var title: String {
switch self {
case .laterThisMorning:
Expand Down Expand Up @@ -92,25 +85,6 @@ enum ScheduleOption: Identifiable, Equatable {
}
}

var shouldBeDisplayedNow: Bool {
let weekday = Calendar.current.component(.weekday, from: Date.now)

switch self {
case .laterThisMorning:
return isInTimeWindow(firstHour: 0, lastHour: 7)
case .thisAfternoon:
return isInTimeWindow(firstHour: 7, lastHour: 13)
case .thisEvening:
return isInTimeWindow(firstHour: 13, lastHour: 17)
case .tomorrowMorning, .nextMonday:
return true
case .nextMondayMorning, .nextMondayAfternoon:
return weekday == 1 || weekday == 7
case .lastSchedule(let value):
return value >= .minimumScheduleDelay
}
}

var matomoName: String {
switch self {
case .laterThisMorning:
Expand All @@ -132,36 +106,49 @@ enum ScheduleOption: Identifiable, Equatable {
}
}

static var allSimpleCases: [ScheduleOption] = [
.laterThisMorning,
.thisAfternoon,
.thisEvening,
.tomorrowMorning,
.nextMonday,
.nextMondayMorning,
.nextMondayAfternoon
]
var date: Date? {
switch self {
case .laterThisMorning:
return specificHour(at: 8, from: .now)
case .thisAfternoon:
return specificHour(at: 14, from: .now)
case .thisEvening:
return specificHour(at: 18, from: .now)
case .tomorrowMorning:
return specificHour(at: 8, from: .tomorrow)
case .nextMonday, .nextMondayMorning:
return nextMonday(at: 8)
case .nextMondayAfternoon:
return nextMonday(at: 14)
case .lastSchedule(let date):
return date
}
}

private func dateFromNow(setHour: Int, tomorrow: Bool = false) -> Date? {
let startOfDay = Calendar.current.startOfDay(for: .now)
guard let dateWithDay = Calendar.current.date(byAdding: .day, value: tomorrow ? 1 : 0, to: startOfDay) else { return nil }
return Calendar.current.date(bySetting: .hour, value: setHour, of: dateWithDay)
var canBeDisplayed: Bool {
guard let date else { return false }
return isAvailable && date >= .minimumScheduleDelay
}

private func nextMonday(setHour: Int) -> Date? {
let todayMidDay = Calendar.current.startOfDay(for: .now).addingTimeInterval(43200)
return Calendar.current.nextDate(
after: todayMidDay,
matching: .init(hour: setHour, weekday: 2),
matchingPolicy: .nextTime,
direction: .forward
)
private var isAvailable: Bool {
let isInWeekend = Calendar.current.isDateInWeekend(.now)

switch self {
case .laterThisMorning, .thisAfternoon, .thisEvening, .tomorrowMorning, .nextMonday:
return !isInWeekend
case .nextMondayMorning, .nextMondayAfternoon:
return isInWeekend
case .lastSchedule:
return true
}
}

private func isInTimeWindow(firstHour: Int, lastHour: Int) -> Bool {
let hour = Calendar.current.component(.hour, from: .now)
let minute = Calendar.current.component(.minute, from: .now)
private func specificHour(at hour: Int, from date: Date) -> Date? {
return Calendar.current.date(bySetting: .hour, value: hour, of: date.startOfDay)
}

return (hour > firstHour && hour < lastHour) || (hour == lastHour && minute < 55)
private func nextMonday(at hour: Int) -> Date? {
let dateComponents = DateComponents(hour: hour, weekday: 2)
return Calendar.current.nextDate(after: .now.startOfDay, matching: dateComponents, matchingPolicy: .nextTime)
}
}
Loading