/* 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/. */ /* exported onDismissAllAlarms, setupWindow, finishWindow, addWidgetFor, * removeWidgetFor, onSelectAlarm, ensureCalendarVisible */ /* global MozElements */ /* import-globals-from ../item-editing/calendar-item-editing.js */ var { cal } = ChromeUtils.importESModule("resource:///modules/calendar/calUtils.sys.mjs"); var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs"); var lazy = {}; ChromeUtils.defineLazyGetter( lazy, "l10n", () => new Localization(["calendar/calendar.ftl", "calendar/calendar-alarms.ftl"], true) ); window.addEventListener("load", () => { setupWindow(); window.arguments[0].wrappedJSObject.window_onLoad(); }); window.addEventListener("unload", finishWindow); window.addEventListener("focus", onFocusWindow); window.addEventListener("keypress", event => { if (event.key == "Escape") { window.close(); } }); var gShutdownDetected = false; /** * Detects the "mail-unloading-messenger" notification to prevent snoozing items * as well as closes this window when the main window is closed. Not doing so can * cause data loss with CalStorageCalendar. */ var gShutdownObserver = { observe() { const windows = Array.from(Services.wm.getEnumerator("mail:3pane")); if (windows.filter(win => !win.closed).length == 0) { gShutdownDetected = true; window.close(); } }, }; addEventListener("DOMContentLoaded", () => { document.getElementById("alarm-snooze-all-popup").addEventListener("snooze", event => { snoozeAllItems(event.detail); }); }); ChromeUtils.defineLazyGetter(this, "gReadOnlyNotification", () => { return new MozElements.NotificationBox(element => { element.setAttribute("notificationside", "top"); document.getElementById("readonly-notification").append(element); }); }); /** * Helper function to get the alarm service and cache it. * * @returns {calIAlarmService} The alarm service component. */ function getAlarmService() { if (!("mAlarmService" in window)) { window.mAlarmService = Cc["@mozilla.org/calendar/alarm-service;1"].getService( Ci.calIAlarmService ); } return window.mAlarmService; } /** * Event handler for the 'snooze' event. Snoozes the given alarm by the given * number of minutes using the alarm service. * * @param {Event} event - The snooze event. */ function onSnoozeAlarm(event) { // reschedule alarm: const duration = getDuration(event.detail); if (aboveSnoozeLimit(duration)) { // we prevent snoozing too far if the alarm wouldn't be displayed return; } getAlarmService().snoozeAlarm(event.target.item, event.target.alarm, duration); } /** * Event handler for the 'dismiss' event. Dismisses the given alarm using the * alarm service. * * @param {Event} event - The snooze event. */ function onDismissAlarm(event) { getAlarmService().dismissAlarm(event.target.item, event.target.alarm); } /** * Called to dismiss all alarms in the alarm window. */ function onDismissAllAlarms() { // removes widgets on the fly: const alarmRichlist = document.getElementById("alarm-richlist"); const parentItems = {}; const widgets = []; // Make a copy of the child nodes as they get modified live for (const node of alarmRichlist.children) { // Check if the node is a valid alarm and is still part of DOM if ( node.parentNode && node.item && node.alarm && !(node.item.parentItem.hashId in parentItems) ) { // We only need to acknowledge one occurrence for repeating items parentItems[node.item.parentItem.hashId] = node.item.parentItem; widgets.push({ item: node.item, alarm: node.alarm }); } } for (const widget of widgets) { getAlarmService().dismissAlarm(widget.item, widget.alarm); } } /** * Event handler fired when the alarm widget's "Details..." label was clicked. * Open the event dialog in the most recent Thunderbird window. * * @param {Event} event - The itemdetails event. */ function onItemDetails(event) { // We want this to happen in a calendar window if possible. Otherwise open // it using our window. const calWindow = cal.window.getCalendarWindow(); if (calWindow) { calWindow.modifyEventWithDialog(event.target.item, true); } else { modifyEventWithDialog(event.target.item, true); } } /** * Sets up the alarm dialog, initializing the default snooze length and setting * up the relative date update timer. */ var gRelativeDateUpdateTimer; function setupWindow() { // We want to update when we are at 0 seconds past the minute. To do so, use // setTimeout to wait until we are there, then setInterval to execute every // minute. Since setInterval is not totally exact, we may run into problems // here. I hope not! const current = new Date(); const timeout = (60 - current.getSeconds()) * 1000; gRelativeDateUpdateTimer = setTimeout(() => { updateRelativeDates(); gRelativeDateUpdateTimer = setInterval(updateRelativeDates, 60 * 1000); }, timeout); // Configure the shutdown observer. Services.obs.addObserver(gShutdownObserver, "mail-unloading-messenger"); // Give focus to the alarm richlist after onload completes. See bug 103197 setTimeout(onFocusWindow, 0); } /** * Unload function for the alarm dialog. If applicable, snooze the remaining * alarms and clean up the relative date update timer. */ function finishWindow() { Services.obs.removeObserver(gShutdownObserver, "mail-unloading-messenger"); if (gShutdownDetected) { return; } const alarmRichlist = document.getElementById("alarm-richlist"); if (alarmRichlist.children.length > 0) { // If there are still items, the window wasn't closed using dismiss // all/snooze all. This can happen when the closer is clicked or escape // is pressed. Snooze all remaining items using the default snooze // property. let snoozePref = Services.prefs.getIntPref("calendar.alarms.defaultsnoozelength", 0); if (snoozePref <= 0) { snoozePref = 5; } snoozeAllItems(snoozePref); } // Stop updating the relative time clearTimeout(gRelativeDateUpdateTimer); } /** * Set up the focused element. If no element is focused, then switch to the * richlist. */ function onFocusWindow() { if (!document.commandDispatcher.focusedElement) { document.getElementById("alarm-richlist").focus(); } } /** * Timer callback to update all relative date labels */ function updateRelativeDates() { const alarmRichlist = document.getElementById("alarm-richlist"); for (const node of alarmRichlist.children) { if (node.item && node.alarm) { node.updateRelativeDateLabel(); } } } /** * Function to snooze all alarms the given number of minutes. * * @param {integer} aDurationMinutes - The duration in minutes. */ function snoozeAllItems(aDurationMinutes) { const duration = getDuration(aDurationMinutes); if (aboveSnoozeLimit(duration)) { // we prevent snoozing too far if the alarm wouldn't be displayed return; } const alarmRichlist = document.getElementById("alarm-richlist"); const parentItems = {}; // Make a copy of the child nodes as they get modified live for (const node of alarmRichlist.children) { // Check if the node is a valid alarm and is still part of DOM if ( node.parentNode && node.item && node.alarm && cal.acl.isCalendarWritable(node.item.calendar) && cal.acl.userCanModifyItem(node.item) && !(node.item.parentItem.hashId in parentItems) ) { // We only need to acknowledge one occurrence for repeating items parentItems[node.item.parentItem.hashId] = node.item.parentItem; getAlarmService().snoozeAlarm(node.item, node.alarm, duration); } } // we need to close the widget here explicitly because the dialog will stay // opened if there a still not snoozable alarms document.getElementById("alarm-snooze-all-button").firstElementChild.hidePopup(); } /** * Receive a calIDuration object for a given number of minutes * * @param {integer} aMinutes - The number of minutes. * @returns {calIDuration} */ function getDuration(aMinutes) { const MINUTESINWEEK = 7 * 24 * 60; // Converting to weeks if any is required to avoid an integer overflow of // duration.minutes as this is of type short. const weeks = Math.floor(aMinutes / MINUTESINWEEK); aMinutes -= weeks * MINUTESINWEEK; const duration = cal.createDuration(); duration.minutes = aMinutes; duration.weeks = weeks; duration.normalize(); return duration; } /** * Check whether the snooze period exceeds the current limitation of the AlarmService and prompt * the user with a message if so * * @param {calIDuration} aDuration - The duration to snooze. * @returns {boolean} */ function aboveSnoozeLimit(aDuration) { const LIMIT = Ci.calIAlarmService.MAX_SNOOZE_MONTHS; const currentTime = cal.dtz.now().getInTimezone(cal.dtz.UTC); const limitTime = currentTime.clone(); limitTime.month += LIMIT; const durationUntilLimit = limitTime.subtractDate(currentTime); if (aDuration.compare(durationUntilLimit) > 0) { const msg = lazy.l10n.formatValueSync("alarm-snooze-limit-exceeded", { count: LIMIT }); cal.showError(msg, window); return true; } return false; } /** * Sets up the window title, counting the number of alarms in the window. */ function setupTitle() { const alarmRichlist = document.getElementById("alarm-richlist"); const reminders = alarmRichlist.children.length; document.title = lazy.l10n.formatValueSync("alarm-window-title-label", { count: reminders }); } /** * Comparison function for the start date of a calendar item and * the start date of a calendar-alarm-widget. * * @param {calIItemBase} aItem - A calendar item for the comparison of the * start date property. * @param {MozCalendarAlarmWidgetRichlistitem} aWidgetItem - The alarm widget * item for the start date comparison with the given calendar item. * @returns {number} 1 if the calendar item starts before the * calendar-alarm-widget. * -1 if the calendar-alarm-widget starts before the calendar item. * 0 otherwise. */ function widgetAlarmComptor(aItem, aWidgetItem) { if (aItem == null || aWidgetItem == null) { return -1; } // Get the dates to compare const aDate = aItem[cal.dtz.startDateProp(aItem)]; const bDate = aWidgetItem[cal.dtz.startDateProp(aWidgetItem)]; return aDate.compare(bDate); } /** * Add an alarm widget for the passed alarm and item. * * @param {calIItemBase} aItem - The calendar item to add a widget for. * @param {calIAlarm} aAlarm - The alarm to add a widget for. */ function addWidgetFor(aItem, aAlarm) { const widget = document.createXULElement("richlistitem", { is: "calendar-alarm-widget-richlistitem", }); const alarmRichlist = document.getElementById("alarm-richlist"); // Add widgets sorted by start date ascending cal.data.binaryInsertNode(alarmRichlist, widget, aItem, widgetAlarmComptor, false); widget.item = aItem; widget.alarm = aAlarm; widget.addEventListener("snooze", onSnoozeAlarm); widget.addEventListener("dismiss", onDismissAlarm); widget.addEventListener("itemdetails", onItemDetails); setupTitle(); doReadOnlyChecks(); if (!alarmRichlist.userSelectedWidget) { // Always select first widget of the list. // Since the onselect event causes scrolling, // we don't want to process the event when adding widgets. alarmRichlist.suppressOnSelect = true; alarmRichlist.selectedItem = alarmRichlist.firstElementChild; alarmRichlist.suppressOnSelect = false; } window.focus(); window.getAttention(); } /** * Remove the alarm widget for the passed alarm and item. * * @param {calIItemBase} aItem - The calendar item to remove the alarm widget for. * @param {calIAlarm} aAlarm - The alarm to remove the widget for. */ function removeWidgetFor(aItem, aAlarm) { const hashId = aItem.hashId; const alarmRichlist = document.getElementById("alarm-richlist"); const nodes = alarmRichlist.children; let notfound = true; for (let i = nodes.length - 1; notfound && i >= 0; --i) { const widget = nodes[i]; if ( widget.item && widget.item.hashId == hashId && widget.alarm && widget.alarm.icalString == aAlarm.icalString ) { if (widget.selected) { // Advance selection if needed widget.control.selectedItem = widget.previousElementSibling || widget.nextElementSibling; } widget.removeEventListener("snooze", onSnoozeAlarm); widget.removeEventListener("dismiss", onDismissAlarm); widget.removeEventListener("itemdetails", onItemDetails); widget.remove(); doReadOnlyChecks(); closeIfEmpty(); notfound = false; } } // Update the title setupTitle(); closeIfEmpty(); } /** * Enables/disables the 'snooze all' button and displays or removes a r/o * notification based on the readability of the calendars of the alarms visible * in the alarm list */ async function doReadOnlyChecks() { let countRO = 0; const alarmRichlist = document.getElementById("alarm-richlist"); for (const node of alarmRichlist.children) { if (!cal.acl.isCalendarWritable(node.item.calendar) || !cal.acl.userCanModifyItem(node.item)) { countRO++; } } // we disable the button if there are only alarms for not-writable items const snoozeAllButton = document.getElementById("alarm-snooze-all-button"); snoozeAllButton.disabled = countRO && countRO == alarmRichlist.children.length; if (snoozeAllButton.disabled) { const tooltip = lazy.l10n.formatValueSync("reminder-disabled-snooze-button-tooltip"); snoozeAllButton.setAttribute("tooltiptext", tooltip); } else { snoozeAllButton.removeAttribute("tooltiptext"); } const notification = gReadOnlyNotification.getNotificationWithValue("calendar-readonly"); if (countRO && !notification) { await gReadOnlyNotification.appendNotification( "calendar-readonly", { label: { "l10n-id": "reminder-readonly-notification", "l10n-args": { label: snoozeAllButton.label }, }, priority: gReadOnlyNotification.PRIORITY_WARNING_MEDIUM, }, null ); } else if (notification && !countRO) { gReadOnlyNotification.removeNotification(notification); } } /** * Close the alarm dialog if there are no further alarm widgets */ function closeIfEmpty() { const alarmRichlist = document.getElementById("alarm-richlist"); // we don't want to close if the alarm service is still loading, as the // removed alarms may be immediately added again. if (!alarmRichlist.hasChildNodes() && !getAlarmService().isLoading) { window.close(); } } /** * Handler function called when an alarm entry in the richlistbox is selected * * @param {Event} event - The DOM event from the click action */ function onSelectAlarm(event) { const richList = document.getElementById("alarm-richlist"); if (richList == event.target) { richList.ensureElementIsVisible(richList.getSelectedItem(0)); richList.userSelectedWidget = true; } } function ensureCalendarVisible() { // This function is called on the alarm dialog from calendar-item-editing.js. // Normally, it makes sure that the calendar being edited is made visible, // but the alarm dialog is too far away from the calendar views that it // makes sense to force visibility for the calendar. Therefore, do nothing. }