Skip to content
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

Advanced repeat rules #8456

Merged
merged 33 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
811ac1b
Prototype event generation using ByRules
andrehgdias Nov 28, 2024
2b6983c
Implements event expansion for daily BYDAY and BYMONTH
Dec 2, 2024
9b10f95
Implements event expansion for Weekly interval
Dec 3, 2024
7b1b3f3
Implements event expansion for Monthly interval
Dec 3, 2024
971d6c8
Implements expansion for Yearly interval
Dec 9, 2024
9f7e6bd
Add export of advanced repeat rules
Patrik-wav Dec 10, 2024
60ff001
Implements BYMONTHDAY filtering
andrehgdias Dec 12, 2024
cac89f0
Filter events happening before progenitor
andrehgdias Dec 12, 2024
3827ed9
Implements BYSETPOS filtering
andrehgdias Dec 9, 2024
d7e0e8b
Fixes BYWEEKNO expansion
andrehgdias Dec 11, 2024
38c799a
Implements BYYEARDAY filtering
Dec 16, 2024
1aacef8
Fixes SETPOS filtering
Dec 17, 2024
7ce09c1
Simplifies generator with advanced rules
Dec 17, 2024
b1121c8
[SDK] Implements BYMONTH expansion
Dec 18, 2024
f2a6781
[SDK] Implements BYWEEKNO expansion
Dec 18, 2024
c0956a4
[SDK] Implements BYYEARDAY expansion
Dec 19, 2024
5af07b1
[SDK] Implements BYMONTHDAY expansion
Dec 19, 2024
4d3d066
[SDK] Implements BYDAY expansion
Dec 20, 2024
ea406c6
[SDK] Implements tests for the complete recurrence generation flow
Jan 6, 2025
d61281c
[SDK] Exposes EventFacade to uniffi
Jan 8, 2025
c8dfd98
[Android] Integrates SDK event expansion during alarm scheduling
Jan 8, 2025
a06961a
[iOS] Integrates SDK event expansion during alarm scheduling
murilopereirame Jan 13, 2025
7ce738c
Fixes BYSETPOS on Web/Desktop
Jan 14, 2025
fead3ed
[Desktop] Integrates event expansion during alarm scheduling
Jan 14, 2025
0e0c0e1
[iOS] Adds BYSETPOS handling during alarm schedule
murilopereirame Jan 15, 2025
bfc06b0
[Android] Fixes BYSETPOS during alarm schedule
Jan 15, 2025
52662bf
Adds info banner for unsupported rules
Jan 16, 2025
4785dd7
Adds translations to Advanced Repeat Rules
murilopereirame Jan 13, 2025
dfa4faa
Fixes styling and linting
Feb 3, 2025
1cff871
[Android] Update encryption tests
Feb 5, 2025
5d5d261
[Android] Adds dexmaker to mock classes during Instrumented Tests
Feb 4, 2025
3901d4c
[iOS] Fix iOS tests and SDK event expansion
Feb 6, 2025
945ddb4
Changes after review
murilopereirame Feb 11, 2025
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
Prev Previous commit
Next Next commit
[iOS] Integrates SDK event expansion during alarm scheduling
  • Loading branch information
murilopereirame authored and mup committed Feb 12, 2025
commit a06961a769c22e45d3422a3e4c41b1f92a7e68c0
69 changes: 57 additions & 12 deletions app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import tutasdk

