Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Many new calendar features #34

Merged
merged 17 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ module.exports = {
global: true,
Services: true,
},
rules: {
"mozilla/reject-importGlobalProperties": "off"
}
}
]
};
39 changes: 39 additions & 0 deletions calendar/experiments/calendar/child/ext-calendar-timezones.js
Original file line number Diff line number Diff line change
@@ -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) {
const 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;
},
}
}
};
}
};
187 changes: 106 additions & 81 deletions calendar/experiments/calendar/ext-calendar-utils.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -45,7 +49,6 @@ export function getCachedCalendar(calendar) {
}

export function isCachedCalendar(id) {
// TODO make this better
return id.endsWith("#cache");
}

Expand All @@ -60,78 +63,104 @@ 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",
};

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?
}

return props;
}

export function propsToItem(props, baseItem) {
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);
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 (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;
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;
}

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 (subComp.hasProperty("recurrence-id")) {
exceptions.push(subComp);
continue;
}

if (parent) {
throw new ExtensionError("Cannot parse more than one parent item");
}

parent = generateItem(subComp);
}
if (props.location) {
item.setProperty("location", props.location);

if (!parent) {
throw new ExtensionError("TODO need to retrieve a parent item from storage");
}
if (props.categories) {
item.setCategories(props.categories);

if (exceptions.length && !parent.recurrenceInfo) {
throw new ExtensionError("Exceptions were supplied to a non-recurring item");
}

if (props.type == "event") {
// TODO need to do something about timezone
if (props.startDate) {
item.startDate = cal.createDateTime(props.startDate);
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");
}
if (props.endDate) {
item.endDate = cal.createDateTime(props.endDate);
}
} else if (props.type == "task") {
// entryDate, dueDate, completedDate, isCompleted, duration
parent.recurrenceInfo.modifyException(excItem, true);
}
return parent;
}
throw new ExtensionError("Don't know how to handle component type " + jcalComp.name);
}

export function propsToItem(props) {
let jcalComp;

if (props.format == "ical") {
try {
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.format == "jcal") {
try {
jcalComp = new ICAL.Component(props.item);
} catch (e) {
throw new ExtensionError("Could not parse jCal", { cause: e });
}
return parseJcalData(jcalComp);
}
return item;

throw new ExtensionError("Invalid item format: " + props.format);
}

export function convertItem(item, options, extension) {
Expand All @@ -141,18 +170,22 @@ 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;
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) {
const jcalId = ICAL.design.icalendar.value[recId.length == 8 ? "date" : "date-time"].fromICAL(recId);
props.instance = jcalId;
}

if (isOwnCalendar(item.calendar, extension)) {
props.metadata = {};
Expand All @@ -166,34 +199,27 @@ export function convertItem(item, options, extension) {
}

if (options?.returnFormat) {
props.formats = { use: null };
let formats = options.returnFormat;
if (!Array.isArray(formats)) {
formats = [formats];
}
props.format = options.returnFormat;

for (const format of formats) {
switch (format) {
case "ical":
props.formats.ical = item.icalString;
break;
case "jcal":
// TODO shortcut when using icaljs backend
props.formats.jcal = ICAL.parse(item.icalString);
break;
default:
throw new ExtensionError("Invalid format specified: " + format);
}
const serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance(
Ci.calIIcsSerializer
);
serializer.addItems([item]);
const icalString = serializer.serializeToString();

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;
}

Expand Down Expand Up @@ -250,7 +276,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;
Expand Down
28 changes: 26 additions & 2 deletions calendar/experiments/calendar/parent/ext-calendar-calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -65,7 +69,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);
Expand All @@ -87,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);

Expand Down Expand Up @@ -114,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]);
Expand All @@ -124,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) {
Expand Down Expand Up @@ -237,6 +255,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;
Expand Down
Loading