Compare commits

...
1 Commits
Author SHA1 Message Date
tomas.kracmarandClaude Sonnet 5 e45a7dbe81 Release 1.10.0
Mirror "Maybe" (tentative RSVP) events instead of dropping them.

Previously, with "Mirror accepted events only" on, events you'd replied
tentative to were skipped entirely. They're now mirrored with a "Maybe: "
marker in the title (after the normal prefix) and written with tentative
availability so Calendar shows them hatched. The marker is applied for any
tentative RSVP regardless of the accepted-only setting; an accepted<->
tentative flip re-syncs the title. Not carried on the merge path
(mergeGapMin > 0), which already discards per-event titles.

Block gains a `tentative` flag (in ==/hash); accepted-only gate lets
.tentative through alongside .accepted.

All unit tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 15:30:55 +02:00
5 changed files with 32 additions and 11 deletions
+4 -4
View File
@@ -410,7 +410,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 31;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusyMirror/Info.plist;
@@ -421,7 +421,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.9.2;
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -440,7 +440,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 31;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusyMirror/Info.plist;
@@ -451,7 +451,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.9.2;
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
+5 -1
View File
@@ -8,6 +8,8 @@ struct Block: Hashable {
let notes: String? // source notes (for optional copy)
let occurrence: Date? // occurrenceDate for recurring instances
let alarmOffsets: [TimeInterval]? // relative alarm offsets copied from source event
var tentative: Bool = false // source "Maybe" RSVP; mirrored with a marker
// ponytail: lost on the merge path (mergeGapMin > 0) along with the title; only tracked in per-event mode
/// Convenience factory for time-only blocks (used internally for occupancy tracking).
static func span(start: Date, end: Date) -> Block {
@@ -22,7 +24,8 @@ struct Block: Hashable {
lhs.srcStableID == rhs.srcStableID &&
lhs.label == rhs.label &&
lhs.notes == rhs.notes &&
lhs.occurrence == rhs.occurrence
lhs.occurrence == rhs.occurrence &&
lhs.tentative == rhs.tentative
}
func hash(into hasher: inout Hasher) {
@@ -32,6 +35,7 @@ struct Block: Hashable {
hasher.combine(label)
hasher.combine(notes)
hasher.combine(occurrence)
hasher.combine(tentative)
}
}
+10 -6
View File
@@ -145,10 +145,12 @@ final class MirrorEngine {
var skippedStatus = 0
for ev in srcEvents {
if Task.isCancelled { break }
let myStatus = ev.attendees?.first(where: { $0.isCurrentUser })?.participantStatus
let isTentative = (myStatus == .tentative)
if config.mirrorAcceptedOnly, ev.hasAttendees {
let attendees = ev.attendees ?? []
if let me = attendees.first(where: { $0.isCurrentUser }) {
if me.participantStatus != .accepted {
if let me = ev.attendees?.first(where: { $0.isCurrentUser }) {
// "Maybe" replies still get mirrored, just marked (see displayTitle below)
if me.participantStatus != .accepted && me.participantStatus != .tentative {
skippedStatus += 1
continue
}
@@ -178,7 +180,7 @@ final class MirrorEngine {
guard let s = ev.startDate, let e = ev.endDate, e > s else { continue }
guard ev.calendar.calendarIdentifier == srcCal.calendarIdentifier else { continue }
let srcID = stableSourceIdentifier(for: ev)
srcBlocks.append(Block(start: s, end: e, srcStableID: srcID, label: ev.title, notes: ev.notes, occurrence: ev.occurrenceDate, alarmOffsets: alarmOffsets(for: ev)))
srcBlocks.append(Block(start: s, end: e, srcStableID: srcID, label: ev.title, notes: ev.notes, occurrence: ev.occurrenceDate, alarmOffsets: alarmOffsets(for: ev), tentative: isTentative))
}
if skippedMirrors > 0 {
log("- SKIP mirrored-on-source: \(skippedMirrors) instance(s)")
@@ -379,7 +381,8 @@ final class MirrorEngine {
let baseSourceTitle = stripPrefix(blk.label, prefix: config.titlePrefix)
let effectiveTitle = config.hideDetails ? config.placeholderTitle : (baseSourceTitle.isEmpty ? config.placeholderTitle : baseSourceTitle)
let titleSuffix = config.hideDetails ? "" : (baseSourceTitle.isEmpty ? "" : " — \(baseSourceTitle)")
let displayTitle = (config.titlePrefix.isEmpty ? "" : config.titlePrefix) + effectiveTitle
let maybeMark = blk.tentative ? "Maybe: " : ""
let displayTitle = (config.titlePrefix.isEmpty ? "" : config.titlePrefix) + maybeMark + effectiveTitle
let notes = desiredNotes(for: blk)
let desiredURL = buildMirrorURL(
targetCalID: tgt.calendarIdentifier,
@@ -409,6 +412,7 @@ final class MirrorEngine {
return
}
existing.title = displayTitle
existing.availability = blk.tentative ? .tentative : .busy
existing.startDate = blk.start
existing.endDate = blk.end
existing.isAllDay = false
@@ -467,7 +471,7 @@ final class MirrorEngine {
newEv.notes = notes
newEv.url = desiredURL
newEv.alarms = desiredAlarms(for: blk)
newEv.availability = .busy
newEv.availability = blk.tentative ? .tentative : .busy
do {
try store.save(newEv, span: .thisEvent, commit: true)
created += 1
+8
View File
@@ -165,6 +165,14 @@ final class BlockMathTests: XCTestCase {
XCTAssertEqual(result.count, 2)
}
func testTentativeAffectsEqualityAndHash() {
let base = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: "x", notes: nil, occurrence: d, alarmOffsets: nil)
var maybe = base
maybe.tentative = true
XCTAssertNotEqual(base, maybe)
XCTAssertNotEqual(base.hashValue, maybe.hashValue)
}
func testUniqueBlocksByIDDifferentOccurrence() {
let b1 = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: nil, notes: nil, occurrence: d, alarmOffsets: nil)
let b2 = Block(start: d, end: d.addingTimeInterval(600), srcStableID: "a", label: nil, notes: nil, occurrence: d.addingTimeInterval(3600), alarmOffsets: nil)
+5
View File
@@ -2,6 +2,11 @@
All notable changes to BusyMirror will be documented in this file.
## [1.10.0] - 2026-08-31
### Added
- **"Maybe" events are now mirrored.** Previously, when "Mirror accepted events only" was on, events you'd RSVP'd tentative to were dropped entirely. They're now mirrored with a `Maybe: ` marker in the title (after the normal prefix, e.g. `🪞 Maybe: Standup`) and written with tentative availability so Calendar shows them hatched. Applies regardless of the accepted-only setting — any tentative RSVP gets the marker. A flip between accepted and tentative re-syncs the title. Not carried on the merge path (`mergeGapMin > 0`), which already discards per-event titles. ([MirrorEngine.swift](BusyMirror/MirrorEngine.swift), [BlockMath.swift](BusyMirror/BlockMath.swift))
## [1.9.2] - 2026-08-27
### Fixed