Release 2.30.0 (#3673)

## [2.30.0] - 2025-01-01

Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel,
@KristjanESPERANTO, @rejas, @sdetweil.

> ⚠️ This release needs nodejs version `v20` or `v22 or higher`, minimum
version is `v20.18.1`

### Added

- [core] Add wayland and windows start options to `package.json` (#3594)
- [docs] Add step for npm publishing in release process (#3595)
- [core] Add GitHub workflow to run spellcheck a few days before each
release (#3623)
- [core] Add test flag to `index.html` to pass to module js for test
mode detection (needed by #3630)
- [core] Add export on animation names (#3644)
- [compliments] Add support for refreshing remote compliments file, and
test cases (#3630)
- [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9
(#3586)
- [linter] Re-activate `eslint-plugin-package-json` to lint
`package.json` (#3643)
- [linter] Add linting for markdown files (#3646)
- [linter] Add some handy ESLint rules.
- [calendar] Add ability to display end date for full date events, where
end is not same day (showEnd=true) (#3650)
- [core] Add text to the config.js.sample file about the locale variable
(#3654, #3655)
- [core] Add fetch timeout for all node_helpers (thru undici, forces
node 20.18.1 minimum) to help on slower systems. (#3660) (3661)

### Changed

- [core] Run code style checks in workflow only once (#3648)
- [core] Fix animations export #3644 only on server side (#3649)
- [core] Use project URL in fallback config (#3656)
- [core] Fix Access Denied crash writing js/positions.js (on synology
nas) #3651. new message, MM starts, but no modules showing (#3652)
- [linter] Switch to 'npx' for lint-staged in pre-commit hook (#3658)

### Removed

- [tests] Remove `node-pty` and `drivelist` from rebuilded test (#3575)
- [deps] Remove `@eslint/js` dependency. Already installed with `eslint`
in deep (#3636)

### Updated

- [repo] Reactivate `stale.yaml` as GitHub action to mark issues as
stale after 60 days and close them 7 days later (if no activity) (#3577,
#3580, #3581)
- [core] Update electron dependency to v32 (test electron rebuild) and
all other dependencies too (#3657)
- [tests] All test configs have been updated to allow full external
access, allowing for easier debugging (especially when running as a
container)
- [core] Run and test with node 23 (#3588)
- [workflow] delete exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in
`dep-review.yaml` (#3659)

### Fixed

- [updatenotification] Fix pm2 using detection when pm2 script is inside
or outside MagicMirror root folder (#3576) (#3605) (#3626) (#3628)
- [core] Fix loading node_helper of modules: avoid black screen, display
errors and continue loading with next module (#3578)
- [weather] Change default value for weatherEndpoint of provider
openweathermap to "/onecall" (#3574)
- [tests] Fix electron tests with mock dates, the mock on server side
was missing (#3597)
- [tests] Fix testcases with hard coded Date.now (#3597)
- [core] Fix missing `basePath` where `location.host` is used (#3613)
- [compliments] croner library changed filenames used in latest version
(#3624)
- [linter] Fix ESLint ignore pattern which caused that default modules
not to be linted (#3632)
- [core] Fix module path in case of sub/sub folder is used and use
path.resolve for resolve `moduleFolder` and `defaultModuleFolder` in
app.js (#3653)
- [calendar] Update to resolve issues #3098 #3144 #3351 #3422 #3443
#3467 #3537 related to timezone changes
- [calendar] Fix #3267 (styles array), also fixes event with both exdate
AND recurrence(and testcase)
- [calendar] Fix showEndsOnlyWithDuration not working, #3598, applies
ONLY to full day events
- [calendar] Fix showEnd for Full Day events (#3602)
- [tests] Suppress "module is not defined" in e2e tests (#3647)
- [calendar] Fix #3267 (styles array, really this time!)
- [core] Fix #3662 js/positions.js created incorrectly

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Michael Teeuw <michael@xonaymedia.nl>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: jkriegshauser <joshuakr@nvidia.com>
Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com>
Co-authored-by: vppencilsharpener <tim.pray@gmail.com>
Co-authored-by: veeck <michael.veeck@nebenan.de>
Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com>
Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com>
Co-authored-by: Jason Stieber <jrstieber@gmail.com>
Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com>
Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com>
Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com>
Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com>
Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com>
Co-authored-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: veeck <gitkraken@veeck.de>
This commit is contained in:
sam detweiler
2025-01-01 08:27:27 -06:00
committed by GitHub
parent 94c3c699e8
commit c24de64d77
145 changed files with 4895 additions and 1915 deletions

View File

@@ -168,12 +168,17 @@ Module.register("calendar", {
this.selfUpdate();
},
notificationReceived (notification, payload, sender) {
if (notification === "FETCH_CALENDAR") {
if (this.hasCalendarURL(payload.url)) {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
}
},
// Override socket notification handler.
socketNotificationReceived (notification, payload) {
if (notification === "FETCH_CALENDAR") {
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
}
if (this.identifier !== payload.id) {
return;
@@ -417,18 +422,26 @@ Module.register("calendar", {
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
// Add end time if showEnd
if (this.config.showEnd) {
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
// no duration here, don't display end
} else {
// and has a duation
if (event.startDate !== event.endDate) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
}
}
// For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
event.endDate -= ONE_SECOND;
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
// only show end if requested and allowed and the dates are different
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && moment(event.startDate, "x").format("YYYYMMDD") !== moment(event.endDate, "x").format("YYYYMMDD")) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat));
} else
if ((moment(event.startDate, "x").format("YYYYMMDD") !== moment(event.endDate, "x").format("YYYYMMDD")) && (moment(event.startDate, "x") < moment(now, "x"))) {
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(now, "x").format(this.config.fullDayEventDateFormat));
}
} else if (this.config.getRelative > 0 && event.startDate < now) {
// Ongoing and getRelative is set
timeWrapper.innerHTML = CalendarUtils.capFirst(
@@ -460,16 +473,18 @@ Module.register("calendar", {
if (event.startDate >= now || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
// Use relative time
if (!this.config.hideTime && !event.fullDayEvent) {
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
Log.debug("event not hidden and not fullday");
timeWrapper.innerHTML = `${CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }))}`;
} else {
timeWrapper.innerHTML = CalendarUtils.capFirst(
Log.debug("event full day or hidden");
timeWrapper.innerHTML = `${CalendarUtils.capFirst(
moment(event.startDate, "x").calendar(null, {
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
nextDay: `[${this.translate("TOMORROW")}]`,
nextWeek: "dddd",
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
})
);
)}`;
}
if (event.fullDayEvent) {
// Full days events within the next two days
@@ -488,9 +503,11 @@ Module.register("calendar", {
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
Log.info("event fullday");
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
Log.info("not full day but within getrelative size");
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
timeWrapper.innerHTML = `${CalendarUtils.capFirst(moment(event.startDate, "x").fromNow())}`;
}
} else {
// Ongoing event
@@ -603,6 +620,7 @@ Module.register("calendar", {
const calendar = this.calendarData[calendarUrl];
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
let by_url_calevents = [];
for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
@@ -620,9 +638,6 @@ Module.register("calendar", {
if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
continue;
}
if (--remainingEntries < 0) {
break;
}
}
event.url = calendarUrl;
@@ -667,15 +682,21 @@ Module.register("calendar", {
for (let splitEvent of splitEvents) {
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
events.push(splitEvent);
by_url_calevents.push(splitEvent);
}
}
} else {
events.push(event);
by_url_calevents.push(event);
}
}
by_url_calevents.sort(function (a, b) {
return a.startDate - b.startDate;
});
Log.debug(`pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`);
events = events.concat(by_url_calevents.slice(0, remainingEntries));
Log.debug(`events for calendar=${events.length}`);
}
Log.info(`sorting events count=${events.length}`);
events.sort(function (a, b) {
return a.startDate - b.startDate;
});
@@ -715,7 +736,7 @@ Module.register("calendar", {
}
events = newEvents;
}
Log.info(`slicing events total maxcount=${this.config.maximumEntries}`);
return events.slice(0, this.config.maximumEntries);
},
@@ -886,9 +907,9 @@ Module.register("calendar", {
let p = this.getCalendarProperty(url, property, defaultValue);
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
p = className + p;
if (p instanceof Array) p.push(className);
else p = className + p;
}
if (!(p instanceof Array)) p = [p];
return p;
},

View File

@@ -56,7 +56,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try {
data = ical.parseICS(responseData);
Log.debug(`parsed data=${JSON.stringify(data)}`);
Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`);
events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents,
includePastEvents,

View File

@@ -160,7 +160,7 @@ const CalendarFetcherUtils = {
}
if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event)}`);
Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`);
let startMoment = eventDate(event, "start");
let endMoment;
@@ -246,6 +246,8 @@ const CalendarFetcherUtils = {
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
let d1;
let d2;
if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
const rule = event.rrule;
@@ -261,9 +263,10 @@ const CalendarFetcherUtils = {
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal;
let futureLocal;
if (CalendarFetcherUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if full day event, only use the date part of the ranges
@@ -283,52 +286,52 @@ const CalendarFetcherUtils = {
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null;
const oneDayInMs = 24 * 60 * 60 * 1000;
d1 = new Date(new Date(pastLocal.valueOf() - oneDayInMs).getTime());
d2 = new Date(new Date(futureLocal.valueOf() + oneDayInMs).getTime());
Log.debug(`Search for recurring events between: ${d1} and ${d2}`);
event.start = rule.options.dtstart;
Log.debug("fix rrule start=", rule.options.dtstart);
Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate);
// fixup the exdate and recurrence date to local time too for post between() handling
CalendarFetcherUtils.fixEventtoLocal(event);
Log.debug(`RRule: ${rule.toString()}`);
rule.options.tzid = null; // RRule gets *very* confused with timezones
let dates = rule.between(new Date(pastLocal.valueOf() - oneDayInMs), new Date(futureLocal.valueOf() + oneDayInMs), true, () => { return true; });
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
let dates = rule.between(d1, d2, true, () => { return true; });
Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`);
// shouldn't need this anymore, as RRULE not passed junk
dates = dates.filter((d) => {
if (JSON.stringify(d) === "null") return false;
else return true;
});
// RRule can generate dates with an incorrect recurrence date. Process the array here and apply date correction.
if (hasByWeekdayRule) {
Log.debug("Rule has byweekday, checking for correction");
dates.forEach((date, index, arr) => {
// NOTE: getTimezoneOffset() is negative of the expected value. For America/Los_Angeles under DST (GMT-7),
// this value is +420. For Australia/Sydney under DST (GMT+11), this value is -660.
const tzOffset = date.getTimezoneOffset() / 60;
const hour = date.getHours();
if ((tzOffset < 0) && (hour < -tzOffset)) { // east of GMT
Log.debug(`East of GMT (tzOffset: ${tzOffset}) and hour=${hour} < ${-tzOffset}, Subtracting 1 day from ${date}`);
arr[index] = new Date(date.valueOf() - oneDayInMs);
} else if ((tzOffset > 0) && (hour >= (24 - tzOffset))) { // west of GMT
Log.debug(`West of GMT (tzOffset: ${tzOffset}) and hour=${hour} >= 24-${tzOffset}, Adding 1 day to ${date}`);
arr[index] = new Date(date.valueOf() + oneDayInMs);
}
});
// Adjusting the dates could push it beyond the 'until' date, so filter those out here.
if (rule.options.until !== null) {
dates = dates.filter((date) => {
return date.valueOf() <= rule.options.until.valueOf();
});
}
}
// The dates array from rrule can be confused by DST. If the event was created during DST and we
// are querying a date range during non-DST, rrule can have the incorrect time for the date range.
// Reprocess the array here computing and applying the time offset.
dates.forEach((date, index, arr) => {
let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
if (adjustHours !== 0) {
Log.debug(`Applying timezone adjustment hours=${adjustHours} to ${date}`);
arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000));
}
// go thru all the rrule.between() dates and put back the tz offset removed so rrule.between would work
let datesLocal = [];
let offset = d1.getTimezoneOffset();
Log.debug("offset =", offset);
dates.forEach((d) => {
let dtext = d.toISOString().slice(0, -5);
Log.debug(" date text form without tz=", dtext);
let dLocal = new Date(d.valueOf() + (offset * 60000));
let offset2 = dLocal.getTimezoneOffset();
Log.debug("date after offset applied=", dLocal);
if (offset !== offset2) {
// woops, dst/std switch
let delta = offset - offset2;
Log.debug("offset delta=", delta);
dLocal = new Date(d.valueOf() + ((offset - delta) * 60000));
Log.debug("corrected normalized date=", dLocal);
} else Log.debug(" neutralized date=", dLocal);
datesLocal.push(dLocal);
});
dates = datesLocal;
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
@@ -337,29 +340,22 @@ const CalendarFetcherUtils = {
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
Log.debug(`event.recurrences: ${event.recurrences}`);
//
// i don't think we will ever see this anymore (oct 2024) due to code fixes for rrule.between()
//
Log.debug("event.recurrences:", event.recurrences);
if (event.recurrences !== undefined) {
for (let dateKey in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don't double-add those events.
let d = new Date(dateKey);
if (!moment(d).isBetween(pastMoment, futureMoment)) {
if (!moment(d).isBetween(d1, d2)) {
Log.debug("adding recurring event not found in between list =", d, " should not happen now using local dates oct 17,24");
dates.push(d);
}
}
}
// Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure
// inclusion here. Unfortunately dates.includes() doesn't find it so we have to do forEach().
{
let found = false;
dates.forEach((d) => { if (d.valueOf() === event.start.valueOf()) found = true; });
if (!found) {
Log.debug(`event.start=${event.start} was not included in results from rrule; adding`);
dates.splice(0, 0, event.start);
}
}
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
@@ -367,30 +363,42 @@ const CalendarFetcherUtils = {
let curDurationMs = durationMs;
let showRecurrence = true;
startMoment = moment(date);
let startMoment = moment(date);
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
// This must be done after `date` is adjusted
const dateKey = date.toISOString().substring(0, 10);
let dateKey = CalendarFetcherUtils.getDateKeyFromDate(date);
Log.debug("event date dateKey=", dateKey);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
startMoment = moment(curEvent.start);
curDurationMs = curEvent.end.valueOf() - startMoment.valueOf();
if (curEvent.recurrences !== undefined) {
Log.debug("have recurrences=", curEvent.recurrences);
if (curEvent.recurrences[dateKey] !== undefined) {
Log.debug("have a recurrence match for dateKey=", dateKey);
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
curEvent.start = new Date(new Date(curEvent.start.valueOf()).getTime());
curEvent.end = new Date(new Date(curEvent.end.valueOf()).getTime());
startMoment = CalendarFetcherUtils.getAdjustedStartMoment(curEvent.start, event);
endMoment = CalendarFetcherUtils.getAdjustedStartMoment(curEvent.end, event);
date = curEvent.start;
curDurationMs = new Date(endMoment).valueOf() - startMoment.valueOf();
} else {
Log.debug("recurrence key ", dateKey, " doesn't match");
}
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
if (curEvent.exdate !== undefined) {
Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate);
if (curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
}
Log.debug(`duration: ${curDurationMs}`);
startMoment = CalendarFetcherUtils.getAdjustedStartMoment(date, event);
endMoment = moment(startMoment.valueOf() + curDurationMs);
if (startMoment.valueOf() === endMoment.valueOf()) {
endMoment = endMoment.endOf("day");
}
@@ -408,7 +416,7 @@ const CalendarFetcherUtils = {
}
if (showRecurrence === true) {
Log.debug(`saving event: ${description}`);
Log.debug(`saving event: ${recurrenceTitle}`);
newEvents.push({
title: recurrenceTitle,
startDate: startMoment.format("x"),
@@ -421,7 +429,10 @@ const CalendarFetcherUtils = {
geo: geo,
description: description
});
} else {
Log.debug("not saving event ", recurrenceTitle, new Date(startMoment));
}
Log.debug(" ");
}
// End recurring event parsing.
} else {
@@ -472,7 +483,9 @@ const CalendarFetcherUtils = {
startDate: startMoment.add(adjustHours, "hours").format("x"),
endDate: endMoment.add(adjustHours, "hours").format("x"),
fullDayEvent: fullDayEvent,
recurringEvent: false,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
@@ -488,6 +501,200 @@ const CalendarFetcherUtils = {
return newEvents;
},
/**
* fixup thew event fields that have dates to use local time
* BEFORE calling rrule.between
* @param the event being processed
* @returns nothing
*/
fixEventtoLocal (event) {
// if there are excluded dates, their date is incorrect and possibly key as well.
if (event.exdate !== undefined) {
Object.keys(event.exdate).forEach((dateKey) => {
// get the date
let exdate = event.exdate[dateKey];
Log.debug("exdate w key=", exdate);
//exdate=CalendarFetcherUtils.convertDateToLocalTime(exdate, event.end.tz)
exdate = new Date(new Date(exdate.valueOf() - ((120 * 60 * 1000))).getTime());
Log.debug("new exDate item=", exdate, " with old key=", dateKey);
let newkey = exdate.toISOString().slice(0, 10);
if (newkey !== dateKey) {
Log.debug("new exDate item=", exdate, ` key=${newkey}`);
event.exdate[newkey] = exdate;
//delete event.exdate[dateKey]
}
});
Log.debug("updated exdate list=", event.exdate);
}
if (event.recurrences) {
Object.keys(event.recurrences).forEach((dateKey) => {
let exdate = event.recurrences[dateKey];
//exdate=new Date(new Date(exdate.valueOf()-(60*60*1000)).getTime())
Log.debug("new recurrence item=", exdate, " with old key=", dateKey);
exdate.start = CalendarFetcherUtils.convertDateToLocalTime(exdate.start, exdate.start.tz);
exdate.end = CalendarFetcherUtils.convertDateToLocalTime(exdate.end, exdate.end.tz);
Log.debug("adjusted recurringEvent start=", exdate.start, " end=", exdate.end);
});
}
Log.debug("modified recurrences before rrule.between", event.recurrences);
},
/**
* convert a UTC date to local time
* BEFORE calling rrule.between
* @param date ti conert
* tz event is currently in
* @returns updated date object
*/
convertDateToLocalTime (date, tz) {
let delta_tz_offset = 0;
let now_offset = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess());
let event_offset = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(tz);
Log.debug("date to convert=", date);
if (Math.sign(now_offset) !== Math.sign(event_offset)) {
delta_tz_offset = Math.abs(now_offset) + Math.abs(event_offset);
} else {
// signs are the same
// if negative
if (Math.sign(now_offset) === -1) {
// la looking at chicago
if (now_offset < event_offset) { // 5 -7
delta_tz_offset = now_offset - event_offset;
}
else { //7 -5 , chicago looking at LA
delta_tz_offset = event_offset - now_offset;
}
}
else {
// berlin looking at sydney
if (now_offset < event_offset) { // 5 -7
delta_tz_offset = event_offset - now_offset;
Log.debug("less delta=", delta_tz_offset);
}
else { // 11 - 2, sydney looking at berlin
delta_tz_offset = -(now_offset - event_offset);
Log.debug("more delta=", delta_tz_offset);
}
}
}
const newdate = new Date(new Date(date.valueOf() + (delta_tz_offset * 60 * 1000)).getTime());
Log.debug("modified date =", newdate);
return newdate;
},
/**
* get the exdate/recurrence hash key from the date object
* BEFORE calling rrule.between
* @param the date of the event
* @returns string date key YYYY-MM-DD
*/
getDateKeyFromDate (date) {
// get our runtime timezone offset
const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess());
let startday = date.getDate();
let adjustment = 0;
Log.debug(" day of month=", (`0${startday}`).slice(-2), " nowDiff=", nowDiff, ` start time=${date.toString().split(" ")[4].slice(0, 2)}`);
Log.debug("date string= ", date.toString());
Log.debug("date iso string ", date.toISOString());
// if the dates are different
if (date.toString().slice(8, 10) < date.toISOString().slice(8, 10)) {
startday = date.toString().slice(8, 10);
Log.debug("< ", startday);
} else { // tostring is more
if (date.toString().slice(8, 10) > date.toISOString().slice(8, 10)) {
startday = date.toISOString().slice(8, 10);
Log.debug("> ", startday);
}
}
return date.toISOString().substring(0, 8) + (`0${startday}`).slice(-2);
},
/**
* get the timezone offset from the timezone string
*
* @param the timezone string
* @returns the numerical offset
*/
getTimezoneOffsetFromTimezone (timeZone) {
const str = new Date().toLocaleString("en", { timeZone, timeZoneName: "longOffset" });
Log.debug("tz offset=", str);
const [_, h, m] = str.match(/([+-]\d+):(\d+)$/) || ["", "+00", "00"];
return h * 60 + (h > 0 ? +m : -m);
},
/**
* fixup the date start moment after rrule.between returns date array
*
* @param date object from rrule.between results
* the event object it came from
* @returns moment object
*/
getAdjustedStartMoment (date, event) {
let startMoment = moment(date);
Log.debug("startMoment pre=", startMoment);
// get our runtime timezone offset
const nowDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(moment.tz.guess()); // 10/18 16:49, 300
let eventDiff = CalendarFetcherUtils.getTimezoneOffsetFromTimezone(event.end.tz); // watch out, start tz is cleared to handle rrule 120 23:49
Log.debug("tz diff event=", eventDiff, " local=", nowDiff, " end event timezone=", event.end.tz);
// if the diffs are different (not same tz for processing as event)
if (nowDiff !== eventDiff) {
// if signs are different
if (Math.sign(nowDiff) !== Math.sign(eventDiff)) {
// its the accumulated total
Log.debug("diff signs, accumulate");
eventDiff = Math.abs(eventDiff) + Math.abs(nowDiff);
// sign of diff depends on where you are looking at which event.
// australia looking at US, add to get same time
Log.debug("new different event diff=", eventDiff);
if (Math.sign(nowDiff) === -1) {
eventDiff *= -1;
// US looking at australia event have to subtract
Log.debug("new diff, same sign, total event diff=", eventDiff);
}
}
else {
// signs are the same, all east of UTC or all west of UTC
// if the signs are negative (west of UTC)
Log.debug("signs are the same");
if (Math.sign(eventDiff) === -1) {
//if west, looking at more west
if (nowDiff < eventDiff) {
//-600 -420
eventDiff = -(eventDiff - (nowDiff - eventDiff)); //-180
Log.debug("now looking back east delta diff=", eventDiff);
}
else {
Log.debug("now looking more west");
eventDiff = Math.abs(eventDiff - nowDiff);
}
} else {
Log.debug("signs are both positive");
// signs are positive (east of UTC)
// berlin < sydney
if (nowDiff < eventDiff) {
// germany vs australia
eventDiff = -(eventDiff - nowDiff);
}
else {
// australia vs germany
//eventDiff = eventDiff; //- nowDiff
}
}
}
startMoment = moment.tz(new Date(date.valueOf() + (eventDiff * (60 * 1000))), event.end.tz);
} else {
Log.debug("same tz event and display");
eventDiff = 0;
startMoment = moment.tz(new Date(date.valueOf() - (eventDiff * (60 * 1000))), event.end.tz);
}
Log.debug("startMoment post=", startMoment);
return startMoment;
},
/**
* Lookup iana tz from windows
* @param {string} msTZName the timezone name to lookup