From 6dd2a123f835c926b2383d65a42ec48f5b7c45c3 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:15:47 +0200 Subject: [PATCH 01/17] calendar: Support for minimum refresh interval --- .../calendar/parent/ext-calendar-provider.js | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 3bb60a3..4bb1f90 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -456,11 +456,39 @@ this.calendar_provider = class extends ExtensionAPI { .QueryInterface(Ci.nsIResProtocolHandler) .setSubstitution("tb-experiments-calendar", this.extension.rootURI); - const { setupE10sBrowser } = ChromeUtils.importESModule("resource://tb-experiments-calendar/experiments/calendar/ext-calendar-utils.sys.mjs"); + const { setupE10sBrowser, unwrapCalendar } = ChromeUtils.importESModule("resource://tb-experiments-calendar/experiments/calendar/ext-calendar-utils.sys.mjs"); ChromeUtils.registerWindowActor("CalendarProvider", { child: { esModuleURI: "resource://tb-experiments-calendar/experiments/calendar/child/ext-calendar-provider-actor.sys.mjs" } }); - ExtensionSupport.registerWindowListener("ext-calendar-provider-" + this.extension.id, { + ExtensionSupport.registerWindowListener("ext-calendar-provider-properties-" + this.extension.id, { + chromeURLs: ["chrome://calendar/content/calendar-properties-dialog.xhtml"], + onLoadWindow: (win) => { + const calendar = unwrapCalendar(win.arguments[0].calendar); + console.log(calendar.type); + if (calendar.type != "ext-" + this.extension.id) { + return; + } + + // Work around a bug where the notification is shown when imip is disabled + if (calendar.getProperty("imip.identity.disabled")) { + win.gIdentityNotification.removeAllNotifications(); + } + + let minRefresh = calendar.capabilities?.minimumRefresh; + + if (minRefresh) { + let refInterval = win.document.getElementById("calendar-refreshInterval-menupopup"); + for (let node of [...refInterval.children]) { + let nodeval = parseInt(node.getAttribute("value"), 10); + if (nodeval < minRefresh && nodeval != 0) { + node.remove(); + } + } + } + } + }); + + ExtensionSupport.registerWindowListener("ext-calendar-provider-creation-" + this.extension.id, { chromeURLs: ["chrome://calendar/content/calendar-creation.xhtml"], onLoadWindow: (win) => { const provider = this.extension.manifest.calendar_provider; @@ -504,7 +532,8 @@ this.calendar_provider = class extends ExtensionAPI { if (isAppShutdown) { return; } - ExtensionSupport.unregisterWindowListener("ext-calendar-provider-" + this.extension.id); + ExtensionSupport.unregisterWindowListener("ext-calendar-provider-creation-" + this.extension.id); + ExtensionSupport.unregisterWindowListener("ext-calendar-provider-properties-" + this.extension.id); ChromeUtils.unregisterWindowActor("CalendarProvider"); if (this.extension.manifest.calendar_provider) { From 7c437c3f1df036535bba312521a442085b649dec Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:17:26 +0200 Subject: [PATCH 02/17] calendar: Make freebusy lookups work --- .../calendar/parent/ext-calendar-provider.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 4bb1f90..28896c1 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -423,21 +423,24 @@ class ExtFreeBusyProvider { async getFreeBusyIntervals(aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) { try { const TYPE_MAP = { + unknown: Ci.calIFreeBusyInterval.UNKNOWN, free: Ci.calIFreeBusyInterval.FREE, busy: Ci.calIFreeBusyInterval.BUSY, unavailable: Ci.calIFreeBusyInterval.BUSY_UNAVAILABLE, tentative: Ci.calIFreeBusyInterval.BUSY_TENTATIVE, }; const attendee = aCalId.replace(/^mailto:/, ""); - const start = aRangeStart.icalString; - const end = aRangeEnd.icalString; + const start = cal.dtz.toRFC3339(aRangeStart); + const end = cal.dtz.toRFC3339(aRangeEnd); const types = ["free", "busy", "unavailable", "tentative"].filter((type, index) => aBusyTypes & (1 << index)); - const results = await this.fire.async({ attendee, start, end, types }); + const results = await this.fire.async(attendee, start, end, types); aListener.onResult({ status: Cr.NS_OK }, results.map(interval => new cal.provider.FreeBusyInterval(aCalId, TYPE_MAP[interval.type], - cal.createDateTime(interval.start), - cal.createDateTime(interval.end)))); + cal.dtz.fromRFC3339(interval.start, cal.dtz.UTC), + cal.dtz.fromRFC3339(interval.end, cal.dtz.UTC) + ) + )); } catch (e) { console.error(e); aListener.onResult({ status: e.result || Cr.NS_ERROR_FAILURE }, e.message || e); @@ -474,12 +477,12 @@ this.calendar_provider = class extends ExtensionAPI { win.gIdentityNotification.removeAllNotifications(); } - let minRefresh = calendar.capabilities?.minimumRefresh; + const minRefresh = calendar.capabilities?.minimumRefresh; if (minRefresh) { - let refInterval = win.document.getElementById("calendar-refreshInterval-menupopup"); - for (let node of [...refInterval.children]) { - let nodeval = parseInt(node.getAttribute("value"), 10); + const refInterval = win.document.getElementById("calendar-refreshInterval-menupopup"); + for (const node of [...refInterval.children]) { + const nodeval = parseInt(node.getAttribute("value"), 10); if (nodeval < minRefresh && nodeval != 0) { node.remove(); } From f1ca4edbc3a70720baa20b07b7fbca0ce94cc754 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:19:12 +0200 Subject: [PATCH 03/17] calendar: Add support for calendars that are immutable --- .../experiments/calendar/parent/ext-calendar-provider.js | 7 +++++++ .../experiments/calendar/schema/calendar-calendars.json | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 28896c1..2a6057d 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -36,6 +36,7 @@ function convertProps(props, extension) { calendar.setProperty("readOnly", props.readOnly); calendar.setProperty("disabled", props.enabled === false); calendar.setProperty("color", props.color || "#A8C2E1"); + calendar.capabilities = props.capabilities; // TODO validation necessary? calendar.uri = Services.io.newURI(props.url); @@ -488,6 +489,12 @@ this.calendar_provider = class extends ExtensionAPI { } } } + + let mutable = calendar.capabilities?.mutable; + + if (!mutable) { + win.document.getElementById("read-only").disabled = true; + } } }); diff --git a/calendar/experiments/calendar/schema/calendar-calendars.json b/calendar/experiments/calendar/schema/calendar-calendars.json index 7b81444..2c410b1 100644 --- a/calendar/experiments/calendar/schema/calendar-calendars.json +++ b/calendar/experiments/calendar/schema/calendar-calendars.json @@ -78,7 +78,8 @@ } }, "requires_network": { "type": "boolean", "optional": true }, - "minimum_refresh_interval": { "type": "integer", "minimum": -1, "optional": true } + "minimum_refresh_interval": { "type": "integer", "minimum": -1, "optional": true }, + "mutable": { "type": "boolean", "optional": true, "default": true } } } ], From 2b82aacefb9e77d5fa6635b5f0fb4442c068adf2 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:27:07 +0200 Subject: [PATCH 04/17] calendar: Support for extending the event dialog --- .../calendar/ext-calendar-utils.sys.mjs | 1 - .../calendar/parent/ext-calendar-provider.js | 3 +- .../parent/ext-calendarItemDetails.js | 126 +++++++++++++++--- .../calendar/schema/calendarItemDetails.json | 19 ++- 4 files changed, 129 insertions(+), 20 deletions(-) diff --git a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs index 453073c..ed7aa56 100644 --- a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs +++ b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs @@ -250,7 +250,6 @@ export async function setupE10sBrowser(extension, browser, parent, initOptions={ } sheets.push("chrome://browser/content/extension-popup-panel.css"); - const initBrowser = () => { ExtensionParent.apiManager.emit("extension-browser-inserted", browser); const mm = browser.messageManager; diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 2a6057d..5e047c9 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -468,7 +468,6 @@ this.calendar_provider = class extends ExtensionAPI { chromeURLs: ["chrome://calendar/content/calendar-properties-dialog.xhtml"], onLoadWindow: (win) => { const calendar = unwrapCalendar(win.arguments[0].calendar); - console.log(calendar.type); if (calendar.type != "ext-" + this.extension.id) { return; } @@ -490,7 +489,7 @@ this.calendar_provider = class extends ExtensionAPI { } } - let mutable = calendar.capabilities?.mutable; + const mutable = calendar.capabilities?.mutable; if (!mutable) { win.document.getElementById("read-only").disabled = true; diff --git a/calendar/experiments/calendar/parent/ext-calendarItemDetails.js b/calendar/experiments/calendar/parent/ext-calendarItemDetails.js index fa9e56f..e2a1e05 100644 --- a/calendar/experiments/calendar/parent/ext-calendarItemDetails.js +++ b/calendar/experiments/calendar/parent/ext-calendarItemDetails.js @@ -4,6 +4,8 @@ var { ExtensionCommon: { ExtensionAPI, makeWidgetId } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs"); +var { ExtensionUtils: { ExtensionError } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs"); + var { ExtensionSupport } = ChromeUtils.importESModule("resource:///modules/ExtensionSupport.sys.mjs"); this.calendarItemDetails = class extends ExtensionAPI { @@ -11,17 +13,25 @@ this.calendarItemDetails = class extends ExtensionAPI { const { setupE10sBrowser } = ChromeUtils.importESModule("resource://tb-experiments-calendar/experiments/calendar/ext-calendar-utils.sys.mjs"); const res = origLoadCalendarItemPanel(iframeId, url); - if (this.extension.manifest.calendar_item_details) { - let panelFrame; - if (window.tabmail) { - panelFrame = window.document.getElementById(iframeId || window.tabmail.currentTabInfo.iframe?.id); - } else { - panelFrame = window.document.getElementById("calendar-item-panel-iframe"); - } + if (!this.extension.manifest.calendar_item_details) { + return res; + } + let panelFrame; + if (window.tabmail) { + panelFrame = window.document.getElementById(iframeId || window.tabmail.currentTabInfo.iframe?.id); + } else { + panelFrame = window.document.getElementById("calendar-item-panel-iframe"); + } + + panelFrame.contentWindow.addEventListener("load", (event) => { + const document = event.target.ownerGlobal.document; - panelFrame.contentWindow.addEventListener("load", (event) => { - const document = event.target.ownerGlobal.document; + let areas = this.extension.manifest.calendar_item_details.allowed_areas || ["secondary"]; + if (!Array.isArray(areas)) { + areas = [areas]; + } + if (areas.includes("secondary")) { const widgetId = makeWidgetId(this.extension.id); const tabs = document.getElementById("event-grid-tabs"); @@ -40,17 +50,87 @@ this.calendarItemDetails = class extends ExtensionAPI { const browser = document.createXULElement("browser"); browser.setAttribute("flex", "1"); - const loadPromise = setupE10sBrowser(this.extension, browser, tabpanel); - return loadPromise.then(() => { - browser.fixupAndLoadURIString(this.extension.manifest.calendar_item_details.default_content, { triggeringPrincipal: this.extension.principal }); + const options = { maxWidth: null, fixedWidth: true }; + setupE10sBrowser(this.extension, browser, tabpanel, options).then(() => { + const target = new URL(this.extension.manifest.calendar_item_details.default_content); + target.searchParams.set("area", "secondary"); + browser.fixupAndLoadURIString(target.href, { triggeringPrincipal: this.extension.principal }); }); - }); - } + } else if (areas.includes("inline")) { + const tabbox = document.getElementById("event-grid"); + + const browserRow = tabbox.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "tr")); + const browserCell = browserRow.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "td")); + browserRow.className = "event-grid-link-row"; + browserCell.setAttribute("colspan", "2"); + + const separator = tabbox.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "tr")); + separator.className = "separator"; + const separatorCell = separator.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "td")); + separatorCell.setAttribute("colspan", "2"); + + const browser = document.createXULElement("browser"); + browser.setAttribute("flex", "1"); + + // TODO The real version will need a max-height and auto-resizing + browser.style.height = "200px"; + browser.style.width = "100%"; + browser.style.display = "block"; + + // Fix an annoying bug, this should be part of a different patch + document.getElementById("url-link").style.maxWidth = "42em"; + + const options = { maxWidth: null, fixedWidth: true }; + setupE10sBrowser(this.extension, browser, browserCell, options).then(() => { + const target = new URL(this.extension.manifest.calendar_item_details.default_content); + target.searchParams.set("area", "inline"); + browser.fixupAndLoadURIString(target.href, { triggeringPrincipal: this.extension.principal }); + }); + } + }); return res; } + onLoadSummary(window) { + const { setupE10sBrowser } = ChromeUtils.importESModule("resource://tb-experiments-calendar/experiments/calendar/ext-calendar-utils.sys.mjs"); + + const document = window.document; + + // Fix an annoying bug, this should be part of a different patch + document.querySelector(".url-link").style.maxWidth = "42em"; + + let areas = this.extension.manifest.calendar_item_details.allowed_areas || ["secondary"]; + if (!Array.isArray(areas)) { + areas = [areas]; + } + + + if (areas.includes("summary")) { + const summaryBox = document.querySelector(".item-summary-box"); + + const browser = document.createXULElement("browser"); + browser.id = "ext-calendar-item-details-" + this.extension.id; + browser.style.minHeight = "150px"; + document.getElementById(browser.id)?.remove(); + + const separator = document.createXULElement("separator"); + separator.id = "ext-calendar-item-details-separator-" + this.extension.id; + separator.className = "groove"; + + document.getElementById(separator.id)?.remove(); + summaryBox.appendChild(separator); + + const options = { maxWidth: null, fixedWidth: true }; + setupE10sBrowser(this.extension, browser, summaryBox, options).then(() => { + const target = new URL(this.extension.manifest.calendar_item_details.default_content); + target.searchParams.set("area", "summary"); + browser.fixupAndLoadURIString(target.href, { triggeringPrincipal: this.extension.principal }); + }); + } + } + onStartup() { const calendarItemDetails = this.extension.manifest?.calendar_item_details; if (calendarItemDetails) { @@ -66,9 +146,22 @@ this.calendarItemDetails = class extends ExtensionAPI { if (calendarItemDetails.default_title) { calendarItemDetails.default_title = localize(calendarItemDetails.default_title); } + + const areas = calendarItemDetails.allowed_areas; + if (Array.isArray(areas) && areas.includes("inline") && areas.includes("secondary")) { + throw new ExtensionError("Cannot show calendar_item_details both inline and secondary"); + } } + ExtensionSupport.registerWindowListener("ext-calendarItemDetails-summary-" + this.extension.id, { + chromeURLs: [ + "chrome://calendar/content/calendar-summary-dialog.xhtml" + ], + onLoadWindow: (window) => { + this.onLoadSummary(window); + } + }); - ExtensionSupport.registerWindowListener("ext-calendarItemDetails-" + this.extension.id, { + ExtensionSupport.registerWindowListener("ext-calendarItemDetails-event-" + this.extension.id, { chromeURLs: [ "chrome://messenger/content/messenger.xhtml", "chrome://calendar/content/calendar-event-dialog.xhtml" @@ -87,7 +180,8 @@ this.calendarItemDetails = class extends ExtensionAPI { }); } onShutdown() { - ExtensionSupport.unregisterWindowListener("ext-calendarItemDetails-" + this.extension.id); + ExtensionSupport.unregisterWindowListener("ext-calendarItemDetails-event-" + this.extension.id); + ExtensionSupport.unregisterWindowListener("ext-calendarItemDetails-summary-" + this.extension.id); for (const wnd of ExtensionSupport.openWindows) { if (wnd.location.href == "chrome://messenger/content/messenger.xhtml") { diff --git a/calendar/experiments/calendar/schema/calendarItemDetails.json b/calendar/experiments/calendar/schema/calendarItemDetails.json index f126525..f62ae67 100644 --- a/calendar/experiments/calendar/schema/calendarItemDetails.json +++ b/calendar/experiments/calendar/schema/calendarItemDetails.json @@ -39,6 +39,17 @@ "optional": true, "description": "Enable browser styles. See the `MDN documentation on browser styles `__ for more information.", "default": false + }, + "allowed_areas": { + "optional": true, + "default": "secondary", + "choices": [ + { "$ref": "CalendarItemDetailsArea" }, + { + "type": "array", + "items": { "$ref": "CalendarItemDetailsArea" } + } + ] } }, "optional": true @@ -51,7 +62,13 @@ "namespace": "calendarItemDetails", "description": "TODO", "permissions": ["manifest:calendar_item_details"], - "types": [], + "types": [ + { + "id": "CalendarItemDetailsArea", + "type": "string", + "enum": ["secondary", "inline", "summary"] + } + ], "functions": [], "events": [] } From 17e38f561293e60c7e9c1b331441be6089611c06 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:29:17 +0200 Subject: [PATCH 05/17] calendar: Allow getting current item in event dialog scripts --- .../calendar/parent/ext-calendar-items.js | 11 +++++++++++ .../calendar/schema/calendar-items.json | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/calendar/experiments/calendar/parent/ext-calendar-items.js b/calendar/experiments/calendar/parent/ext-calendar-items.js index dd7ac0f..56ef1ca 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-items.js +++ b/calendar/experiments/calendar/parent/ext-calendar-items.js @@ -141,6 +141,17 @@ this.calendar_items = class extends ExtensionAPI { await calendar.deleteItem(item); }, + async getCurrent(options) { + try { + // TODO This seems risky, could be null depending on remoteness + let item = context.browsingContext.embedderElement.ownerGlobal.calendarItem; + return convertItem(item, options, context.extension); + } catch (e) { + console.error(e); + return null; + } + }, + onCreated: new EventManager({ context, name: "calendar.items.onCreated", diff --git a/calendar/experiments/calendar/schema/calendar-items.json b/calendar/experiments/calendar/schema/calendar-items.json index 5fa6dc0..61894af 100644 --- a/calendar/experiments/calendar/schema/calendar-items.json +++ b/calendar/experiments/calendar/schema/calendar-items.json @@ -170,6 +170,21 @@ { "type": "string", "name": "calendarId" }, { "type": "string", "name": "id" } ] + }, + { + "name": "getCurrent", + "async": true, + "type": "function", + "parameters": [ + { + "type": "object", + "name": "getOptions", + "optional": true, + "properties": { + "returnFormat": { "$ref": "ReturnFormat", "optional": true } + } + } + ] } ], "events": [ From 7d903b9ba2d85ba607e0015ac3bd90b17f0e48a4 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:30:23 +0200 Subject: [PATCH 06/17] calendar: Don't rely on instanceof for type checks --- calendar/experiments/calendar/ext-calendar-utils.sys.mjs | 6 ++++-- .../experiments/calendar/parent/ext-calendar-items.js | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs index ed7aa56..f97bd04 100644 --- a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs +++ b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs @@ -141,10 +141,12 @@ export function convertItem(item, options, extension) { const props = {}; - if (item instanceof Ci.calIEvent) { + if (item.isEvent()) { props.type = "event"; - } else if (item instanceof Ci.calITodo) { + } else if (item.isTodo()) { props.type = "task"; + } else { + throw new ExtensionError(`Encountered unknown item type for ${item.calendar.id}/${item.id}`); } props.id = item.id; diff --git a/calendar/experiments/calendar/parent/ext-calendar-items.js b/calendar/experiments/calendar/parent/ext-calendar-items.js index 56ef1ca..0767991 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-items.js +++ b/calendar/experiments/calendar/parent/ext-calendar-items.js @@ -91,11 +91,14 @@ this.calendar_items = class extends ExtensionAPI { if (!oldItem) { throw new ExtensionError("Could not find item " + id); } - if (oldItem instanceof Ci.calIEvent) { + if (oldItem.isEvent()) { updateProperties.type = "event"; - } else if (oldItem instanceof Ci.calITodo) { + } else if (oldItem.isTodo()) { updateProperties.type = "task"; + } else { + throw new ExtensionError(`Encountered unknown item type for ${calendarId}/${id}`); } + const newItem = propsToItem(updateProperties, oldItem?.clone()); newItem.calendar = calendar.superCalendar; @@ -144,7 +147,7 @@ this.calendar_items = class extends ExtensionAPI { async getCurrent(options) { try { // TODO This seems risky, could be null depending on remoteness - let item = context.browsingContext.embedderElement.ownerGlobal.calendarItem; + const item = context.browsingContext.embedderElement.ownerGlobal.calendarItem; return convertItem(item, options, context.extension); } catch (e) { console.error(e); From c37637ed35a2a222774fae37fcbdee6b0512965d Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:31:36 +0200 Subject: [PATCH 07/17] calendar: Support for offline and invitation properties in item operations --- .../calendar/parent/ext-calendar-provider.js | 29 ++++++++++++++++++- .../calendar/schema/calendar-provider.json | 18 +++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 5e047c9..67ebc2d 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -43,6 +43,11 @@ function convertProps(props, extension) { return calendar; } +function stackContains(part) { + return new Error().stack.includes(part); +} + + class ExtCalendarProvider { QueryInterface = ChromeUtils.generateQI(["calICalendarProvider"]); @@ -227,7 +232,15 @@ class ExtCalendar extends cal.provider.BaseClass { async adoptItem(aItem) { const adoptCallback = this._cachedAdoptItemCallback; try { - const items = await this.extension.emit("calendar.provider.onItemCreated", this, aItem); + // TODO There should be an easier way to determine this + const options = {}; + if (stackContains("calItipUtils")) { + options.invitation = true; + } else if (stackContains("playbackOfflineItems")) { + options.offline = true; + } + + const items = await this.extension.emit("calendar.provider.onItemCreated", this, aItem, options); const { item, metadata } = items.find(props => props.item) || {}; if (!item) { throw new Components.Exception("Did not receive item from extension", Cr.NS_ERROR_FAILURE); @@ -292,6 +305,13 @@ class ExtCalendar extends cal.provider.BaseClass { async modifyItem(aNewItem, aOldItem, aOptions = {}) { const modifyCallback = this._cachedModifyItemCallback; + // TODO There should be an easier way to determine this + if (stackContains("calItipUtils")) { + aOptions.invitation = true; + } else if (stackContains("playbackOfflineItems")) { + aOptions.offline = true; + } + try { const results = await this.extension.emit( "calendar.provider.onItemUpdated", @@ -340,6 +360,13 @@ class ExtCalendar extends cal.provider.BaseClass { } async deleteItem(aItem, aOptions = {}) { + // TODO There should be an easier way to determine this + if (stackContains("calItipUtils")) { + aOptions.invitation = true; + } else if (stackContains("playbackOfflineItems")) { + aOptions.offline = true; + } + try { const results = await this.extension.emit( "calendar.provider.onItemRemoved", diff --git a/calendar/experiments/calendar/schema/calendar-provider.json b/calendar/experiments/calendar/schema/calendar-provider.json index 36abeab..abbf1c9 100644 --- a/calendar/experiments/calendar/schema/calendar-provider.json +++ b/calendar/experiments/calendar/schema/calendar-provider.json @@ -38,10 +38,20 @@ } } }, { - "id": "ItemOptions", + "id": "ItemOperationOptions", "type": "object", "description": "Options for the create/modify/delete event handlers", "properties": { + "invitation": { + "type": "boolean", + "description": "If true, the item operation is from an invitation", + "optional": true + }, + "offline": { + "type": "boolean", + "description": "If true, an offline operation is being replayed", + "optional": true + }, "force": { "type": "boolean", "description": "If true, instruct the provider to force overwrite changes (i.e. after a conflict)", @@ -56,7 +66,7 @@ "parameters": [ { "name": "calendar", "$ref": "calendar.calendars.Calendar" }, { "name": "item", "$ref": "calendar.items.CalendarItem" }, - { "name": "options", "$ref": "calendar.provider.ItemOptions" } + { "name": "options", "$ref": "calendar.provider.ItemOperationOptions" } ], "extraParameters": [ { @@ -82,7 +92,7 @@ { "name": "calendar", "$ref": "calendar.calendars.Calendar" }, { "name": "item", "$ref": "calendar.items.CalendarItem" }, { "name": "oldItem", "$ref": "calendar.items.CalendarItem" }, - { "name": "options", "$ref": "calendar.provider.ItemOptions" } + { "name": "options", "$ref": "calendar.provider.ItemOperationOptions" } ], "extraParameters": [ { @@ -107,7 +117,7 @@ "parameters": [ { "name": "calendar", "$ref": "calendar.calendars.Calendar" }, { "name": "item", "$ref": "calendar.items.CalendarItem" }, - { "name": "options", "$ref": "calendar.provider.ItemOptions" } + { "name": "options", "$ref": "calendar.provider.ItemOperationOptions" } ], "extraParameters": [ { From 6d14b3572439907777b39c230280ac0016eb8c3c Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:32:51 +0200 Subject: [PATCH 08/17] calendar: Make network errors trigger offline operations --- .../calendar/parent/ext-calendar-provider.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 67ebc2d..70f371f 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -267,7 +267,9 @@ class ExtCalendar extends cal.provider.BaseClass { return item; } catch (e) { let code; - if (e instanceof ItemError) { + if (e.message.startsWith("NetworkError")) { + code = Cr.NS_ERROR_NET_INTERRUPT; + } else if (e instanceof ItemError) { code = e.xpcomReason; } else { code = e.result || Cr.NS_ERROR_FAILURE; @@ -341,7 +343,9 @@ class ExtCalendar extends cal.provider.BaseClass { return item; } catch (e) { let code; - if (e instanceof ItemError) { + if (e.message.startsWith("NetworkError")) { + code = Cr.NS_ERROR_NET_INTERRUPT; + } else if (e instanceof ItemError) { if (e.reason == ItemError.CONFLICT) { const overwrite = cal.provider.promptOverwrite("modify", aOldItem); if (overwrite) { @@ -388,7 +392,9 @@ class ExtCalendar extends cal.provider.BaseClass { this.observers.notify("onDeleteItem", [aItem]); } catch (e) { let code; - if (e instanceof ItemError) { + if (e.message.startsWith("NetworkError")) { + code = Cr.NS_ERROR_NET_INTERRUPT; + } else if (e instanceof ItemError) { if (e.reason == ItemError.CONFLICT) { const overwrite = cal.provider.promptOverwrite("delete", aItem); if (overwrite) { From 607bad54cbb3eeb519a199f043476a2ad2742b2a Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:36:13 +0200 Subject: [PATCH 09/17] calendar: Document CalendarItemDetailsArea --- calendar/experiments/calendar/schema/calendarItemDetails.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calendar/experiments/calendar/schema/calendarItemDetails.json b/calendar/experiments/calendar/schema/calendarItemDetails.json index f62ae67..b712067 100644 --- a/calendar/experiments/calendar/schema/calendarItemDetails.json +++ b/calendar/experiments/calendar/schema/calendarItemDetails.json @@ -60,11 +60,11 @@ }, { "namespace": "calendarItemDetails", - "description": "TODO", "permissions": ["manifest:calendar_item_details"], "types": [ { "id": "CalendarItemDetailsArea", + "description": "Describes the area(s) where the item details should be displayed", "type": "string", "enum": ["secondary", "inline", "summary"] } From bc1fb6ecfbdbf6eeae4e7fb43a21c4b96d4dc932 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:38:13 +0200 Subject: [PATCH 10/17] calendar: Support for a timezone data API --- .../calendar/child/ext-calendar-timezones.js | 39 +++++++++++++++ .../calendar/parent/ext-calendar-timezones.js | 48 ++++++++++++++++++ .../calendar/schema/calendar-timezones.json | 50 +++++++++++++++++++ calendar/manifest.json | 17 +++++++ 4 files changed, 154 insertions(+) create mode 100644 calendar/experiments/calendar/child/ext-calendar-timezones.js create mode 100644 calendar/experiments/calendar/parent/ext-calendar-timezones.js create mode 100644 calendar/experiments/calendar/schema/calendar-timezones.json diff --git a/calendar/experiments/calendar/child/ext-calendar-timezones.js b/calendar/experiments/calendar/child/ext-calendar-timezones.js new file mode 100644 index 0000000..f38fd88 --- /dev/null +++ b/calendar/experiments/calendar/child/ext-calendar-timezones.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { ExtensionCommon: { ExtensionAPI } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs"); + +var { default: ICAL } = ChromeUtils.importESModule("resource:///modules/calendar/Ical.sys.mjs"); + +var { cal } = ChromeUtils.importESModule("resource:///modules/calendar/calUtils.sys.mjs"); + +this.calendar_timezones = class extends ExtensionAPI { + getAPI(context) { + return { + calendar: { + timezones: { + get timezoneIds() { + return cal.timezoneService.timezoneIds; + }, + get currentZone() { + cal.timezoneService.wrappedJSObject._updateDefaultTimezone(); + return cal.timezoneService.defaultTimezone?.tzid; + }, + getDefinition(tzid, returnFormat) { + let timezoneDatabase = Cc["@mozilla.org/calendar/timezone-database;1"].getService( + Ci.calITimezoneDatabase + ); + let zoneInfo = timezoneDatabase.getTimezoneDefinition(tzid); + + if (returnFormat == "jcal") { + zoneInfo = ICAL.parse(zoneInfo); + } + + return zoneInfo; + }, + } + } + }; + } +}; diff --git a/calendar/experiments/calendar/parent/ext-calendar-timezones.js b/calendar/experiments/calendar/parent/ext-calendar-timezones.js new file mode 100644 index 0000000..8dd3382 --- /dev/null +++ b/calendar/experiments/calendar/parent/ext-calendar-timezones.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { ExtensionCommon: { ExtensionAPI, EventManager } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs"); + +var { cal } = ChromeUtils.importESModule("resource:///modules/calendar/calUtils.sys.mjs"); + +this.calendar_timezones = class extends ExtensionAPI { + getAPI(context) { + return { + calendar: { + timezones: { + onUpdated: new EventManager({ + context, + name: "calendar.timezones.onUpdated", + register: fire => { + cal.timezoneService.wrappedJSObject._updateDefaultTimezone(); + let lastValue = cal.timezoneService.defaultTimezone?.tzid; + + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(_subject, _topic, _data) { + // Make sure the default timezone is updated before firing + cal.timezoneService.wrappedJSObject._updateDefaultTimezone(); + let currentValue = cal.timezoneService.defaultTimezone?.tzid; + if (currentValue != lastValue) { + lastValue = currentValue; + fire.sync(currentValue); + } + } + }; + + Services.prefs.addObserver("calendar.timezone.useSystemTimezone", observer); + Services.prefs.addObserver("calendar.timezone.local", observer); + Services.obs.addObserver(observer, "default-timezone-changed"); + return () => { + Services.obs.removeObserver(observer, "default-timezone-changed"); + Services.prefs.removeObserver("calendar.timezone.local", observer); + Services.prefs.removeObserver("calendar.timezone.useSystemTimezone", observer); + }; + }, + }).api(), + } + } + }; + } +}; diff --git a/calendar/experiments/calendar/schema/calendar-timezones.json b/calendar/experiments/calendar/schema/calendar-timezones.json new file mode 100644 index 0000000..fa496fe --- /dev/null +++ b/calendar/experiments/calendar/schema/calendar-timezones.json @@ -0,0 +1,50 @@ + +[ + { + "namespace": "calendar.timezones", + "properties": { + "currentZone": { + "description": "The current timezone id", + "type": "string" + }, + "timezoneIds": { + "description": "The current timezone id", + "type": "array", + "items": { "type": "string" } + } + }, + "functions": [ + { + "name": "getDefinition", + "type": "function", + "description": "Retrieve the vtimezone definition of a timezone with the specified id", + "parameters": [ + { + "type": "string", + "name": "tzid", + "description": "The timezone id to retrieve defintiion for" + }, + { + "$ref": "calendar.items.CalendarItemFormats", + "name":"returnFormat", + "optional": true, + "default": "ical", + "description": "The return format of the definition" + } + ], + "returns": { + "type": "string" + } + } + ], + "events": [ + { + "name": "onUpdated", + "type": "function", + "parameters": [ + { "name": "tzid", "type": "string" } + ] + } + ] + } +] diff --git a/calendar/manifest.json b/calendar/manifest.json index d1c458e..c54188e 100644 --- a/calendar/manifest.json +++ b/calendar/manifest.json @@ -86,6 +86,23 @@ ] } }, + "calendar_timezones": { + "schema": "experiments/calendar/schema/calendar-timezones.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiments/calendar/parent/ext-calendar-timezones.js", + "paths": [ + ["calendar", "timezones"] + ] + }, + "child": { + "scopes": ["addon_child"], + "script": "experiments/calendar/child/ext-calendar-timezones.js", + "paths": [ + ["calendar", "timezones"] + ] + } + }, "calendarItemAction": { "schema": "experiments/calendar/schema/calendarItemAction.json", "parent": { From 09e5d2a182e56b2aec3e02aafb820614ccd25388 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:43:03 +0200 Subject: [PATCH 11/17] calendar: Support for recurrence exceptions --- .../calendar/child/ext-calendar-timezones.js | 4 +- .../calendar/ext-calendar-utils.sys.mjs | 178 +++++++++++++----- .../calendar/parent/ext-calendar-timezones.js | 4 +- .../calendar/schema/calendar-items.json | 1 + 4 files changed, 140 insertions(+), 47 deletions(-) diff --git a/calendar/experiments/calendar/child/ext-calendar-timezones.js b/calendar/experiments/calendar/child/ext-calendar-timezones.js index f38fd88..0487ba0 100644 --- a/calendar/experiments/calendar/child/ext-calendar-timezones.js +++ b/calendar/experiments/calendar/child/ext-calendar-timezones.js @@ -9,7 +9,7 @@ var { default: ICAL } = ChromeUtils.importESModule("resource:///modules/calendar var { cal } = ChromeUtils.importESModule("resource:///modules/calendar/calUtils.sys.mjs"); this.calendar_timezones = class extends ExtensionAPI { - getAPI(context) { + getAPI(_context) { return { calendar: { timezones: { @@ -21,7 +21,7 @@ this.calendar_timezones = class extends ExtensionAPI { return cal.timezoneService.defaultTimezone?.tzid; }, getDefinition(tzid, returnFormat) { - let timezoneDatabase = Cc["@mozilla.org/calendar/timezone-database;1"].getService( + const timezoneDatabase = Cc["@mozilla.org/calendar/timezone-database;1"].getService( Ci.calITimezoneDatabase ); let zoneInfo = timezoneDatabase.getTimezoneDefinition(tzid); diff --git a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs index f97bd04..e6615bb 100644 --- a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs +++ b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs @@ -2,6 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* + * WARNING: This file usually doesn't live reload, you need to restart Thunderbird after editing + */ + var { ExtensionUtils: { ExtensionError, promiseEvent } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs"); var { cal } = ChromeUtils.importESModule("resource:///modules/calendar/calUtils.sys.mjs"); @@ -73,7 +77,75 @@ export function convertCalendar(extension, calendar) { return props; } -export function propsToItem(props, baseItem) { +function parseJcalData(jcalComp) { + function generateItem(jcalSubComp) { + let item; + if (jcalSubComp.name == "vevent") { + item = new CalEvent(); + } else if (jcalSubComp.name == "vtodo") { + item = new CalTodo(); + } else { + throw new ExtensionError("Invalid item component"); + } + + // TODO use calIcalComponent directly when bringing this to core + const comp = cal.icsService.createIcalComponent(jcalSubComp.name); + comp.wrappedJSObject.innerObject = jcalSubComp; + + item.icalComponent = comp; + return item; + } + + if (jcalComp.name == "vevent" || jcalComp.name == "vtodo") { + // Single item only, no exceptions + return generateItem(jcalComp); + } else if (jcalComp.name == "vcalendar") { + // A vcalendar with vevents or vtodos + const exceptions = []; + let parent; + + for (const subComp of jcalComp.getAllSubcomponents()) { + if (subComp.name != "vevent" && subComp.name != "vtodo") { + continue; + } + + if (subComp.hasProperty("recurrence-id")) { + exceptions.push(subComp); + continue; + } + + if (parent) { + throw new ExtensionError("Cannot parse more than one parent item"); + } + + parent = generateItem(subComp); + } + + if (!parent) { + throw new ExtensionError("TODO need to retrieve a parent item from storage"); + } + + if (exceptions.length && !parent.recurrenceInfo) { + throw new ExtensionError("Exceptions were supplied to a non-recurring item"); + } + + for (const exception of exceptions) { + const excItem = generateItem(exception); + if (excItem.id != parent.id || parent.isEvent() != excItem.isEvent()) { + throw new ExtensionError("Exception does not relate to parent item"); + } + parent.recurrenceInfo.modifyException(excItem, true); + } + return parent; + } + throw new ExtensionError("Don't know how to handle component type " + jcalComp.name); +} + +function convertSimpleFormat(props, baseItem) { + // TODO this was kind of a quick hack. Consider not having a simple format + // and forcing ical or jcal, or maybe using jsCalendar which is close enough + // to simple (but not backwards compatible) + let item; if (baseItem) { item = baseItem; @@ -87,51 +159,58 @@ export function propsToItem(props, baseItem) { throw new ExtensionError("Invalid item type: " + props.type); } - if (props.formats?.use == "ical") { - item.icalString = props.formats.ical; - } else if (props.formats?.use == "jcal") { - try { - item.icalString = ICAL.stringify(props.formats.jcal); - } catch (e) { - let jsonstring; - try { - jsonstring = JSON.stringify(props.formats.jcal, null, 2); - } catch { - jsonstring = props.formats.jcal; - } + // TODO allow empty/null props - throw new ExtensionError("Could not parse jCal: " + e + "\n" + jsonstring); - } - } else { - if (props.id) { - item.id = props.id; - } - if (props.title) { - item.title = props.title; - } - if (props.description) { - item.setProperty("description", props.description); - } - if (props.location) { - item.setProperty("location", props.location); + if (props.id) { + item.id = props.id; + } + if (props.title) { + item.title = props.title; + } + if (props.description) { + item.setProperty("description", props.description); + } + if (props.location) { + item.setProperty("location", props.location); + } + if (props.categories) { + item.setCategories(props.categories); + } + + if (props.type == "event") { + // TODO need to do something about timezone + if (props.startDate) { + item.startDate = cal.createDateTime(props.startDate); } - if (props.categories) { - item.setCategories(props.categories); + if (props.endDate) { + item.endDate = cal.createDateTime(props.endDate); } + } else if (props.type == "task") { + // entryDate, dueDate, completedDate, isCompleted, duration + } - if (props.type == "event") { - // TODO need to do something about timezone - if (props.startDate) { - item.startDate = cal.createDateTime(props.startDate); - } - if (props.endDate) { - item.endDate = cal.createDateTime(props.endDate); - } - } else if (props.type == "task") { - // entryDate, dueDate, completedDate, isCompleted, duration + return item; +} + +export function propsToItem(props, baseItem) { + let jcalComp; + + if (props.formats?.use == "ical") { + try { + jcalComp = new ICAL.Component(ICAL.parse(props.formats.ical)); + } catch (e) { + throw new ExtensionError("Could not parse iCalendar", { cause: e }); + } + return parseJcalData(jcalComp); + } else if (props.formats?.use == "jcal") { + try { + jcalComp = new ICAL.Component(props.formats.jcal); + } catch (e) { + throw new ExtensionError("Could not parse jCal", { cause: e }); } + return parseJcalData(jcalComp); } - return item; + return convertSimpleFormat(props, baseItem); } export function convertItem(item, options, extension) { @@ -156,6 +235,12 @@ export function convertItem(item, options, extension) { props.location = item.getProperty("location") || ""; props.categories = item.getCategories(); + const recId = item.recurrenceId?.getInTimezone(cal.timezoneService.UTC)?.icalString; + if (recId) { + const jcalId = ICAL.design.icalendar.value[recId.length == 8 ? "date" : "date-time"].fromICAL(recId); + props.instance = jcalId; + } + if (isOwnCalendar(item.calendar, extension)) { props.metadata = {}; const cache = getCachedCalendar(item.calendar); @@ -168,20 +253,27 @@ export function convertItem(item, options, extension) { } if (options?.returnFormat) { - props.formats = { use: null }; let formats = options.returnFormat; + props.formats = { use: formats }; + if (!Array.isArray(formats)) { formats = [formats]; } + const serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( + Ci.calIIcsSerializer + ); + serializer.addItems([item]); + const icalString = serializer.serializeToString(); + for (const format of formats) { switch (format) { case "ical": - props.formats.ical = item.icalString; + props.formats.ical = icalString; break; case "jcal": // TODO shortcut when using icaljs backend - props.formats.jcal = ICAL.parse(item.icalString); + props.formats.jcal = ICAL.parse(icalString); break; default: throw new ExtensionError("Invalid format specified: " + format); diff --git a/calendar/experiments/calendar/parent/ext-calendar-timezones.js b/calendar/experiments/calendar/parent/ext-calendar-timezones.js index 8dd3382..0422bf9 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-timezones.js +++ b/calendar/experiments/calendar/parent/ext-calendar-timezones.js @@ -18,12 +18,12 @@ this.calendar_timezones = class extends ExtensionAPI { cal.timezoneService.wrappedJSObject._updateDefaultTimezone(); let lastValue = cal.timezoneService.defaultTimezone?.tzid; - let observer = { + const observer = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), observe(_subject, _topic, _data) { // Make sure the default timezone is updated before firing cal.timezoneService.wrappedJSObject._updateDefaultTimezone(); - let currentValue = cal.timezoneService.defaultTimezone?.tzid; + const currentValue = cal.timezoneService.defaultTimezone?.tzid; if (currentValue != lastValue) { lastValue = currentValue; fire.sync(currentValue); diff --git a/calendar/experiments/calendar/schema/calendar-items.json b/calendar/experiments/calendar/schema/calendar-items.json index 61894af..73a8b0d 100644 --- a/calendar/experiments/calendar/schema/calendar-items.json +++ b/calendar/experiments/calendar/schema/calendar-items.json @@ -16,6 +16,7 @@ "categories": { "type": "array", "items": { "type": "string" }, "optional": true }, "startDate": { "type": "string", "optional": true }, "endDate": { "type": "string", "optional": true }, + "instance": { "type": "string", "optional": true }, "formats": { "$ref": "RawCalendarItem", "optional": true }, "metadata": { "type": "object", "additionalProperties": { "type": "any" }, "optional": true } } From fd38a6df3597ff51e4884489d4f0cb0bd9018f87 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:44:41 +0200 Subject: [PATCH 12/17] calendar: Remove a few unneeded TODOs --- calendar/experiments/calendar/ext-calendar-utils.sys.mjs | 2 -- calendar/experiments/calendar/parent/ext-calendar-calendars.js | 1 - 2 files changed, 3 deletions(-) diff --git a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs index e6615bb..cc6da18 100644 --- a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs +++ b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs @@ -49,7 +49,6 @@ export function getCachedCalendar(calendar) { } export function isCachedCalendar(id) { - // TODO make this better return id.endsWith("#cache"); } @@ -69,7 +68,6 @@ export function convertCalendar(extension, calendar) { }; if (isOwnCalendar(calendar, extension)) { - // TODO find a better way to define the cache id props.cacheId = calendar.superCalendar.id + "#cache"; props.capabilities = unwrapCalendar(calendar.superCalendar).capabilities; // TODO needs deep clone? } diff --git a/calendar/experiments/calendar/parent/ext-calendar-calendars.js b/calendar/experiments/calendar/parent/ext-calendar-calendars.js index 01d9238..64154cc 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-calendars.js +++ b/calendar/experiments/calendar/parent/ext-calendar-calendars.js @@ -65,7 +65,6 @@ this.calendar_calendars = class extends ExtensionAPI { .map(calendar => convertCalendar(context.extension, calendar)); }, async get(id) { - // TODO find a better way to determine cache id if (id.endsWith("#cache")) { const calendar = unwrapCalendar(cal.manager.getCalendarById(id.substring(0, id.length - 6))); const own = calendar.offlineStorage && isOwnCalendar(calendar, context.extension); From b473ec2f3655d0edd34ced85255941f30f13e6cb Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:46:03 +0200 Subject: [PATCH 13/17] calendar: Support visible and showReminders in query options --- .../calendar/ext-calendar-utils.sys.mjs | 2 ++ .../calendar/parent/ext-calendar-calendars.js | 26 ++++++++++++++++++- .../calendar/schema/calendar-calendars.json | 7 +++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs index cc6da18..59ca2de 100644 --- a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs +++ b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs @@ -63,6 +63,8 @@ export function convertCalendar(extension, calendar) { name: calendar.name, url: calendar.uri.spec, readOnly: calendar.readOnly, + visible: !!calendar.getProperty("calendar-main-in-composite"), + showReminders: !calendar.getProperty("suppressAlarms"), enabled: !calendar.getProperty("disabled"), color: calendar.getProperty("color") || "#A8C2E1", }; diff --git a/calendar/experiments/calendar/parent/ext-calendar-calendars.js b/calendar/experiments/calendar/parent/ext-calendar-calendars.js index 64154cc..a544be5 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-calendars.js +++ b/calendar/experiments/calendar/parent/ext-calendar-calendars.js @@ -19,7 +19,7 @@ this.calendar_calendars = class extends ExtensionAPI { return { calendar: { calendars: { - async query({ type, url, name, color, readOnly, enabled }) { + async query({ type, url, name, color, readOnly, enabled, visible }) { const calendars = cal.manager.getCalendars(); let pattern = null; @@ -56,6 +56,10 @@ this.calendar_calendars = class extends ExtensionAPI { matches = false; } + if (visible != null & calendar.getProperty("calendar-main-in-composite") != visible) { + matches = false; + } + if (readOnly != null && calendar.readOnly != readOnly) { matches = false; } @@ -86,6 +90,12 @@ this.calendar_calendars = class extends ExtensionAPI { if (typeof createProperties.color != "undefined") { calendar.setProperty("color", createProperties.color); } + if (typeof createProperties.visible != "undefined") { + calendar.setProperty("calendar-main-in-composite", createProperties.visible); + } + if (typeof createProperties.showReminders != "undefined") { + calendar.setProperty("suppressAlarms", !createProperties.showReminders); + } cal.manager.registerCalendar(calendar); @@ -113,6 +123,14 @@ this.calendar_calendars = class extends ExtensionAPI { calendar.setProperty("disabled", !updateProperties.enabled); } + if (updateProperties.visible != null) { + calendar.setProperty("calendar-main-in-composite", updateProperties.visible); + } + + if (updateProperties.showReminders != null) { + calendar.setProperty("suppressAlarms", !updateProperties.showReminders); + } + for (const prop of ["readOnly", "name", "color"]) { if (updateProperties[prop] != null) { calendar.setProperty(prop, updateProperties[prop]); @@ -236,6 +254,12 @@ this.calendar_calendars = class extends ExtensionAPI { case "uri": fire.sync(converted, { url: value?.spec }); break; + case "suppressAlarms": + fire.sync(converted, { showReminders: !value }); + break; + case "calendar-main-in-composite": + fire.sync(converted, { visible: value }); + break; case "disabled": fire.sync(converted, { enabled: !value }); break; diff --git a/calendar/experiments/calendar/schema/calendar-calendars.json b/calendar/experiments/calendar/schema/calendar-calendars.json index 2c410b1..388fae5 100644 --- a/calendar/experiments/calendar/schema/calendar-calendars.json +++ b/calendar/experiments/calendar/schema/calendar-calendars.json @@ -13,6 +13,8 @@ "url": { "type": "string" }, "readOnly": { "type": "boolean" }, "enabled": { "type": "boolean" }, + "visible": { "type": "boolean" }, + "showReminders": { "type": "boolean" }, "color": { "type": "string", "optional": true }, "capabilities": { "$ref": "CalendarCapabilities", "optional": true } } @@ -25,6 +27,8 @@ "url": { "type": "string" }, "readOnly": { "type": "boolean" }, "enabled": { "type": "boolean" }, + "visible": { "type": "boolean" }, + "showReminders": { "type": "boolean" }, "color": { "type": "string", "optional": true } } }, @@ -104,6 +108,7 @@ "name": { "type": "string", "optional": true }, "color": { "type": "string", "optional": true }, "readOnly": { "type": "boolean", "optional": true }, + "visible": { "type": "boolean", "optional": true }, "enabled": { "type": "boolean", "optional": true } } } @@ -131,6 +136,8 @@ "url": { "type": "string" }, "readOnly": { "type": "boolean", "optional": true }, "enabled": { "type": "boolean", "optional": true }, + "visible": { "type": "boolean", "optional": true }, + "showReminders": { "type": "boolean", "optional": true }, "color": { "type": "string", "optional": true }, "capabilities": { "$ref": "CalendarCapabilities", "optional": true } } From 7878615acf1d4709017a6a523561f781fe057316 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 7 Aug 2024 23:49:26 +0200 Subject: [PATCH 14/17] calendar: Interactions with the new calendar wizard --- .../calendar/parent/ext-calendar-provider.js | 104 ++++++++++++++++-- .../calendar/schema/calendar-provider.json | 24 ++++ 2 files changed, 118 insertions(+), 10 deletions(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 70f371f..3d2ed1a 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -2,11 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -var { ExtensionCommon: { ExtensionAPI, EventManager } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs"); +var { ExtensionCommon: { ExtensionAPI, EventManager, EventEmitter } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs"); +var { ExtensionUtils: { ExtensionError } } = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs"); var { cal } = ChromeUtils.importESModule("resource:///modules/calendar/calUtils.sys.mjs"); var { ExtensionSupport } = ChromeUtils.importESModule("resource:///modules/ExtensionSupport.sys.mjs"); +// TODO move me +function getNewCalendarWindow() { + // This window is missing a windowtype attribute + for (const win of Services.wm.getEnumerator(null)) { + if (win.location == "chrome://calendar/content/calendar-creation.xhtml") { + return win; + } + } + return null; +} + // TODO move me class ItemError extends Error { static CONFLICT = "CONFLICT"; @@ -553,19 +565,42 @@ this.calendar_provider = class extends ExtensionAPI { browser.fixupAndLoadURIString(calendarType.panelSrc, { triggeringPrincipal: this.extension.principal }); }); - win.gButtonHandlers.forNodeId["panel-addon-calendar-settings"].accept = calendarType.onCreated; + win.gButtonHandlers.forNodeId["panel-addon-calendar-settings"].accept = (event) => { + const addonPanel = win.document.getElementById("panel-addon-calendar-settings"); + if (addonPanel.dataset.addonForward) { + event.preventDefault(); + event.target.getButton("accept").disabled = true; + win.gAddonAdvance.emit("advance", "forward", addonPanel.dataset.addonForward).finally(() => { + event.target.getButton("accept").disabled = false; + }); + } else if (calendarType.onCreated) { + calendarType.onCreated(); + } else { + win.close(); + } + }; + win.gButtonHandlers.forNodeId["panel-addon-calendar-settings"].extra2 = (_event) => { + const addonPanel = win.document.getElementById("panel-addon-calendar-settings"); + + if (addonPanel.dataset.addonBackward) { + win.gAddonAdvance.emit("advance", "back", addonPanel.dataset.addonBackward); + } else { + win.selectPanel("panel-select-calendar-type"); + + // Reload the window, the add-on might expect to do some initial setup when going + // back and forward again. + win.setUpAddonCalendarSettingsPanel(extCalendarType); + } + }; }; - win.registerCalendarType({ + const extCalendarType = { label: this.extension.localize(provider.name), panelSrc: this.extension.getURL(this.extension.localize(provider.creation_panel)), - onCreated: () => { - // TODO temporary - const browser = win.document.getElementById("panel-addon-calendar-settings").lastElementChild; - const actor = browser.browsingContext.currentWindowGlobal.getActor("CalendarProvider"); - actor.sendAsyncMessage("postMessage", { message: "create", origin: this.extension.getURL("") }); - } - }); + }; + win.registerCalendarType(extCalendarType); + + win.gAddonAdvance = new EventEmitter(); } } }); @@ -773,6 +808,55 @@ this.calendar_provider = class extends ExtensionAPI { }; } }).api(), + + + // New calendar dialog + async setAdvanceAction({ forward, back, label }) { + const window = getNewCalendarWindow(); + if (!window) { + throw new ExtensionError("New calendar wizard is not open"); + } + const addonPanel = window.document.getElementById("panel-addon-calendar-settings"); + if (forward) { + addonPanel.dataset.addonForward = forward; + } else { + delete addonPanel.dataset.addonForward; + } + + if (back) { + addonPanel.dataset.addonBackward = back; + } else { + delete addonPanel.dataset.addonBackward; + } + + addonPanel.setAttribute("buttonlabelaccept", label); + if (!addonPanel.hidden) { + window.updateButton("accept", addonPanel); + } + }, + onAdvanceNewCalendar: new EventManager({ + context, + name: "calendar.provider.onAdvanceNewCalendar", + register: fire => { + const handler = async (event, direction, actionId) => { + const result = await fire.async(actionId); + + if (direction == "forward" && result !== false) { + getNewCalendarWindow()?.close(); + } + }; + + const win = getNewCalendarWindow(); + if (!win) { + throw new ExtensionError("New calendar wizard is not open"); + } + + win.gAddonAdvance.on("advance", handler); + return () => { + getNewCalendarWindow()?.gAddonAdvance.off("advance", handler); + }; + }, + }).api() }, }, }; diff --git a/calendar/experiments/calendar/schema/calendar-provider.json b/calendar/experiments/calendar/schema/calendar-provider.json index abbf1c9..ce3a4fd 100644 --- a/calendar/experiments/calendar/schema/calendar-provider.json +++ b/calendar/experiments/calendar/schema/calendar-provider.json @@ -59,6 +59,23 @@ } } }], + "functions": [ + { + "name": "setAdvanceAction", + "async": true, + "type": "function", + "parameters": [ + { + "type": "object", + "properties": { + "forward": { "type": "string" }, + "back": { "type": "string", "optional": true }, + "label": { "type": "string" } + } + } + ] + } + ], "events": [ { "name": "onItemCreated", @@ -187,6 +204,13 @@ { "name": "savePassword", "type": "boolean" }, { "name": "extraProperties", "type": "object" } ] + }, + { + "name": "onAdvanceNewCalendar", + "type": "function", + "parameters": [ + { "name": "id", "type": "string" } + ] } ] } From c649390e4ca62b68763088f44a7fccadb65e9ffd Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Mon, 19 Aug 2024 17:58:02 +0200 Subject: [PATCH 15/17] calendar: Change to shell item format --- .../calendar/ext-calendar-utils.sys.mjs | 104 +++--------------- .../calendar/parent/ext-calendar-items.js | 2 +- .../calendar/parent/ext-calendar-provider.js | 4 +- .../calendar/schema/calendar-items.json | 39 ++----- 4 files changed, 28 insertions(+), 121 deletions(-) diff --git a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs index 59ca2de..3aa1354 100644 --- a/calendar/experiments/calendar/ext-calendar-utils.sys.mjs +++ b/calendar/experiments/calendar/ext-calendar-utils.sys.mjs @@ -141,76 +141,26 @@ function parseJcalData(jcalComp) { throw new ExtensionError("Don't know how to handle component type " + jcalComp.name); } -function convertSimpleFormat(props, baseItem) { - // TODO this was kind of a quick hack. Consider not having a simple format - // and forcing ical or jcal, or maybe using jsCalendar which is close enough - // to simple (but not backwards compatible) - - let item; - if (baseItem) { - item = baseItem; - } else if (props.type == "event") { - item = new CalEvent(); - cal.dtz.setDefaultStartEndHour(item); - } else if (props.type == "task") { - item = new CalTodo(); - cal.dtz.setDefaultStartEndHour(item); - } else { - throw new ExtensionError("Invalid item type: " + props.type); - } - - // TODO allow empty/null props - - if (props.id) { - item.id = props.id; - } - if (props.title) { - item.title = props.title; - } - if (props.description) { - item.setProperty("description", props.description); - } - if (props.location) { - item.setProperty("location", props.location); - } - if (props.categories) { - item.setCategories(props.categories); - } - - if (props.type == "event") { - // TODO need to do something about timezone - if (props.startDate) { - item.startDate = cal.createDateTime(props.startDate); - } - if (props.endDate) { - item.endDate = cal.createDateTime(props.endDate); - } - } else if (props.type == "task") { - // entryDate, dueDate, completedDate, isCompleted, duration - } - - return item; -} - -export function propsToItem(props, baseItem) { +export function propsToItem(props) { let jcalComp; - if (props.formats?.use == "ical") { + if (props.format == "ical") { try { - jcalComp = new ICAL.Component(ICAL.parse(props.formats.ical)); + jcalComp = new ICAL.Component(ICAL.parse(props.item)); } catch (e) { throw new ExtensionError("Could not parse iCalendar", { cause: e }); } return parseJcalData(jcalComp); - } else if (props.formats?.use == "jcal") { + } else if (props.format == "jcal") { try { - jcalComp = new ICAL.Component(props.formats.jcal); + jcalComp = new ICAL.Component(props.item); } catch (e) { throw new ExtensionError("Could not parse jCal", { cause: e }); } return parseJcalData(jcalComp); } - return convertSimpleFormat(props, baseItem); + + throw new ExtensionError("Invalid item format: " + props.format); } export function convertItem(item, options, extension) { @@ -230,10 +180,6 @@ export function convertItem(item, options, extension) { props.id = item.id; props.calendarId = item.calendar.superCalendar.id; - props.title = item.title || ""; - props.description = item.getProperty("description") || ""; - props.location = item.getProperty("location") || ""; - props.categories = item.getCategories(); const recId = item.recurrenceId?.getInTimezone(cal.timezoneService.UTC)?.icalString; if (recId) { @@ -253,12 +199,7 @@ export function convertItem(item, options, extension) { } if (options?.returnFormat) { - let formats = options.returnFormat; - props.formats = { use: formats }; - - if (!Array.isArray(formats)) { - formats = [formats]; - } + props.format = options.returnFormat; const serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( Ci.calIIcsSerializer @@ -266,28 +207,19 @@ export function convertItem(item, options, extension) { serializer.addItems([item]); const icalString = serializer.serializeToString(); - for (const format of formats) { - switch (format) { - case "ical": - props.formats.ical = icalString; - break; - case "jcal": - // TODO shortcut when using icaljs backend - props.formats.jcal = ICAL.parse(icalString); - break; - default: - throw new ExtensionError("Invalid format specified: " + format); - } + switch (options.returnFormat) { + case "ical": + props.item = icalString; + break; + case "jcal": + // TODO shortcut when using icaljs backend + props.item = ICAL.parse(icalString); + break; + default: + throw new ExtensionError("Invalid format specified: " + options.returnFormat); } } - if (props.type == "event") { - props.startDate = item.startDate.icalString; - props.endDate = item.endDate.icalString; - } else if (props.type == "task") { - // TODO extra properties - } - return props; } diff --git a/calendar/experiments/calendar/parent/ext-calendar-items.js b/calendar/experiments/calendar/parent/ext-calendar-items.js index 0767991..e4dca08 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-items.js +++ b/calendar/experiments/calendar/parent/ext-calendar-items.js @@ -99,7 +99,7 @@ this.calendar_items = class extends ExtensionAPI { throw new ExtensionError(`Encountered unknown item type for ${calendarId}/${id}`); } - const newItem = propsToItem(updateProperties, oldItem?.clone()); + const newItem = propsToItem(updateProperties); newItem.calendar = calendar.superCalendar; if (updateProperties.metadata && isOwnCalendar(calendar, context.extension)) { diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index 3d2ed1a..cd5dce3 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -674,7 +674,7 @@ this.calendar_provider = class extends ExtensionAPI { } if (props?.type) { - item = propsToItem(props, item); + item = propsToItem(props); } if (!item.id) { item.id = cal.getUUID(); @@ -704,7 +704,7 @@ this.calendar_provider = class extends ExtensionAPI { return { error: props.error }; } if (props?.type) { - item = propsToItem(props, item); + item = propsToItem(props); } return { item, metadata: props?.metadata }; }; diff --git a/calendar/experiments/calendar/schema/calendar-items.json b/calendar/experiments/calendar/schema/calendar-items.json index 73a8b0d..d8ae00a 100644 --- a/calendar/experiments/calendar/schema/calendar-items.json +++ b/calendar/experiments/calendar/schema/calendar-items.json @@ -10,30 +10,15 @@ "id": { "type": "string" }, "calendarId": { "type": "string" }, "type": { "type": "string", "enum": ["event", "task"] }, - "title": { "type": "string", "optional": true }, - "description": { "type": "string", "optional": true }, - "location": { "type": "string", "optional": true }, - "categories": { "type": "array", "items": { "type": "string" }, "optional": true }, - "startDate": { "type": "string", "optional": true }, - "endDate": { "type": "string", "optional": true }, "instance": { "type": "string", "optional": true }, - "formats": { "$ref": "RawCalendarItem", "optional": true }, + "format": { "$ref": "CalendarItemFormat", "optional": true }, + "item": { "$ref": "RawCalendarItem" }, "metadata": { "type": "object", "additionalProperties": { "type": "any" }, "optional": true } } }, { "id": "RawCalendarItem", - "type": "object", - "properties": { - "use": { - "choices": [ - { "type": "null" }, - { "$ref": "CalendarItemFormats" } - ] - }, - "ical": { "type": "string", "optional": true }, - "jcal": { "type": "any", "optional": true } - } + "type": "any" }, { "id": "CalendarItemFormats", @@ -116,13 +101,8 @@ "properties": { "id": { "type": "string", "optional": true }, "type": { "type": "string", "enum": ["event", "task"] }, - "title": { "type": "string", "optional": true }, - "description": { "type": "string", "optional": true }, - "location": { "type": "string", "optional": true }, - "categories": { "type": "array", "items": { "type": "string" }, "optional": true }, - "startDate": { "type": "string", "optional": true }, - "endDate": { "type": "string", "optional": true }, - "formats": { "$ref": "RawCalendarItem", "optional": true }, + "format": { "$ref": "CalendarItemFormats", "optional": true }, + "item": { "$ref": "RawCalendarItem" }, "returnFormat": { "$ref": "ReturnFormat", "optional": true }, "metadata": { "type": "object", "properties": {}, "additionalProperties": { "type": "any" }, "optional": true } } @@ -140,13 +120,8 @@ "name": "updateProperties", "type": "object", "properties": { - "title": { "type": "string", "optional": true }, - "description": { "type": "string", "optional": true }, - "location": { "type": "string", "optional": true }, - "categories": { "type": "array", "items": { "type": "string" }, "optional": true }, - "startDate": { "type": "string", "optional": true }, - "endDate": { "type": "string", "optional": true }, - "formats": { "$ref": "RawCalendarItem", "optional": true }, + "format": { "$ref": "CalendarItemFormat", "optional": true }, + "item": { "$ref": "RawCalendarItem" }, "returnFormat": { "$ref": "ReturnFormat", "optional": true }, "metadata": { "type": "object", "additionalProperties": { "type": "any" }, "optional": true } } From 28b4eaadaf17eba3589f9240c1afd254ced381c7 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Mon, 19 Aug 2024 17:58:37 +0200 Subject: [PATCH 16/17] calendar: Persist calendar capabilities --- .../calendar/parent/ext-calendar-calendars.js | 1 + .../calendar/parent/ext-calendar-provider.js | 17 +++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/calendar/experiments/calendar/parent/ext-calendar-calendars.js b/calendar/experiments/calendar/parent/ext-calendar-calendars.js index a544be5..e3fe6b8 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-calendars.js +++ b/calendar/experiments/calendar/parent/ext-calendar-calendars.js @@ -141,6 +141,7 @@ this.calendar_calendars = class extends ExtensionAPI { // TODO validate capability names const unwrappedCalendar = calendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject; unwrappedCalendar.capabilities = Object.assign({}, unwrappedCalendar.capabilities, updateProperties.capabilities); + calendar.setProperty("extensionCapabilities", JSON.stringify(unwrappedCalendar.capabilities)); } if (updateProperties.lastError !== undefined) { diff --git a/calendar/experiments/calendar/parent/ext-calendar-provider.js b/calendar/experiments/calendar/parent/ext-calendar-provider.js index cd5dce3..89964eb 100644 --- a/calendar/experiments/calendar/parent/ext-calendar-provider.js +++ b/calendar/experiments/calendar/parent/ext-calendar-provider.js @@ -134,17 +134,8 @@ class ExtCalendar extends cal.provider.BaseClass { return this.extension.id; } - get capabilities() { - if (!this._capabilities) { - this._capabilities = this.extension.manifest.calendar_provider.capabilities || {}; - } - return this._capabilities; - } - set capabilities(val) { - this._capabilities = val; - } - canRefresh = true; + capabilities = {}; get id() { return super.id; @@ -152,6 +143,12 @@ class ExtCalendar extends cal.provider.BaseClass { set id(val) { super.id = val; if (this.id && this.uri) { + try { + this.capabilities = JSON.parse(super.getProperty("extensionCapabilities")); + } catch (e) { + this.capabilities = this.extension.manifest.calendar_provider.capabilities || {}; + } + this.extension.emit("calendar.provider.onInit", this); } } From d7b122c67824d65c624514455096d5a65b5c32ff Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Mon, 19 Aug 2024 18:02:01 +0200 Subject: [PATCH 17/17] calendar: Re-add missing URL constructor --- .eslintrc.js | 3 +++ .../experiments/calendar/parent/ext-calendarItemDetails.js | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 829deee..510cba8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -50,6 +50,9 @@ module.exports = { global: true, Services: true, }, + rules: { + "mozilla/reject-importGlobalProperties": "off" + } } ] }; diff --git a/calendar/experiments/calendar/parent/ext-calendarItemDetails.js b/calendar/experiments/calendar/parent/ext-calendarItemDetails.js index e2a1e05..d0d6fad 100644 --- a/calendar/experiments/calendar/parent/ext-calendarItemDetails.js +++ b/calendar/experiments/calendar/parent/ext-calendarItemDetails.js @@ -8,6 +8,8 @@ var { ExtensionUtils: { ExtensionError } } = ChromeUtils.importESModule("resourc var { ExtensionSupport } = ChromeUtils.importESModule("resource:///modules/ExtensionSupport.sys.mjs"); +Cu.importGlobalProperties(["URL"]); + this.calendarItemDetails = class extends ExtensionAPI { onLoadCalendarItemPanel(window, origLoadCalendarItemPanel, iframeId, url) { const { setupE10sBrowser } = ChromeUtils.importESModule("resource://tb-experiments-calendar/experiments/calendar/ext-calendar-utils.sys.mjs");