Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e45a7dbe81 |
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user