Skip to content
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
71 changes: 39 additions & 32 deletions defaultmodules/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,20 @@ Module.register("calendar", {
return moment(timestamp, "x").tz(moment.tz.guess());
},

/**
* Sets the relative day flags (today, yesterday, ...) on an event based on its day.
* @param {object} event The event to flag.
* @param {moment.Moment} dayMoment The day the event belongs to.
* @param {moment.Moment} now The current moment.
*/
setRelativeDayFlags (event, dayMoment, now) {
event.today = dayMoment.isSame(now, "d");
event.dayBeforeYesterday = dayMoment.isSame(now.clone().subtract(2, "days"), "d");
event.yesterday = dayMoment.isSame(now.clone().subtract(1, "days"), "d");
event.tomorrow = dayMoment.isSame(now.clone().add(1, "days"), "d");
event.dayAfterTomorrow = dayMoment.isSame(now.clone().add(2, "days"), "d");
},

/**
* Creates the sorted list of all events.
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
Expand Down Expand Up @@ -503,43 +517,36 @@ Module.register("calendar", {
}

event.url = calendarUrl;
event.today = eventStartDateMoment.isSame(now, "d");
event.dayBeforeYesterday = eventStartDateMoment.isSame(now.clone().subtract(2, "days"), "d");
event.yesterday = eventStartDateMoment.isSame(now.clone().subtract(1, "days"), "d");
event.tomorrow = eventStartDateMoment.isSame(now.clone().add(1, "days"), "d");
event.dayAfterTomorrow = eventStartDateMoment.isSame(now.clone().add(2, "days"), "d");
this.setRelativeDayFlags(event, eventStartDateMoment, now);

/*
* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
* If sliceMultiDayEvents is enabled, an event spanning several calendar days is split into one entry per day.
* Otherwise, esp. in dateheaders mode, it is not clear how long these events are.
* dayCount is the number of calendar days the event touches (an end exactly at midnight does not add a day).
*/
const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days");
if (this.config.sliceMultiDayEvents && maxCount > 1) {
const eventStartDay = eventStartDateMoment.clone().startOf("day");
const eventEndDay = eventEndDateMoment.clone().startOf("day");
const endsAtMidnight = !eventEndDateMoment.isAfter(eventEndDay);
const dayCount = eventEndDay.diff(eventStartDay, "days") + (endsAtMidnight ? 0 : 1);
if (this.config.sliceMultiDayEvents && dayCount > 1) {
const splitEvents = [];
let midnight
= eventStartDateMoment
.clone()
.startOf("day")
.add(1, "day")
.endOf("day");
let count = 1;
while (eventEndDateMoment.isAfter(midnight)) {
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d");
thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d");
thisEvent.endDate = midnight.clone().subtract(1, "day").format("x");
thisEvent.title += ` (${count}/${maxCount})`;
splitEvents.push(thisEvent);

event.startDate = midnight.clone().startOf("day").format("x"); // start next slice at 00:00, not 23:59
count += 1;
midnight = midnight.clone().add(1, "day").endOf("day"); // next day
// Each slice covers one day: it starts at the event start (first slice) or midnight,
// and ends at the event end (last slice) or one millisecond before the next midnight.
let sliceStart = eventStartDateMoment.clone();

for (let dayNumber = 1; dayNumber <= dayCount; dayNumber++) {
const isLastSlice = dayNumber === dayCount;
const nextMidnight = sliceStart.clone().startOf("day").add(1, "day");

const slice = JSON.parse(JSON.stringify(event)); // clone object
slice.startDate = sliceStart.format("x");
slice.endDate = isLastSlice ? event.endDate : nextMidnight.clone().subtract(1, "millisecond").format("x");
slice.title = `${event.title} (${dayNumber}/${dayCount})`;
this.setRelativeDayFlags(slice, sliceStart, now);
splitEvents.push(slice);

sliceStart = nextMidnight;
}
// Last day
event.title += ` (${count}/${maxCount})`;
event.today += this.timestampToMoment(event.startDate).isSame(now, "d");
event.tomorrow = this.timestampToMoment(event.startDate).isSame(now.clone().add(1, "days"), "d");
splitEvents.push(event);

for (const splitEvent of splitEvents) {
if (this.timestampToMoment(splitEvent.endDate).isAfter(now) && this.timestampToMoment(splitEvent.endDate).isSameOrBefore(future)) {
Expand Down
33 changes: 33 additions & 0 deletions tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumEntries: 100,
showEnd: true,
sliceMultiDayEvents: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/calendar_test_slice_multiday_ends_midnight.ics"
}
]
}
}
]
};

/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
14 changes: 14 additions & 0 deletions tests/electron/modules/calendar_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ describe("Calendar module", () => {
await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestCount()).resolves.toBe(6);
});

it("counts all touched dates across DST, not just elapsed 24h blocks", async () => {
// Event runs from 2024-10-25 to 2024-10-28 in Europe/Berlin, crossing the DST change.
// It touches 4 calendar dates: Fri, Sat, Sun, Mon.
await startCalendarShowEndScenario("slice_multiday_timed_start_midnight", "25 Oct 2024 06:00:00 GMT", "Europe/Berlin");
await expect(doTestCount()).resolves.toBe(4);
});

it("does not create an extra slice when an event ends exactly at 00:00", async () => {
// Event runs Fri 12:00 -> Mon 00:00. It should cover Fri, Sat, Sun only; Monday is not touched.
await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js", "25 Oct 2024 06:00:00 GMT", [], "GMT");
await expect(doTestCount()).resolves.toBe(3);
await expect(doTestTableContent(".calendar .event", ".title", "(3/3)", last)).resolves.toBe(true);
});
});

describe("sliceMultiDayEvents slice start time", () => {
Expand Down
16 changes: 16 additions & 0 deletions tests/mocks/calendar_test_slice_multiday_ends_midnight.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//MagicMirror//slice regression//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART:20241025T120000Z
DTEND:20241028T000000Z
DTSTAMP:20241024T153358Z
UID:slice-midnight-end-regression@magicmirror.test
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Slice
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR