summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-invitations-manager.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/calendar-invitations-manager.js385
1 files changed, 385 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-invitations-manager.js b/comm/calendar/base/content/calendar-invitations-manager.js
new file mode 100644
index 0000000000..9a758c4c49
--- /dev/null
+++ b/comm/calendar/base/content/calendar-invitations-manager.js
@@ -0,0 +1,385 @@
+/* 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 { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+var { CalReadableStreamFactory } = ChromeUtils.import(
+ "resource:///modules/CalReadableStreamFactory.jsm"
+);
+
+/* exported openInvitationsDialog, setUpInvitationsManager,
+ * tearDownInvitationsManager
+ */
+
+var gInvitationsManager = null;
+
+/**
+ * Return a cached instance of the invitations manager
+ *
+ * @returns {InvitationsManager} The invitations manager instance.
+ */
+function getInvitationsManager() {
+ if (!gInvitationsManager) {
+ gInvitationsManager = new InvitationsManager();
+ }
+ return gInvitationsManager;
+}
+
+// Listeners, observers, set up, tear down, opening dialog, etc. This code kept
+// separate from the InvitationsManager class itself for separation of concerns.
+
+// == invitations link
+const FIRST_DELAY_STARTUP = 100;
+const FIRST_DELAY_RESCHEDULE = 100;
+const FIRST_DELAY_REGISTER = 10000;
+const FIRST_DELAY_UNREGISTER = 0;
+
+var gInvitationsCalendarManagerObserver = {
+ mStoredThis: this,
+ QueryInterface: ChromeUtils.generateQI(["calICalendarManagerObserver"]),
+
+ onCalendarRegistered(aCalendar) {
+ this.mStoredThis.rescheduleInvitationsUpdate(FIRST_DELAY_REGISTER);
+ },
+
+ onCalendarUnregistering(aCalendar) {
+ this.mStoredThis.rescheduleInvitationsUpdate(FIRST_DELAY_UNREGISTER);
+ },
+
+ onCalendarDeleting(aCalendar) {},
+};
+
+function scheduleInvitationsUpdate(firstDelay) {
+ getInvitationsManager().scheduleInvitationsUpdate(firstDelay);
+}
+
+function rescheduleInvitationsUpdate(firstDelay) {
+ getInvitationsManager().cancelInvitationsUpdate();
+ scheduleInvitationsUpdate(firstDelay);
+}
+
+function openInvitationsDialog() {
+ getInvitationsManager().cancelInvitationsUpdate();
+ getInvitationsManager().openInvitationsDialog();
+}
+
+function setUpInvitationsManager() {
+ scheduleInvitationsUpdate(FIRST_DELAY_STARTUP);
+ cal.manager.addObserver(gInvitationsCalendarManagerObserver);
+}
+
+function tearDownInvitationsManager() {
+ cal.manager.removeObserver(gInvitationsCalendarManagerObserver);
+}
+
+/**
+ * The invitations manager class constructor
+ *
+ * XXX do we really need this to be an instance?
+ *
+ * @class
+ */
+function InvitationsManager() {
+ this.mItemList = [];
+ this.mStartDate = null;
+ this.mTimer = null;
+
+ window.addEventListener("unload", () => {
+ // Unload handlers get removed automatically
+ this.cancelInvitationsUpdate();
+ });
+}
+
+InvitationsManager.prototype = {
+ mItemList: null,
+ mStartDate: null,
+ mTimer: null,
+ mPendingRequests: null,
+
+ /**
+ * Schedule an update for the invitations manager asynchronously.
+ *
+ * @param firstDelay The timeout before the operation should start.
+ */
+ scheduleInvitationsUpdate(firstDelay) {
+ this.cancelInvitationsUpdate();
+
+ this.mTimer = setTimeout(async () => {
+ if (Services.prefs.getBoolPref("calendar.invitations.autorefresh.enabled", true)) {
+ this.mTimer = setInterval(
+ async () => this._doInvitationsUpdate(),
+ Services.prefs.getIntPref("calendar.invitations.autorefresh.timeout", 3) * 60000
+ );
+ }
+ await this._doInvitationsUpdate();
+ }, firstDelay);
+ },
+
+ async _doInvitationsUpdate() {
+ let items;
+ try {
+ items = await cal.iterate.streamToArray(this.getInvitations());
+ } catch (e) {
+ cal.ERROR(e);
+ }
+ this.toggleInvitationsPanel(items);
+ },
+
+ /**
+ * Toggles the display of the invitations panel in the status bar depending
+ * on the number of invitation items found.
+ *
+ * @param {calIItemBase[]?} items - The invitations found, if empty or not
+ * provided, the panel will not be displayed.
+ */
+ toggleInvitationsPanel(items) {
+ let invitationsBox = document.getElementById("calendar-invitations-panel");
+ if (items) {
+ let count = items.length;
+ let value = cal.l10n.getLtnString("invitationsLink.label", [count]);
+ document.getElementById("calendar-invitations-label").value = value;
+ if (count) {
+ invitationsBox.removeAttribute("hidden");
+ return;
+ }
+ }
+
+ invitationsBox.setAttribute("hidden", "true");
+ },
+
+ /**
+ * Cancel pending any pending invitations update.
+ */
+ cancelInvitationsUpdate() {
+ clearTimeout(this.mTimer);
+ },
+
+ /**
+ * Cancel any pending queries for invitations.
+ */
+ async cancelPendingRequests() {
+ return this.mPendingRequests && this.mPendingRequests.cancel();
+ },
+
+ /**
+ * Retrieve invitations from all calendars. Notify all passed
+ * operation listeners.
+ *
+ * @returns {ReadableStream<calIItemBase>}
+ */
+ getInvitations() {
+ this.updateStartDate();
+ this.deleteAllItems();
+
+ let streams = [];
+ for (let calendar of cal.manager.getCalendars()) {
+ if (!cal.acl.isCalendarWritable(calendar) || calendar.getProperty("disabled")) {
+ continue;
+ }
+
+ // temporary hack unless calCachedCalendar supports REQUEST_NEEDS_ACTION filter:
+ calendar = calendar.getProperty("cache.uncachedCalendar");
+ if (!calendar) {
+ continue;
+ }
+
+ let endDate = this.mStartDate.clone();
+ endDate.year += 1;
+ streams.push(
+ calendar.getItems(
+ Ci.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION |
+ Ci.calICalendar.ITEM_FILTER_TYPE_ALL |
+ // we need to retrieve by occurrence to properly filter exceptions,
+ // should be fixed with bug 416975
+ Ci.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES,
+ 0,
+ this.mStartDate,
+ endDate /* we currently cannot pass null here, because of bug 416975 */
+ )
+ );
+ }
+
+ let self = this;
+ let mHandledItems = {};
+ return CalReadableStreamFactory.createReadableStream({
+ async start(controller) {
+ await self.cancelPendingRequests();
+
+ self.mPendingRequests = cal.iterate.streamValues(
+ CalReadableStreamFactory.createCombinedReadableStream(streams)
+ );
+
+ for await (let items of self.mPendingRequests) {
+ for (let item of items) {
+ // we need to retrieve by occurrence to properly filter exceptions,
+ // should be fixed with bug 416975
+ item = item.parentItem;
+ let hid = item.hashId;
+ if (!mHandledItems[hid]) {
+ mHandledItems[hid] = true;
+ self.addItem(item);
+ }
+ }
+ }
+
+ self.mItemList.sort((a, b) => {
+ return a.startDate.compare(b.startDate);
+ });
+
+ controller.enqueue(self.mItemList.slice());
+ controller.close();
+ },
+ close() {
+ self.mPendingRequests = null;
+ },
+ });
+ },
+
+ /**
+ * Open the invitations dialog, non-modal.
+ *
+ * XXX Passing these listeners in instead of keeping them in the window
+ * sounds fishy to me. Maybe there is a more encapsulated solution.
+ */
+ openInvitationsDialog() {
+ let args = {};
+ args.queue = [];
+ args.finishedCallBack = () => this.scheduleInvitationsUpdate(FIRST_DELAY_RESCHEDULE);
+ args.invitationsManager = this;
+ // the dialog will reset this to auto when it is done loading
+ window.setCursor("wait");
+ // open the dialog
+ window.openDialog(
+ "chrome://calendar/content/calendar-invitations-dialog.xhtml",
+ "_blank",
+ "chrome,titlebar,resizable",
+ args
+ );
+ },
+
+ /**
+ * Process the passed job queue. A job is an object that consists of an
+ * action, a newItem and and oldItem. This processor only takes "modify"
+ * operations into account.
+ *
+ * @param queue The array of objects to process.
+ */
+ async processJobQueue(queue) {
+ // TODO: undo/redo
+ for (let i = 0; i < queue.length; i++) {
+ let job = queue[i];
+ let oldItem = job.oldItem;
+ let newItem = job.newItem;
+ switch (job.action) {
+ case "modify":
+ let item = await newItem.calendar.modifyItem(newItem, oldItem);
+ cal.itip.checkAndSend(Ci.calIOperationListener.MODIFY, item, oldItem);
+ this.deleteItem(item);
+ this.addItem(item);
+ break;
+ default:
+ break;
+ }
+ }
+ },
+
+ /**
+ * Checks if the internal item list contains the given item
+ * XXXdbo Please document these correctly.
+ *
+ * @param item The item to look for.
+ * @returns A boolean value indicating if the item was found.
+ */
+ hasItem(item) {
+ let hid = item.hashId;
+ return this.mItemList.some(item_ => hid == item_.hashId);
+ },
+
+ /**
+ * Adds an item to the internal item list.
+ * XXXdbo Please document these correctly.
+ *
+ * @param item The item to add.
+ */
+ addItem(item) {
+ let recInfo = item.recurrenceInfo;
+ if (recInfo && !cal.itip.isOpenInvitation(item)) {
+ // scan exceptions:
+ let ids = recInfo.getExceptionIds();
+ for (let id of ids) {
+ let ex = recInfo.getExceptionFor(id);
+ if (ex && this.validateItem(ex) && !this.hasItem(ex)) {
+ this.mItemList.push(ex);
+ }
+ }
+ } else if (this.validateItem(item) && !this.hasItem(item)) {
+ this.mItemList.push(item);
+ }
+ },
+
+ /**
+ * Removes an item from the internal item list
+ * XXXdbo Please document these correctly.
+ *
+ * @param item The item to remove.
+ */
+ deleteItem(item) {
+ let id = item.id;
+ this.mItemList.filter(item_ => id != item_.id);
+ },
+
+ /**
+ * Remove all items from the internal item list
+ * XXXdbo Please document these correctly.
+ */
+ deleteAllItems() {
+ this.mItemList = [];
+ },
+
+ /**
+ * Helper function to create a start date to search from. This date is the
+ * current time with hour/minute/second set to zero.
+ *
+ * @returns Potential start date.
+ */
+ getStartDate() {
+ let date = cal.dtz.now();
+ date.second = 0;
+ date.minute = 0;
+ date.hour = 0;
+ return date;
+ },
+
+ /**
+ * Updates the start date for the invitations manager to the date returned
+ * from this.getStartDate(), unless the previously existing start date is
+ * the same or after what getStartDate() returned.
+ */
+ updateStartDate() {
+ if (this.mStartDate) {
+ let startDate = this.getStartDate();
+ if (startDate.compare(this.mStartDate) > 0) {
+ this.mStartDate = startDate;
+ }
+ } else {
+ this.mStartDate = this.getStartDate();
+ }
+ },
+
+ /**
+ * Checks if the item is valid for the invitation manager. Checks if the
+ * item is in the range of the invitation manager and if the item is a valid
+ * invitation.
+ *
+ * @param item The item to check
+ * @returns A boolean indicating if the item is a valid invitation.
+ */
+ validateItem(item) {
+ if (item.calendar instanceof Ci.calISchedulingSupport && !item.calendar.isInvitation(item)) {
+ return false; // exclude if organizer has invited himself
+ }
+ let start = item[cal.dtz.startDateProp(item)] || item[cal.dtz.endDateProp(item)];
+ return cal.itip.isOpenInvitation(item) && start.compare(this.mStartDate) >= 0;
+ },
+};