summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/activity/content
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/activity/content')
-rw-r--r--comm/mail/components/activity/content/activity-widgets.js384
-rw-r--r--comm/mail/components/activity/content/activity.js239
-rw-r--r--comm/mail/components/activity/content/activity.xhtml61
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>