diff --git a/BusyMirror.xcodeproj/project.pbxproj b/BusyMirror.xcodeproj/project.pbxproj index 630b6bc..1c59e92 100644 --- a/BusyMirror.xcodeproj/project.pbxproj +++ b/BusyMirror.xcodeproj/project.pbxproj @@ -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; diff --git a/BusyMirror/BlockMath.swift b/BusyMirror/BlockMath.swift index a87da91..3c07269 100644 --- a/BusyMirror/BlockMath.swift +++ b/BusyMirror/BlockMath.swift @@ -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) } } diff --git a/BusyMirror/MirrorEngine.swift b/BusyMirror/MirrorEngine.swift index 994bf92..f7de638 100644 --- a/BusyMirror/MirrorEngine.swift +++ b/BusyMirror/MirrorEngine.swift @@ -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 diff --git a/BusyMirrorTests/BlockMathTests.swift b/BusyMirrorTests/BlockMathTests.swift index a5ea06e..42250a9 100644 --- a/BusyMirrorTests/BlockMathTests.swift +++ b/BusyMirrorTests/BlockMathTests.swift @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b391e..8596553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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