diff options
Diffstat (limited to 'comm/mail/components/activity/content')
-rw-r--r-- | comm/mail/components/activity/content/activity-widgets.js | 384 | ||||
-rw-r--r-- | comm/mail/components/activity/content/activity.js | 239 | ||||
-rw-r--r-- | comm/mail/components/activity/content/activity.xhtml | 61 |
3 files changed, 684 insertions, 0 deletions
diff --git a/comm/mail/components/activity/content/activity-widgets.js b/comm/mail/components/activity/content/activity-widgets.js new file mode 100644 index 0000000000..44ee16bff8 --- /dev/null +++ b/comm/mail/components/activity/content/activity-widgets.js @@ -0,0 +1,384 @@ +/* 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/. */ + +"use strict"; + +/* global MozXULElement, activityManager */ + +// Wrap in a block to prevent leaking to window scope. +{ + const { makeFriendlyDateAgo } = ChromeUtils.import( + "resource:///modules/TemplateUtils.jsm" + ); + + let activityStrings = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + ); + + /** + * The ActivityItemBase widget is the base class for all the activity item. + * It initializes activity details: i.e. id, status, icon, name, progress, + * date etc. for the activity widgets. + * + * @abstract + * @augments HTMLLIElement + */ + class ActivityItemBase extends HTMLLIElement { + connectedCallback() { + if (!this.hasChildNodes()) { + // fetch the activity and set the base attributes + this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }); + let actID = this.getAttribute("actID"); + this._activity = activityManager.getActivity(actID); + this._activity.QueryInterface(this.constructor.activityInterface); + + // Construct the children. + this.classList.add("activityitem"); + + let icon = document.createElement("img"); + icon.setAttribute( + "src", + this._activity.iconClass + ? `chrome://messenger/skin/icons/new/activity/${this._activity.iconClass}Icon.svg` + : this.constructor.defaultIconSrc + ); + icon.setAttribute("alt", ""); + this.appendChild(icon); + + let display = document.createElement("span"); + display.classList.add("displayText"); + this.appendChild(display); + + if (this.isEvent || this.isWarning) { + let time = document.createElement("time"); + time.classList.add("dateTime"); + this.appendChild(time); + } + + if (this.isProcess) { + let progress = document.createElement("progress"); + progress.setAttribute("value", "0"); + progress.setAttribute("max", "100"); + progress.classList.add("progressmeter"); + this.appendChild(progress); + } + + let statusText = document.createElement("span"); + statusText.setAttribute("role", "note"); + statusText.classList.add("statusText"); + this.appendChild(statusText); + } + // (Re-)Attach the listener. + this.attachToActivity(); + } + + disconnectedCallback() { + this.detachFromActivity(); + } + + get isProcess() { + return this.constructor.activityInterface == Ci.nsIActivityProcess; + } + + get isEvent() { + return this.constructor.activityInterface == Ci.nsIActivityEvent; + } + + get isWarning() { + return this.constructor.activityInterface == Ci.nsIActivityWarning; + } + + get isGroup() { + return false; + } + + get activity() { + return this._activity; + } + + detachFromActivity() { + if (this.activityListener) { + this._activity.removeListener(this.activityListener); + } + } + + attachToActivity() { + if (this.activityListener) { + this._activity.addListener(this.activityListener); + } + } + + static _dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "long", + timeStyle: "short", + }); + + /** + * The time the activity occurred. + * + * @type {number} - The time in milliseconds since the epoch. + */ + set dateTime(time) { + let element = this.querySelector(".dateTime"); + if (!element) { + return; + } + time = new Date(parseInt(time)); + + element.setAttribute("datetime", time.toISOString()); + element.textContent = makeFriendlyDateAgo(time); + element.setAttribute( + "title", + this.constructor._dateTimeFormatter.format(time) + ); + } + + /** + * The text that describes additional information to the user. + * + * @type {string} + */ + set statusText(val) { + this.querySelector(".statusText").textContent = val; + } + + get statusText() { + return this.querySelector(".statusText").textContent; + } + + /** + * The text that describes the activity to the user. + * + * @type {string} + */ + set displayText(val) { + this.querySelector(".displayText").textContent = val; + } + + get displayText() { + return this.querySelector(".displayText").textContent; + } + } + + /** + * The MozActivityEvent widget displays information about events (like + * deleting or moving the message): e.g image, name, date and description. + * It is typically used in Activity Manager window. + * + * @augments ActivityItemBase + */ + class ActivityEventItem extends ActivityItemBase { + static defaultIconSrc = + "chrome://messenger/skin/icons/new/activity/defaultEventIcon.svg"; + static activityInterface = Ci.nsIActivityEvent; + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("is", "activity-event-item"); + + this.displayText = this.activity.displayText; + this.statusText = this.activity.statusText; + this.dateTime = this.activity.completionTime; + } + } + + customElements.define("activity-event-item", ActivityEventItem, { + extends: "li", + }); + + /** + * The ActivityGroupItem widget displays information about the activities of + * the group: e.g. name of the group, list of the activities with their name, + * progress and icon. It is shown in Activity Manager window. It gets removed + * when there is no activities from the group. + * + * @augments HTMLLIElement + */ + class ActivityGroupItem extends HTMLLIElement { + constructor() { + super(); + + let heading = document.createElement("h2"); + heading.classList.add("contextDisplayText"); + this.appendChild(heading); + + let list = document.createElement("ul"); + list.classList.add("activitygroup-list", "activityview"); + this.appendChild(list); + + this.classList.add("activitygroup"); + this.setAttribute("is", "activity-group-item"); + } + + /** + * The text heading for the group, as seen by the user. + * + * @type {string} + */ + set contextDisplayText(val) { + this.querySelector(".contextDisplayText").textContent = val; + } + + get contextDisplayText() { + return this.querySelctor(".contextDisplayText").textContent; + } + + get isGroup() { + return true; + } + } + + customElements.define("activity-group-item", ActivityGroupItem, { + extends: "li", + }); + + /** + * The ActivityProcessItem widget displays information about the internal + * process : e.g image, progress, name, date and description. + * It is typically used in Activity Manager window. + * + * @augments ActivityItemBase + */ + class ActivityProcessItem extends ActivityItemBase { + static defaultIconSrc = + "chrome://messenger/skin/icons/new/activity/deafultProcessIcon.svg"; + static activityInterface = Ci.nsIActivityProcess; + static textMap = { + paused: activityStrings.GetStringFromName("paused2"), + canceled: activityStrings.GetStringFromName("canceled"), + failed: activityStrings.GetStringFromName("failed"), + waitingforinput: activityStrings.GetStringFromName("waitingForInput"), + waitingforretry: activityStrings.GetStringFromName("waitingForRetry"), + }; + + constructor() { + super(); + + this.activityListener = { + onStateChanged: (activity, oldState) => { + // change the view of the element according to the new state + // default states for each item + let hideProgressMeter = false; + let statusText = this.statusText; + + switch (this.activity.state) { + case Ci.nsIActivityProcess.STATE_INPROGRESS: + statusText = ""; + break; + case Ci.nsIActivityProcess.STATE_COMPLETED: + hideProgressMeter = true; + statusText = ""; + break; + case Ci.nsIActivityProcess.STATE_CANCELED: + hideProgressMeter = true; + statusText = this.constructor.textMap.canceled; + break; + case Ci.nsIActivityProcess.STATE_PAUSED: + statusText = this.constructor.textMap.paused; + break; + case Ci.nsIActivityProcess.STATE_WAITINGFORINPUT: + statusText = this.constructor.textMap.waitingforinput; + break; + case Ci.nsIActivityProcess.STATE_WAITINGFORRETRY: + hideProgressMeter = true; + statusText = this.constructor.textMap.waitingforretry; + break; + } + + // Set the visibility + let meter = this.querySelector(".progressmeter"); + meter.hidden = hideProgressMeter; + + // Ensure progress meter not active when hidden + if (hideProgressMeter) { + meter.value = 0; + } + + // Update Status text and Display Text Areas + // In some states we need to modify Display Text area of + // the process (e.g. Failure). + this.statusText = statusText; + }, + onProgressChanged: ( + activity, + statusText, + workUnitsComplete, + totalWorkUnits + ) => { + let element = document.querySelector(".progressmeter"); + if (totalWorkUnits == 0) { + element.removeAttribute("value"); + } else { + let _percentComplete = (100.0 * workUnitsComplete) / totalWorkUnits; + element.value = _percentComplete; + } + this.statusText = statusText; + }, + }; + } + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("is", "activity-process-item"); + + this.displayText = this.activity.displayText; + // make sure that custom element reflects the latest state of the process + this.activityListener.onStateChanged( + this.activity.state, + Ci.nsIActivityProcess.STATE_NOTSTARTED + ); + this.activityListener.onProgressChanged( + this.activity, + this.activity.lastStatusText, + this.activity.workUnitComplete, + this.activity.totalWorkUnits + ); + } + + get inProgress() { + return this.activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS; + } + + get isRemovable() { + return ( + this.activity.state == Ci.nsIActivityProcess.STATE_COMPLETED || + this.activity.state == Ci.nsIActivityProcess.STATE_CANCELED + ); + } + } + + customElements.define("activity-process-item", ActivityProcessItem, { + extends: "li", + }); + + /** + * The ActivityWarningItem widget displays information about + * warnings : e.g image, name, date and description. + * It is typically used in Activity Manager window. + * + * @augments ActivityItemBase + */ + class ActivityWarningItem extends ActivityItemBase { + static defaultIconSrc = + "chrome://messenger/skin/icons/new/activity/warning.svg"; + static activityInterface = Ci.nsIActivityWarning; + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("is", "activity-warning-item"); + + this.displayText = this.activity.displayText; + this.dateTime = this.activity.time; + this.statusText = this.activity.recoveryTipText; + } + } + + customElements.define("activity-warning-item", ActivityWarningItem, { + extends: "li", + }); +} diff --git a/comm/mail/components/activity/content/activity.js b/comm/mail/components/activity/content/activity.js new file mode 100644 index 0000000000..dcaba3d808 --- /dev/null +++ b/comm/mail/components/activity/content/activity.js @@ -0,0 +1,239 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +const activityManager = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager +); + +var ACTIVITY_LIMIT = 250; + +var activityObject = { + _activityMgrListener: null, + _activitiesView: null, + _activityLogger: console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }), + _ignoreNotifications: false, + _groupCache: new Map(), + + // Utility Functions for Activity element management + + /** + * Creates the proper element for the given activity + */ + createActivityWidget(type) { + let element = document.createElement("li", { + is: type.bindingName, + }); + + if (element) { + element.setAttribute("actID", type.id); + } + + return element; + }, + + /** + * Returns the activity group element that matches the context_type + * and context of the given activity, if any. + */ + getActivityGroupElementByContext(aContextType, aContextObj) { + return this._groupCache.get(aContextType + ":" + aContextObj); + }, + + /** + * Inserts the given element into the correct position on the + * activity manager window. + */ + placeActivityElement(element) { + if (element.isGroup || element.isProcess) { + this._activitiesView.insertBefore( + element, + this._activitiesView.firstElementChild + ); + } else { + let next = this._activitiesView.firstElementChild; + while (next && (next.isWarning || next.isProcess || next.isGroup)) { + next = next.nextElementSibling; + } + if (next) { + this._activitiesView.insertBefore(element, next); + } else { + this._activitiesView.appendChild(element); + } + } + if (element.isGroup) { + this._groupCache.set( + element.contextType + ":" + element.contextObj, + element + ); + } + while (this._activitiesView.children.length > ACTIVITY_LIMIT) { + this.removeActivityElement( + this._activitiesView.lastElementChild.getAttribute("actID") + ); + } + }, + + /** + * Adds a new element to activity manager window for the + * given activity. It is called by ActivityMgrListener when + * a new activity is added into the activity manager's internal + * list. + */ + addActivityElement(aID, aActivity) { + try { + this._activityLogger.info(`Adding ActivityElement: ${aID}, ${aActivity}`); + // get |groupingStyle| of the activity. Grouping style determines + // whether we show the activity standalone or grouped by context in + // the activity manager window. + let isGroupByContext = + aActivity.groupingStyle == Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + + // find out if an activity group has already been created for this context + let group = null; + if (isGroupByContext) { + group = this.getActivityGroupElementByContext( + aActivity.contextType, + aActivity.contextObj + ); + // create a group if it's not already created. + if (!group) { + group = document.createElement("li", { + is: "activity-group-item", + }); + this._activityLogger.info("created group element"); + // Set the context type and object of the newly created group + group.contextType = aActivity.contextType; + group.contextObj = aActivity.contextObj; + group.contextDisplayText = aActivity.contextDisplayText; + + // add group into the list + this.placeActivityElement(group); + } + } + + // create the appropriate element for the activity + let actElement = this.createActivityWidget(aActivity); + this._activityLogger.info("created activity element"); + + if (group) { + // get the inner list element of the group + let groupView = group.querySelector(".activitygroup-list"); + groupView.appendChild(actElement); + } else { + this.placeActivityElement(actElement); + } + } catch (e) { + this._activityLogger.error("addActivityElement: " + e); + throw e; + } + }, + + /** + * Removes the activity element from the activity manager window. + * It is called by ActivityMgrListener when the activity in question + * is removed from the activity manager's internal list. + */ + removeActivityElement(aID) { + this._activityLogger.info("removing Activity ID: " + aID); + let item = this._activitiesView.querySelector(`[actID="${aID}"]`); + + if (item) { + let group = item.closest(".activitygroup"); + item.remove(); + if (group && !group.querySelector(".activityitem")) { + // Empty group is removed. + this._groupCache.delete(group.contextType + ":" + group.contextObj); + group.remove(); + } + } + }, + + // ----------------- + // Startup, Shutdown + + startup() { + try { + this._activitiesView = document.getElementById("activityView"); + + let activities = activityManager.getActivities(); + for ( + let iActivity = Math.max(0, activities.length - ACTIVITY_LIMIT); + iActivity < activities.length; + iActivity++ + ) { + let activity = activities[iActivity]; + this.addActivityElement(activity.id, activity); + } + + // start listening changes in the activity manager's + // internal list + this._activityMgrListener = new this.ActivityMgrListener(); + activityManager.addListener(this._activityMgrListener); + } catch (e) { + this._activityLogger.error("Exception: " + e); + } + }, + + rebuild() { + let activities = activityManager.getActivities(); + for (let activity of activities) { + this.addActivityElement(activity.id, activity); + } + }, + + shutdown() { + activityManager.removeListener(this._activityMgrListener); + }, + + // ----------------- + // Utility Functions + + /** + * Remove all activities not in-progress from the activity list. + */ + clearActivityList() { + this._activityLogger.debug("clearActivityList"); + + this._ignoreNotifications = true; + // If/when we implement search, we'll want to remove just the items + // that are on the search display, however for now, we'll just clear up + // everything. + activityManager.cleanUp(); + + while (this._activitiesView.lastChild) { + this._activitiesView.lastChild.remove(); + } + + this._groupCache.clear(); + this.rebuild(); + this._ignoreNotifications = false; + this._activitiesView.focus(); + }, +}; + +// An object to monitor nsActivityManager operations. This class acts as +// binding layer between nsActivityManager and nsActivityManagerUI objects. +activityObject.ActivityMgrListener = function () {}; +activityObject.ActivityMgrListener.prototype = { + onAddedActivity(aID, aActivity) { + activityObject._activityLogger.info(`added activity: ${aID} ${aActivity}`); + if (!activityObject._ignoreNotifications) { + activityObject.addActivityElement(aID, aActivity); + } + }, + + onRemovedActivity(aID) { + if (!activityObject._ignoreNotifications) { + activityObject.removeActivityElement(aID); + } + }, +}; + +window.addEventListener("load", () => activityObject.startup()); +window.addEventListener("unload", () => activityObject.shutdown()); diff --git a/comm/mail/components/activity/content/activity.xhtml b/comm/mail/components/activity/content/activity.xhtml new file mode 100644 index 0000000000..cdff19cbe6 --- /dev/null +++ b/comm/mail/components/activity/content/activity.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +#ifdef XP_UNIX +#ifndef XP_MACOSX +#define XP_GNOME 1 +#endif +#endif + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css"?> +<?xml-stylesheet href="chrome://messenger/skin/activity/activity.css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css"?> + +<!DOCTYPE html [ +<!ENTITY % activityManagerDTD SYSTEM "chrome://messenger/locale/activity.dtd"> +%activityManagerDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="activityManager" windowtype="Activity:Manager" + width="&window.width2;" height="&window.height;" + screenX="10" screenY="10" + persist="width height screenX screenY sizemode" + lightweightthemes="true"> +<head> + <title>&activity.title;</title> + + <script defer="defer" src="chrome://messenger/content/activity.js"></script> + <script defer="defer" src="chrome://messenger/content/activity-widgets.js"></script> +</head> +<body> + <xul:keyset id="activityKeys"> + <xul:key id="key_close" key="&cmd.close.commandkey;" + oncommand="window.close();" modifiers="accel"/> +#ifdef XP_GNOME + <xul:key id="key_close2" key="&cmd.close2Unix.commandkey;" + oncommand="window.close();" modifiers="accel"/> +#else + <xul:key id="key_close2" key="&cmd.close2.commandkey;" + oncommand="window.close();" modifiers="accel"/> +#endif + <xul:key keycode="VK_ESCAPE" oncommand="window.close();"/> + </xul:keyset> + + <div id="activityContainer"> + <ul id="activityView" class="activityview"></ul> + <button id="clearListButton" + onclick="activityObject.clearActivityList();" + accesskey="&cmd.clearList.accesskey;" + title="&cmd.clearList.tooltip;"> + &cmd.clearList.label; + </button> + </div> +</body> +</html> |