/// Identifier for when event will happen
public struct EventOccurrence {
Expand Down Expand Up @@ -91,7 +92,14 @@ public class AlarmModel: AlarmCalculator {

cal.timeZone = isAllDayEvent ? localTimeZone : TimeZone(identifier: repeatRule.timeZone) ?? localTimeZone

return LazyEventSequence(calcEventStart: calcEventStart, endDate: endDate, repeatRule: repeatRule, cal: cal, calendarComponent: calendarUnit)
return LazyEventSequence(
calcEventStart: calcEventStart,
endDate: endDate,
repeatRule: repeatRule,
cal: cal,
calendarComponent: calendarUnit,
dateProvider: self.dateProvider
)
}

static func alarmTime(trigger: AlarmInterval, eventTime: Date) -> Date {
Expand All @@ -111,22 +119,48 @@ private struct LazyEventSequence: Sequence, IteratorProtocol {
let repeatRule: RepeatRule
let cal: Calendar
let calendarComponent: Calendar.Component
let dateProvider: DateProvider

var expandedEvents: [DateTime] = []

fileprivate var ocurrenceNumber = 0
fileprivate var intervalNumber = 0
fileprivate var occurrenceNumber = 0
fileprivate var exclusionNumber = 0

mutating func next() -> EventOccurrence? {
if case let .count(n) = repeatRule.endCondition, ocurrenceNumber >= n { return nil }
let occurrenceDate = cal.date(byAdding: self.calendarComponent, value: repeatRule.interval * ocurrenceNumber, to: calcEventStart)!
if let endDate, occurrenceDate >= endDate {
return nil
} else {
let occurrence = EventOccurrence(occurrenceNumber: ocurrenceNumber, occurenceDate: occurrenceDate)
ocurrenceNumber += 1
if case let .count(n) = repeatRule.endCondition, occurrenceNumber >= n { return nil }

if expandedEvents.isEmpty {
let nextExpansionProgenitor = cal.date(byAdding: self.calendarComponent, value: repeatRule.interval * intervalNumber, to: calcEventStart)!
let progenitorTime = UInt64(nextExpansionProgenitor.timeIntervalSince1970)
let eventFacade = EventFacade()
let byRules = repeatRule.advancedRules.map { $0.toSDKRule() }
let generatedEvents = eventFacade.generateFutureInstances(
date: progenitorTime * 1000,
repeatRule: EventRepeatRule(frequency: repeatRule.frequency.toSDKPeriod(), byRules: byRules)
)
self.expandedEvents.append(contentsOf: generatedEvents)
// Handle the event 0
if self.intervalNumber == 0 && !self.expandedEvents.contains(progenitorTime) { expandedEvents.append(progenitorTime) }

intervalNumber += 1
}

if let date = expandedEvents.popLast() {
occurrenceNumber += 1

if let endDate, date >= UInt64(endDate.timeIntervalSince1970) { return nil }

while exclusionNumber < repeatRule.excludedDates.count && repeatRule.excludedDates[exclusionNumber] < occurrenceDate { exclusionNumber += 1 }
if exclusionNumber < repeatRule.excludedDates.count && repeatRule.excludedDates[exclusionNumber] == occurrenceDate { return self.next() }
return occurrence
while exclusionNumber < repeatRule.excludedDates.count && UInt64(repeatRule.excludedDates[exclusionNumber].timeIntervalSince1970) < date {
exclusionNumber += 1
}
if exclusionNumber < repeatRule.excludedDates.count && UInt64(repeatRule.excludedDates[exclusionNumber].timeIntervalSince1970) == date {
return self.next()
}

return EventOccurrence(occurrenceNumber: occurrenceNumber, occurenceDate: Date(timeIntervalSince1970: Double(date)))
} else {
return self.next()
}
}
}
Expand Down Expand Up @@ -174,3 +208,14 @@ private func calendarUnit(for repeatPeriod: RepeatPeriod) -> Calendar.Component
case .annually: return .year
}
}

private extension RepeatPeriod {
func toSDKPeriod() -> tutasdk.RepeatPeriod {
switch self {
case .annually: return tutasdk.RepeatPeriod.annually
case .daily: return tutasdk.RepeatPeriod.daily
case .monthly: return tutasdk.RepeatPeriod.monthly
case .weekly: return tutasdk.RepeatPeriod.weekly
}
}
}
9 changes: 9 additions & 0 deletions app-ios/TutanotaSharedFramework/Alarms/RepeatRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ struct RepeatRule: Equatable {
let timeZone: String
let endCondition: RepeatEndCondition
let excludedDates: [Date]
let advancedRules: [AdvancedRule]
}

extension RepeatRule {
Expand All @@ -16,5 +17,13 @@ extension RepeatRule {
self.endCondition = RepeatEndCondition(endType: endType, endValue: endValue ?? 0)
let decryptedExclusions: [Date] = try encrypted.excludedDates.map { try decrypt(base64: $0.date, key: sessionKey) }
self.excludedDates = decryptedExclusions
let advancedRules: [AdvancedRule] = try encrypted.advancedRules.map {
let decryptedType: String = try decrypt(base64: $0.ruleType, key: sessionKey)
let type = try ByRuleType(value: decryptedType)

let interval: String = try decrypt(base64: $0.interval, key: sessionKey)
return AdvancedRule(ruleType: type, interval: interval)
}
self.advancedRules = advancedRules
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import tutasdk

public enum RepeatPeriod: Int, SimpleStringDecodable {
case daily = 0
Expand Down Expand Up @@ -29,7 +30,60 @@ public enum RepeatEndCondition: Equatable {
}
}

public enum ByRuleType: String, Codable, Equatable, SimpleStringDecodable {
case byminute
case byhour
case byday
case bymonthday
case byyearday
case byweekno
case bymonth
case bysetpos
case wkst

public init(value: String) throws {
switch value {
case "0": self = .byminute
case "1": self = .byhour
case "2": self = .byday
case "3": self = .bymonthday
case "4": self = .byyearday
case "5": self = .byweekno
case "6": self = .bymonth
case "7": self = .bysetpos
case "8": self = .wkst
default: throw TUTErrorFactory.createError("Invalid ByRuleType")
}
}
}

public struct EncryptedDateWrapper: Codable, Hashable { public let date: Base64 }
public struct EncryptedAdvancedRuleWrapper: Codable, Hashable {
public let ruleType: String
public let interval: String
}
public struct AdvancedRule: Codable, Hashable {
public let ruleType: ByRuleType
public let interval: String

public func toSDKRule() -> ByRule { ByRule(byRule: self.ruleType.toSDKType(), interval: self.interval) }
}

extension ByRuleType {
func toSDKType() -> tutasdk.ByRuleType {
switch self {
case .byminute: return tutasdk.ByRuleType.byminute
case .byhour: return tutasdk.ByRuleType.byhour
case .byday: return tutasdk.ByRuleType.byday
case .bymonth: return tutasdk.ByRuleType.bymonth
case .bymonthday: return tutasdk.ByRuleType.bymonthday
case .byyearday: return tutasdk.ByRuleType.byyearday
case .byweekno: return tutasdk.ByRuleType.byweekno
case .bysetpos: return tutasdk.ByRuleType.bysetpos
case .wkst: return tutasdk.ByRuleType.wkst
}
}
}

public struct EncryptedRepeatRule: Codable, Hashable {
public let frequency: Base64
Expand All @@ -38,6 +92,7 @@ public struct EncryptedRepeatRule: Codable, Hashable {
public let endType: Base64
public let endValue: Base64?
public let excludedDates: [EncryptedDateWrapper]
public let advancedRules: [EncryptedAdvancedRuleWrapper]

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
Expand All @@ -47,6 +102,12 @@ public struct EncryptedRepeatRule: Codable, Hashable {
self.excludedDates = [EncryptedDateWrapper]()
}

if let advancedRules = try container.decodeIfPresent([EncryptedAdvancedRuleWrapper].self, forKey: .advancedRules) {
self.advancedRules = advancedRules
} else {
self.advancedRules = [EncryptedAdvancedRuleWrapper]()
}

self.frequency = try container.decode(Base64.self, forKey: .frequency)
self.interval = try container.decode(Base64.self, forKey: .interval)
self.timeZone = try container.decode(Base64.self, forKey: .timeZone)
Expand Down
4 changes: 2 additions & 2 deletions app-ios/TutanotaSharedFramework/Utils/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ public func translate(_ key: String, default defaultValue: String) -> String {
}

// // keep in sync with src/native/main/NativePushServiceApp.ts
let SYS_MODEL_VERSION = 99
let SYS_MODEL_VERSION = 118

// api/entities/tutanota/ModelInfo.ts
// FIXME there are at least 5 places needs manual sync for these version numbers.
// Definitely need a script to automate.
public let TUTANOTA_MODEL_VERSION: UInt32 = 71
public let TUTANOTA_MODEL_VERSION: UInt32 = 80

public func addSystemModelHeaders(to headers: inout [String: String]) { headers["v"] = String(SYS_MODEL_VERSION) }
public func addTutanotaModelHeaders(to headers: inout [String: String]) { headers["v"] = String(TUTANOTA_MODEL_VERSION) }
Expand Down