diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/activity | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/activity')
19 files changed, 3633 insertions, 0 deletions
diff --git a/comm/mail/components/activity/Activity.jsm b/comm/mail/components/activity/Activity.jsm new file mode 100644 index 0000000000..1b23efe1c7 --- /dev/null +++ b/comm/mail/components/activity/Activity.jsm @@ -0,0 +1,322 @@ +/* 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 EXPORTED_SYMBOLS = ["ActivityProcess", "ActivityEvent", "ActivityWarning"]; + +// Base class for ActivityProcess and ActivityEvent objects + +function Activity() { + this._initLogging(); + this._listeners = []; + this._subjects = []; +} + +Activity.prototype = { + id: -1, + bindingName: "", + iconClass: "", + groupingStyle: Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT, + facet: "", + displayText: "", + initiator: null, + contextType: "", + context: "", + contextObj: null, + + _initLogging() { + this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }); + }, + + addListener(aListener) { + this._listeners.push(aListener); + }, + + removeListener(aListener) { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] == aListener) { + this._listeners.splice(i, 1); + break; + } + } + }, + + addSubject(aSubject) { + this._subjects.push(aSubject); + }, + + getSubjects() { + return this._subjects.slice(); + }, +}; + +function ActivityProcess() { + Activity.call(this); + this.bindingName = "activity-process-item"; + this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; +} + +ActivityProcess.prototype = { + __proto__: Activity.prototype, + + percentComplete: -1, + lastStatusText: "", + workUnitComplete: 0, + totalWorkUnits: 0, + startTime: Date.now(), + _cancelHandler: null, + _pauseHandler: null, + _retryHandler: null, + _state: Ci.nsIActivityProcess.STATE_INPROGRESS, + + init(aDisplayText, aInitiator) { + this.displayText = aDisplayText; + this.initiator = aInitiator; + }, + + get state() { + return this._state; + }, + + set state(val) { + if (val == this._state) { + return; + } + + // test validity of the new state + // + if ( + this._state == Ci.nsIActivityProcess.STATE_INPROGRESS && + !( + val == Ci.nsIActivityProcess.STATE_COMPLETED || + val == Ci.nsIActivityProcess.STATE_CANCELED || + val == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY || + val == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT || + val == Ci.nsIActivityProcess.STATE_PAUSED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + // we cannot change the state after the activity is completed, + // or it is canceled. + if ( + this._state == Ci.nsIActivityProcess.STATE_COMPLETED || + this._state == Ci.nsIActivityProcess.STATE_CANCELED + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + if ( + this._state == Ci.nsIActivityProcess.STATE_PAUSED && + !( + val == Ci.nsIActivityProcess.STATE_COMPLETED || + val == Ci.nsIActivityProcess.STATE_INPROGRESS || + val == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY || + val == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT || + val == Ci.nsIActivityProcess.STATE_CANCELED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + if ( + this._state == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT && + !( + val == Ci.nsIActivityProcess.STATE_INPROGRESS || + val == Ci.nsIActivityProcess.STATE_CANCELED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + if ( + this._state == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY && + !( + val == Ci.nsIActivityProcess.STATE_INPROGRESS || + val == Ci.nsIActivityProcess.STATE_CANCELED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + let oldState = this._state; + this._state = val; + + // let the listeners know about the change + this.log.debug("Notifying onStateChanged listeners"); + for (let value of this._listeners) { + try { + value.onStateChanged(this, oldState); + } catch (e) { + this.log.error("Exception thrown by onStateChanged listener: " + e); + } + } + }, + + setProgress(aStatusText, aWorkUnitsComplete, aTotalWorkUnits) { + if (aTotalWorkUnits == 0) { + this.percentComplete = -1; + this.workUnitComplete = 0; + this.totalWorkUnits = 0; + } else { + this.percentComplete = parseInt( + (100.0 * aWorkUnitsComplete) / aTotalWorkUnits + ); + this.workUnitComplete = aWorkUnitsComplete; + this.totalWorkUnits = aTotalWorkUnits; + } + this.lastStatusText = aStatusText; + + // notify listeners + for (let value of this._listeners) { + try { + value.onProgressChanged( + this, + aStatusText, + aWorkUnitsComplete, + aTotalWorkUnits + ); + } catch (e) { + this.log.error("Exception thrown by onProgressChanged listener: " + e); + } + } + }, + + get cancelHandler() { + return this._cancelHandler; + }, + + set cancelHandler(val) { + this._cancelHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + try { + value.onHandlerChanged(this); + } catch (e) { + this.log.error("Exception thrown by onHandlerChanged listener: " + e); + } + } + }, + + get pauseHandler() { + return this._pauseHandler; + }, + + set pauseHandler(val) { + this._pauseHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + get retryHandler() { + return this._retryHandler; + }, + + set retryHandler(val) { + this._retryHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityProcess", "nsIActivity"]), +}; + +function ActivityEvent() { + Activity.call(this); + this.bindingName = "activity-event-item"; + this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; +} + +ActivityEvent.prototype = { + __proto__: Activity.prototype, + + statusText: "", + startTime: 0, + completionTime: 0, + _undoHandler: null, + + init(aDisplayText, aInitiator, aStatusText, aStartTime, aCompletionTime) { + this.displayText = aDisplayText; + this.statusText = aStatusText; + this.startTime = aStartTime; + if (aCompletionTime) { + this.completionTime = aCompletionTime; + } else { + this.completionTime = Date.now(); + } + this.initiator = aInitiator; + this._completionTime = aCompletionTime; + }, + + get undoHandler() { + return this._undoHandler; + }, + + set undoHandler(val) { + this._undoHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityEvent", "nsIActivity"]), +}; + +function ActivityWarning() { + Activity.call(this); + this.bindingName = "activity-warning-item"; + this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; +} + +ActivityWarning.prototype = { + __proto__: Activity.prototype, + + recoveryTipText: "", + _time: 0, + _recoveryHandler: null, + + init(aWarningText, aInitiator, aRecoveryTipText) { + this.displayText = aWarningText; + this.initiator = aInitiator; + this.recoveryTipText = aRecoveryTipText; + this._time = Date.now(); + }, + + get recoveryHandler() { + return this._recoveryHandler; + }, + + set recoveryHandler(val) { + this._recoveryHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + get time() { + return this._time; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityWarning", "nsIActivity"]), +}; diff --git a/comm/mail/components/activity/ActivityManager.jsm b/comm/mail/components/activity/ActivityManager.jsm new file mode 100644 index 0000000000..c808cb3354 --- /dev/null +++ b/comm/mail/components/activity/ActivityManager.jsm @@ -0,0 +1,157 @@ +/* -*- 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/. */ + +var EXPORTED_SYMBOLS = ["ActivityManager"]; + +function ActivityManager() {} + +ActivityManager.prototype = { + log: console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }), + _listeners: [], + _processCount: 0, + _db: null, + _idCounter: 1, + _activities: new Map(), + + get processCount() { + let count = 0; + for (let value of this._activities.values()) { + if (value instanceof Ci.nsIActivityProcess) { + count++; + } + } + + return count; + }, + + getProcessesByContext(aContextType, aContextObj) { + let list = []; + for (let activity of this._activities.values()) { + if ( + activity instanceof Ci.nsIActivityProcess && + activity.contextType == aContextType && + activity.contextObj == aContextObj + ) { + list.push(activity); + } + } + return list; + }, + + get db() { + return null; + }, + + get nextId() { + return this._idCounter++; + }, + + addActivity(aActivity) { + try { + this.log.info("adding Activity"); + // get the next valid id for this activity + let id = this.nextId; + aActivity.id = id; + + // add activity into the activities table + this._activities.set(id, aActivity); + // notify all the listeners + for (let value of this._listeners) { + try { + value.onAddedActivity(id, aActivity); + } catch (e) { + this.log.error("Exception calling onAddedActivity" + e); + } + } + return id; + } catch (e) { + // for some reason exceptions don't end up on the console if we don't + // explicitly log them. + this.log.error("Exception: " + e); + throw e; + } + }, + + removeActivity(aID) { + let activity = this.getActivity(aID); + if (!activity) { + return; // Nothing to remove. + } + + // make sure that the activity is not in-progress state + if ( + activity instanceof Ci.nsIActivityProcess && + activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS + ) { + throw Components.Exception(`Activity in progress`, Cr.NS_ERROR_FAILURE); + } + + // remove the activity + this._activities.delete(aID); + + // notify all the listeners + for (let value of this._listeners) { + try { + value.onRemovedActivity(aID); + } catch (e) { + // ignore the exception + } + } + }, + + cleanUp() { + // Get the list of aIDs. + this.log.info("cleanUp\n"); + for (let [id, activity] of this._activities) { + if (activity instanceof Ci.nsIActivityProcess) { + // Note: The .state property will return undefined if you aren't in + // this if-instanceof block. + let state = activity.state; + if ( + state != Ci.nsIActivityProcess.STATE_INPROGRESS && + state != Ci.nsIActivityProcess.STATE_PAUSED && + state != Ci.nsIActivityProcess.STATE_WAITINGFORINPUT && + state != Ci.nsIActivityProcess.STATE_WAITINGFORRETRY + ) { + this.removeActivity(id); + } + } else { + this.removeActivity(id); + } + } + }, + + getActivity(aID) { + return this._activities.get(aID); + }, + + containsActivity(aID) { + return this._activities.has(aID); + }, + + getActivities() { + return [...this._activities.values()]; + }, + + addListener(aListener) { + this.log.info("addListener\n"); + this._listeners.push(aListener); + }, + + removeListener(aListener) { + this.log.info("removeListener\n"); + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] == aListener) { + this._listeners.splice(i, 1); + } + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityManager"]), +}; diff --git a/comm/mail/components/activity/ActivityManagerUI.jsm b/comm/mail/components/activity/ActivityManagerUI.jsm new file mode 100644 index 0000000000..b36b9b5a72 --- /dev/null +++ b/comm/mail/components/activity/ActivityManagerUI.jsm @@ -0,0 +1,47 @@ +/* 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 EXPORTED_SYMBOLS = ["ActivityManagerUI"]; + +const ACTIVITY_MANAGER_URL = "chrome://messenger/content/activity.xhtml"; + +function ActivityManagerUI() {} + +ActivityManagerUI.prototype = { + show(aWindowContext, aID) { + // First we see if it is already visible + let window = this.recentWindow; + if (window) { + window.focus(); + return; + } + + let parent = null; + try { + if (aWindowContext) { + parent = aWindowContext.docShell.domWindow; + } + } catch (e) { + /* it's OK to not have a parent window */ + } + + Services.ww.openWindow( + parent, + ACTIVITY_MANAGER_URL, + "ActivityManager", + "chrome,dialog=no,resizable", + {} + ); + }, + + get visible() { + return null != this.recentWindow; + }, + + get recentWindow() { + return Services.wm.getMostRecentWindow("Activity:Manager"); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityManagerUI"]), +}; diff --git a/comm/mail/components/activity/components.conf b/comm/mail/components/activity/components.conf new file mode 100644 index 0000000000..429fd62bb8 --- /dev/null +++ b/comm/mail/components/activity/components.conf @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{B2C036A3-F7CE-401C-95EE-9C21505167FD}', + 'contract_ids': ['@mozilla.org/activity-process;1'], + 'jsm': 'resource:///modules/Activity.jsm', + 'constructor': 'ActivityProcess', + }, + { + 'cid': '{87AAEB20-89D9-4B95-9542-3BF72405CAB2}', + 'contract_ids': ['@mozilla.org/activity-event;1'], + 'jsm': 'resource:///modules/Activity.jsm', + 'constructor': 'ActivityEvent', + }, + { + 'cid': '{968BAC9E-798B-4952-B384-86B21B8CC71E}', + 'contract_ids': ['@mozilla.org/activity-warning;1'], + 'jsm': 'resource:///modules/Activity.jsm', + 'constructor': 'ActivityWarning', + }, + { + 'cid': '{8aa5972e-19cb-41cc-9696-645f8a8d1a06}', + 'contract_ids': ['@mozilla.org/activity-manager;1'], + 'jsm': 'resource:///modules/ActivityManager.jsm', + 'constructor': 'ActivityManager', + }, + { + 'cid': '{5fa5974e-09cb-40cc-9696-643f8a8d9a06}', + 'contract_ids': ['@mozilla.org/activity-manager-ui;1'], + 'jsm': 'resource:///modules/ActivityManagerUI.jsm', + 'constructor': 'ActivityManagerUI', + }, +] 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> diff --git a/comm/mail/components/activity/jar.mn b/comm/mail/components/activity/jar.mn new file mode 100644 index 0000000000..babeeac23d --- /dev/null +++ b/comm/mail/components/activity/jar.mn @@ -0,0 +1,8 @@ +# 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/. + +messenger.jar: + content/messenger/activity.js (content/activity.js) + content/messenger/activity-widgets.js (content/activity-widgets.js) +* content/messenger/activity.xhtml (content/activity.xhtml) diff --git a/comm/mail/components/activity/modules/activityModules.jsm b/comm/mail/components/activity/modules/activityModules.jsm new file mode 100644 index 0000000000..945f7473c2 --- /dev/null +++ b/comm/mail/components/activity/modules/activityModules.jsm @@ -0,0 +1,33 @@ +/* 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/. */ + +// This module is designed to be a central place to initialise activity related +// modules. + +const EXPORTED_SYMBOLS = []; + +const { sendLaterModule } = ChromeUtils.import( + "resource:///modules/activity/sendLater.jsm" +); +sendLaterModule.init(); +const { moveCopyModule } = ChromeUtils.import( + "resource:///modules/activity/moveCopy.jsm" +); +moveCopyModule.init(); +const { glodaIndexerActivity } = ChromeUtils.import( + "resource:///modules/activity/glodaIndexer.jsm" +); +glodaIndexerActivity.init(); +const { autosyncModule } = ChromeUtils.import( + "resource:///modules/activity/autosync.jsm" +); +autosyncModule.init(); +const { alertHook } = ChromeUtils.import( + "resource:///modules/activity/alertHook.jsm" +); +alertHook.init(); +const { pop3DownloadModule } = ChromeUtils.import( + "resource:///modules/activity/pop3Download.jsm" +); +pop3DownloadModule.init(); diff --git a/comm/mail/components/activity/modules/alertHook.jsm b/comm/mail/components/activity/modules/alertHook.jsm new file mode 100644 index 0000000000..b3083aef0a --- /dev/null +++ b/comm/mail/components/activity/modules/alertHook.jsm @@ -0,0 +1,101 @@ +/* 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 EXPORTED_SYMBOLS = ["alertHook"]; + +var nsActWarning = Components.Constructor( + "@mozilla.org/activity-warning;1", + "nsIActivityWarning", + "init" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// This module provides a link between the send later service and the activity +// manager. +var alertHook = { + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get alertService() { + delete this.alertService; + return (this.alertService = Cc["@mozilla.org/alerts-service;1"].getService( + Ci.nsIAlertsService + )); + }, + + get brandShortName() { + delete this.brandShortName; + return (this.brandShortName = Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandShortName")); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIMsgUserFeedbackListener"]), + + onAlert(aMessage, aUrl) { + // Create a new warning. + let warning = new nsActWarning(aMessage, this.activityMgr, ""); + + if (aUrl && aUrl.server && aUrl.server.prettyName) { + warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + warning.contextType = "incomingServer"; + warning.contextDisplayText = aUrl.server.prettyName; + warning.contextObj = aUrl.server; + } else { + warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; + } + + this.activityMgr.addActivity(warning); + + // If we have a message window in the url, then show a warning prompt, + // just like the modal code used to. Otherwise, don't. + try { + if (!aUrl || !aUrl.msgWindow) { + return true; + } + } catch (ex) { + // nsIMsgMailNewsUrl.msgWindow will throw on a null pointer, so that's + // what we're handling here. + if ( + ex instanceof Ci.nsIException && + ex.result == Cr.NS_ERROR_INVALID_POINTER + ) { + return true; + } + throw ex; + } + + try { + let alert = Cc["@mozilla.org/alert-notification;1"].createInstance( + Ci.nsIAlertNotification + ); + alert.init( + "", // name + "chrome://branding/content/icon48.png", + this.brandShortName, + aMessage + ); + this.alertService.showAlert(alert); + } catch (ex) { + // XXX On Linux, if libnotify isn't supported, showAlert + // can throw an error, so fall-back to the old method of modal dialogs. + return false; + } + + return true; + }, + + init() { + // We shouldn't need to remove the listener as we're not being held by + // anyone except by the send later instance. + MailServices.mailSession.addUserFeedbackListener(this); + }, +}; diff --git a/comm/mail/components/activity/modules/autosync.jsm b/comm/mail/components/activity/modules/autosync.jsm new file mode 100644 index 0000000000..c2483c4b53 --- /dev/null +++ b/comm/mail/components/activity/modules/autosync.jsm @@ -0,0 +1,433 @@ +/* -*- 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 EXPORTED_SYMBOLS = ["autosyncModule"]; + +var nsActProcess = Components.Constructor( + "@mozilla.org/activity-process;1", + "nsIActivityProcess", + "init" +); +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +/** + * This code aims to mediate between the auto-sync code and the activity mgr. + * + * Not every auto-sync activity is directly mapped to a process or event. + * To prevent a possible event overflow, Auto-Sync monitor generates one + * sync'd event per account when after all its _pending_ folders are sync'd, + * rather than generating one event per folder sync. + */ + +var autosyncModule = { + _inQFolderList: [], + _running: false, + _syncInfoPerFolder: new Map(), + _syncInfoPerServer: new Map(), + _lastMessage: new Map(), + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get autoSyncManager() { + delete this.autoSyncManager; + return (this.autoSyncManager = Cc[ + "@mozilla.org/imap/autosyncmgr;1" + ].getService(Ci.nsIAutoSyncManager)); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + createSyncMailProcess(folder) { + try { + // create an activity process for this folder + let msg = this.bundle.formatStringFromName("autosyncProcessDisplayText", [ + folder.prettyName, + ]); + let process = new nsActProcess(msg, this.autoSyncManager); + // we want to use default auto-sync icon + process.iconClass = "syncMail"; + process.addSubject(folder); + // group processes under folder's imap account + process.contextType = "account"; + process.contextDisplayText = this.bundle.formatStringFromName( + "autosyncContextDisplayText", + [folder.server.prettyName] + ); + + process.contextObj = folder.server; + + return process; + } catch (e) { + this.log.error("createSyncMailProcess: " + e); + throw e; + } + }, + + createSyncMailEvent(syncItem) { + try { + // extract the relevant parts + let process = syncItem.activity; + let folder = syncItem.syncFolder; + + // create an activity event + + let msg = this.bundle.formatStringFromName("autosyncEventDisplayText", [ + folder.server.prettyName, + ]); + + let statusMsg; + let numOfMessages = this._syncInfoPerServer.get( + folder.server + ).totalDownloads; + if (numOfMessages) { + statusMsg = this.bundle.formatStringFromName( + "autosyncEventStatusText", + [numOfMessages] + ); + } else { + statusMsg = this.getString("autosyncEventStatusTextNoMsgs"); + } + + let event = new nsActEvent( + msg, + this.autoSyncManager, + statusMsg, + this._syncInfoPerServer.get(folder.server).startTime, + Date.now() + ); // completion time + + // since auto-sync events do not have undo option by nature, + // setting these values are informational only. + event.contextType = process.contextType; + event.contextDisplayText = this.bundle.formatStringFromName( + "autosyncContextDisplayText", + [folder.server.prettyName] + ); + event.contextObj = process.contextObj; + event.iconClass = "syncMail"; + + // transfer all subjects. + // same as above, not mandatory + let subjects = process.getSubjects(); + for (let subject of subjects) { + event.addSubject(subject); + } + + return event; + } catch (e) { + this.log.error("createSyncMailEvent: " + e); + throw e; + } + }, + + onStateChanged(running) { + try { + this._running = running; + this.log.info( + "OnStatusChanged: " + (running ? "running" : "sleeping") + "\n" + ); + } catch (e) { + this.log.error("onStateChanged: " + e); + throw e; + } + }, + + onFolderAddedIntoQ(queue, folder) { + try { + if ( + folder instanceof Ci.nsIMsgFolder && + queue == Ci.nsIAutoSyncMgrListener.PriorityQueue + ) { + this._inQFolderList.push(folder); + this.log.info( + "Auto_Sync OnFolderAddedIntoQ [" + + this._inQFolderList.length + + "] " + + folder.prettyName + + " of " + + folder.server.prettyName + ); + // create an activity process for this folder + let process = this.createSyncMailProcess(folder); + + // create a sync object to keep track of the process of this folder + let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder); + let syncItem = { + syncFolder: folder, + activity: process, + percentComplete: 0, + totalDownloaded: 0, + pendingMsgCount: imapFolder.autoSyncStateObj.pendingMessageCount, + }; + + // if this is the first folder of this server in the queue, then set the sync start time + // for activity event + if (!this._syncInfoPerServer.has(folder.server)) { + this._syncInfoPerServer.set(folder.server, { + startTime: Date.now(), + totalDownloads: 0, + }); + } + + // associate the sync object with the folder in question + // use folder.URI as key + this._syncInfoPerFolder.set(folder.URI, syncItem); + } + } catch (e) { + this.log.error("onFolderAddedIntoQ: " + e); + throw e; + } + }, + onFolderRemovedFromQ(queue, folder) { + try { + if ( + folder instanceof Ci.nsIMsgFolder && + queue == Ci.nsIAutoSyncMgrListener.PriorityQueue + ) { + let i = this._inQFolderList.indexOf(folder); + if (i > -1) { + this._inQFolderList.splice(i, 1); + } + + this.log.info( + "OnFolderRemovedFromQ [" + + this._inQFolderList.length + + "] " + + folder.prettyName + + " of " + + folder.server.prettyName + + "\n" + ); + + let syncItem = this._syncInfoPerFolder.get(folder.URI); + let process = syncItem.activity; + let canceled = false; + if (process instanceof Ci.nsIActivityProcess) { + canceled = process.state == Ci.nsIActivityProcess.STATE_CANCELED; + process.state = Ci.nsIActivityProcess.STATE_COMPLETED; + + try { + this.activityMgr.removeActivity(process.id); + } catch (e) { + // It is OK to end up here; If the folder is queued and the + // message get manually downloaded by the user, we might get + // a folder removed notification even before a download + // started for this folder. This behavior stems from the fact + // that we add activities into the activity manager in + // onDownloadStarted notification rather than onFolderAddedIntoQ. + // This is an expected side effect. + // Log a warning, but do not throw an error. + this.log.warn("onFolderRemovedFromQ: " + e); + } + + // remove the folder/syncItem association from the table + this._syncInfoPerFolder.delete(folder.URI); + } + + // if this is the last folder of this server in the queue + // create a sync event and clean the sync start time + let found = false; + for (let value of this._syncInfoPerFolder.values()) { + if (value.syncFolder.server == folder.server) { + found = true; + break; + } + } + this.log.info( + "Auto_Sync OnFolderRemovedFromQ Last folder of the server: " + !found + ); + if (!found) { + // create an sync event for the completed process if it's not canceled + if (!canceled) { + let key = folder.server.prettyName; + if ( + this._lastMessage.has(key) && + this.activityMgr.containsActivity(this._lastMessage.get(key)) + ) { + this.activityMgr.removeActivity(this._lastMessage.get(key)); + } + this._lastMessage.set( + key, + this.activityMgr.addActivity(this.createSyncMailEvent(syncItem)) + ); + } + this._syncInfoPerServer.delete(folder.server); + } + } + } catch (e) { + this.log.error("onFolderRemovedFromQ: " + e); + throw e; + } + }, + onDownloadStarted(folder, numOfMessages, totalPending) { + try { + if (folder instanceof Ci.nsIMsgFolder) { + this.log.info( + "OnDownloadStarted (" + + numOfMessages + + "/" + + totalPending + + "): " + + folder.prettyName + + " of " + + folder.server.prettyName + + "\n" + ); + + let syncItem = this._syncInfoPerFolder.get(folder.URI); + let process = syncItem.activity; + + // Update the totalPending number. if new messages have been discovered in the folder + // after we added the folder into the q, totalPending might be greater than what we have + // initially set + if (totalPending > syncItem.pendingMsgCount) { + syncItem.pendingMsgCount = totalPending; + } + + if (process instanceof Ci.nsIActivityProcess) { + // if the process has not beed added to activity manager already, add now + if (!this.activityMgr.containsActivity(process.id)) { + this.log.info( + "Auto_Sync OnDownloadStarted: No process, adding a new process" + ); + this.activityMgr.addActivity(process); + } + + syncItem.totalDownloaded += numOfMessages; + + process.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + let percent = + (syncItem.totalDownloaded / syncItem.pendingMsgCount) * 100; + if (percent > syncItem.percentComplete) { + syncItem.percentComplete = percent; + } + + let msg = this.bundle.formatStringFromName( + "autosyncProcessProgress2", + [ + syncItem.totalDownloaded, + syncItem.pendingMsgCount, + folder.prettyName, + folder.server.prettyName, + ] + ); + + process.setProgress( + msg, + syncItem.totalDownloaded, + syncItem.pendingMsgCount + ); + + let serverInfo = this._syncInfoPerServer.get( + syncItem.syncFolder.server + ); + serverInfo.totalDownloads += numOfMessages; + this._syncInfoPerServer.set(syncItem.syncFolder.server, serverInfo); + } + } + } catch (e) { + this.log.error("onDownloadStarted: " + e); + throw e; + } + }, + + onDownloadCompleted(folder) { + try { + if (folder instanceof Ci.nsIMsgFolder) { + this.log.info( + "OnDownloadCompleted: " + + folder.prettyName + + " of " + + folder.server.prettyName + ); + + let process = this._syncInfoPerFolder.get(folder.URI).activity; + if (process instanceof Ci.nsIActivityProcess && !this._running) { + this.log.info( + "OnDownloadCompleted: Auto-Sync Manager is paused, pausing the process" + ); + process.state = Ci.nsIActivityProcess.STATE_PAUSED; + } + } + } catch (e) { + this.log.error("onDownloadCompleted: " + e); + throw e; + } + }, + + onDownloadError(folder) { + if (folder instanceof Ci.nsIMsgFolder) { + this.log.error( + "OnDownloadError: " + + folder.prettyName + + " of " + + folder.server.prettyName + + "\n" + ); + } + }, + + onDiscoveryQProcessed(folder, numOfHdrsProcessed, leftToProcess) { + this.log.info( + "onDiscoveryQProcessed: Processed " + + numOfHdrsProcessed + + "/" + + (leftToProcess + numOfHdrsProcessed) + + " of " + + folder.prettyName + + "\n" + ); + }, + + onAutoSyncInitiated(folder) { + this.log.info( + "onAutoSyncInitiated: " + + folder.prettyName + + " of " + + folder.server.prettyName + + " has been updated.\n" + ); + }, + + init() { + // XXX when do we need to remove ourselves? + this.log.info("initing"); + Cc["@mozilla.org/imap/autosyncmgr;1"] + .getService(Ci.nsIAutoSyncManager) + .addListener(this); + }, +}; diff --git a/comm/mail/components/activity/modules/glodaIndexer.jsm b/comm/mail/components/activity/modules/glodaIndexer.jsm new file mode 100644 index 0000000000..5307d5cefa --- /dev/null +++ b/comm/mail/components/activity/modules/glodaIndexer.jsm @@ -0,0 +1,251 @@ +/* -*- 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 EXPORTED_SYMBOLS = ["glodaIndexerActivity"]; + +var nsActProcess = Components.Constructor( + "@mozilla.org/activity-process;1", + "nsIActivityProcess", + "init" +); +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PluralForm: "resource://gre/modules/PluralForm.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + Gloda: "resource:///modules/gloda/GlodaPublic.jsm", + GlodaConstants: "resource:///modules/gloda/GlodaConstants.jsm", + GlodaIndexer: "resource:///modules/gloda/GlodaIndexer.jsm", +}); + +/** + * Gloda message indexer feedback. + */ +var glodaIndexerActivity = { + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + init() { + // Register a listener with the Gloda indexer that receives notifications + // about Gloda indexing status. We wrap the listener in this function so we + // can set |this| to the GlodaIndexerActivity object inside the listener. + function listenerWrapper(...aArgs) { + glodaIndexerActivity.listener(...aArgs); + } + lazy.GlodaIndexer.addListener(listenerWrapper); + }, + + /** + * Information about the current job. An object with these properties: + * + * folder {String} + * the name of the folder being processed by the job + * jobNumber {Number} + * the index of the job in the list of jobs + * process {nsIActivityProcess} + * the activity process corresponding to the current job + * startTime {Date} + * the time at which we were first notified about the job + * totalItemNum {Number} + * the total number of messages being indexed in the job + * jobType {String} + * The IndexinbJob jobType (ex: "folder", "folderCompact") + */ + currentJob: null, + + listener(aStatus, aFolder, aJobNumber, aItemNumber, aTotalItemNum, aJobType) { + this.log.debug("Gloda Indexer Folder/Status: " + aFolder + "/" + aStatus); + this.log.debug("Gloda Indexer Job: " + aJobNumber); + this.log.debug("Gloda Indexer Item: " + aItemNumber + "/" + aTotalItemNum); + + if (aStatus == lazy.GlodaConstants.kIndexerIdle) { + if (this.currentJob) { + this.onJobCompleted(); + } + } else { + // If the job numbers have changed, the indexer has finished the job + // we were previously tracking, so convert the corresponding process + // into an event and start a new process to track the new job. + if (this.currentJob && aJobNumber != this.currentJob.jobNumber) { + this.onJobCompleted(); + } + + // If we aren't tracking a job, either this is the first time we've been + // called or the last job we were tracking was completed. Either way, + // start tracking the new job. + if (!this.currentJob) { + this.onJobBegun(aFolder, aJobNumber, aTotalItemNum, aJobType); + } + + // If there is only one item, don't bother creating a progress item. + if (aTotalItemNum != 1) { + this.onJobProgress(aFolder, aItemNumber, aTotalItemNum); + } + } + }, + + onJobBegun(aFolder, aJobNumber, aTotalItemNum, aJobType) { + let displayText = aFolder + ? this.getString("indexingFolder").replace("#1", aFolder) + : this.getString("indexing"); + let process = new nsActProcess(displayText, lazy.Gloda); + + process.iconClass = "indexMail"; + process.contextType = "account"; + process.contextObj = aFolder; + process.addSubject(aFolder); + + this.currentJob = { + folder: aFolder, + jobNumber: aJobNumber, + process, + startTime: new Date(), + totalItemNum: aTotalItemNum, + jobType: aJobType, + }; + + this.activityMgr.addActivity(process); + }, + + onJobProgress(aFolder, aItemNumber, aTotalItemNum) { + this.currentJob.process.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + // The total number of items being processed in the job can change, as can + // the folder being processed, since we sometimes get notified about a job + // before it has determined these things, so we update them here. + this.currentJob.folder = aFolder; + this.currentJob.totalItemNum = aTotalItemNum; + + let statusText; + if (aTotalItemNum == null) { + statusText = aFolder + ? this.getString("indexingFolderStatusVague").replace("#1", aFolder) + : this.getString("indexingStatusVague"); + } else { + let percentComplete = + aTotalItemNum == 0 + ? 100 + : parseInt((aItemNumber / aTotalItemNum) * 100); + // Note: we must replace the folder name placeholder last; otherwise, + // if the name happens to contain another one of the placeholders, we'll + // hork the name when replacing it. + statusText = this.getString( + aFolder ? "indexingFolderStatusExact" : "indexingStatusExact" + ); + statusText = lazy.PluralForm.get(aTotalItemNum, statusText) + .replace("#1", aItemNumber + 1) + .replace("#2", aTotalItemNum) + .replace("#3", percentComplete) + .replace("#4", aFolder); + } + + this.currentJob.process.setProgress(statusText, aItemNumber, aTotalItemNum); + }, + + onJobCompleted() { + this.currentJob.process.state = Ci.nsIActivityProcess.STATE_COMPLETED; + + this.activityMgr.removeActivity(this.currentJob.process.id); + + // this.currentJob.totalItemNum might still be null at this point + // if we were first notified about the job before the indexer determined + // the number of messages to index and then it didn't find any to index. + let totalItemNum = this.currentJob.totalItemNum || 0; + + // We only create activity events when specific folders get indexed, + // since event-driven indexing jobs are too numerous. We also only create + // them when we ended up indexing something in the folder, since otherwise + // we'd spam the activity manager with too many "indexed 0 messages" items + // that aren't useful enough to justify their presence in the manager. + // TODO: Aggregate event-driven indexing jobs into batches significant + // enough for us to create activity events for them. + if ( + this.currentJob.jobType == "folder" && + this.currentJob.folder && + totalItemNum > 0 + ) { + // Note: we must replace the folder name placeholder last; otherwise, + // if the name happens to contain another one of the placeholders, we'll + // hork the name when replacing it. + let displayText = lazy.PluralForm.get( + totalItemNum, + this.getString("indexedFolder") + ) + .replace("#1", totalItemNum) + .replace("#2", this.currentJob.folder); + + let endTime = new Date(); + let secondsElapsed = parseInt( + (endTime - this.currentJob.startTime) / 1000 + ); + + let statusText = lazy.PluralForm.get( + secondsElapsed, + this.getString("indexedFolderStatus") + ).replace("#1", secondsElapsed); + + let event = new nsActEvent( + displayText, + lazy.Gloda, + statusText, + this.currentJob.startTime, + endTime + ); + event.contextType = this.currentJob.contextType; + event.contextObj = this.currentJob.contextObj; + event.iconClass = "indexMail"; + + // Transfer subjects. + let subjects = this.currentJob.process.getSubjects(); + for (let subject of subjects) { + event.addSubject(subject); + } + + this.activityMgr.addActivity(event); + } + + this.currentJob = null; + }, +}; diff --git a/comm/mail/components/activity/modules/moveCopy.jsm b/comm/mail/components/activity/modules/moveCopy.jsm new file mode 100644 index 0000000000..de3e51d85b --- /dev/null +++ b/comm/mail/components/activity/modules/moveCopy.jsm @@ -0,0 +1,396 @@ +/* -*- 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 EXPORTED_SYMBOLS = ["moveCopyModule"]; + +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +// This module provides a link between the move/copy code and the activity +// manager. +var moveCopyModule = { + lastMessage: {}, + lastFolder: {}, + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + msgAdded(aMsg) {}, + + msgsDeleted(aMsgList) { + this.log.info("in msgsDeleted"); + + if (aMsgList.length <= 0) { + return; + } + + let displayCount = aMsgList.length; + // get the folder of the deleted messages + let folder = aMsgList[0].folder; + + let activities = this.activityMgr.getActivities(); + if ( + activities.length > 0 && + activities[activities.length - 1].id == this.lastMessage.id && + this.lastMessage.type == "deleteMail" && + this.lastMessage.folder == folder.prettyName + ) { + displayCount += this.lastMessage.count; + this.activityMgr.removeActivity(this.lastMessage.id); + } + + this.lastMessage = {}; + let displayText = PluralForm.get( + displayCount, + this.getString("deletedMessages2") + ); + displayText = displayText.replace("#1", displayCount); + this.lastMessage.count = displayCount; + displayText = displayText.replace("#2", folder.prettyName); + this.lastMessage.folder = folder.prettyName; + + let statusText = folder.server.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + folder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "deleteMail"; + this.lastMessage.type = event.iconClass; + + for (let msgHdr of aMsgList) { + event.addSubject(msgHdr.messageId); + } + + this.lastMessage.id = this.activityMgr.addActivity(event); + }, + + msgsMoveCopyCompleted(aMove, aSrcMsgList, aDestFolder) { + try { + this.log.info("in msgsMoveCopyCompleted"); + + let count = aSrcMsgList.length; + if (count <= 0) { + return; + } + + // get the folder of the moved/copied messages + let folder = aSrcMsgList[0].folder; + this.log.info("got folder"); + + let displayCount = count; + + let activities = this.activityMgr.getActivities(); + if ( + activities.length > 0 && + activities[activities.length - 1].id == this.lastMessage.id && + this.lastMessage.type == (aMove ? "moveMail" : "copyMail") && + this.lastMessage.sourceFolder == folder.prettyName && + this.lastMessage.destFolder == aDestFolder.prettyName + ) { + displayCount += this.lastMessage.count; + this.activityMgr.removeActivity(this.lastMessage.id); + } + + let statusText = ""; + if (folder.server != aDestFolder.server) { + statusText = this.getString("fromServerToServer"); + statusText = statusText.replace("#1", folder.server.prettyName); + statusText = statusText.replace("#2", aDestFolder.server.prettyName); + } else { + statusText = folder.server.prettyName; + } + + this.lastMessage = {}; + let displayText; + if (aMove) { + displayText = PluralForm.get( + displayCount, + this.getString("movedMessages") + ); + } else { + displayText = PluralForm.get( + displayCount, + this.getString("copiedMessages") + ); + } + + displayText = displayText.replace("#1", displayCount); + this.lastMessage.count = displayCount; + displayText = displayText.replace("#2", folder.prettyName); + this.lastMessage.sourceFolder = folder.prettyName; + displayText = displayText.replace("#3", aDestFolder.prettyName); + this.lastMessage.destFolder = aDestFolder.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + folder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + event.iconClass = aMove ? "moveMail" : "copyMail"; + this.lastMessage.type = event.iconClass; + + for (let msgHdr of aSrcMsgList) { + event.addSubject(msgHdr.messageId); + } + this.lastMessage.id = this.activityMgr.addActivity(event); + } catch (e) { + this.log.error("Exception: " + e); + } + }, + + folderAdded(aFolder) {}, + + folderDeleted(aFolder) { + // When a new account is created we get this notification with an empty named + // folder that can't return its server. Ignore it. + // TODO: find out what it is. + let server = aFolder.server; + // If the account has been removed, we're going to ignore this notification. + if ( + !MailServices.accounts.findServer( + server.username, + server.hostName, + server.type + ) + ) { + return; + } + + let displayText; + let statusText = server.prettyName; + + // Display a different message depending on whether we emptied the trash + // or actually deleted a folder + if (aFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, false)) { + displayText = this.getString("emptiedTrash"); + } else { + displayText = this.getString("deletedFolder").replace( + "#1", + aFolder.prettyName + ); + } + + // create an activity event + let event = new nsActEvent( + displayText, + server, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.addSubject(aFolder); + event.iconClass = "deleteMail"; + + // When we rename, we get a delete event as well as a rename, so store + // the last folder we deleted + this.lastFolder = {}; + this.lastFolder.URI = aFolder.URI; + this.lastFolder.event = this.activityMgr.addActivity(event); + }, + + folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) { + this.log.info("in folderMoveCopyCompleted, aMove = " + aMove); + + let displayText; + if (aMove) { + displayText = this.getString("movedFolder"); + } else { + displayText = this.getString("copiedFolder"); + } + + displayText = displayText.replace("#1", aSrcFolder.prettyName); + displayText = displayText.replace("#2", aDestFolder.prettyName); + + let statusText = ""; + if (aSrcFolder.server != aDestFolder.server) { + statusText = this.getString("fromServerToServer"); + statusText = statusText.replace("#1", aSrcFolder.server.prettyName); + statusText = statusText.replace("#2", aDestFolder.server.prettyName); + } else { + statusText = aSrcFolder.server.prettyName; + } + // create an activity event + let event = new nsActEvent( + displayText, + aSrcFolder.server, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.addSubject(aSrcFolder); + event.addSubject(aDestFolder); + event.iconClass = aMove ? "moveMail" : "copyMail"; + + this.activityMgr.addActivity(event); + }, + + folderRenamed(aOrigFolder, aNewFolder) { + this.log.info( + "in folderRenamed, aOrigFolder = " + + aOrigFolder.prettyName + + ", aNewFolder = " + + aNewFolder.prettyName + ); + + let displayText; + let statusText = aNewFolder.server.prettyName; + + // Display a different message depending on whether we moved the folder + // to the trash or actually renamed the folder. + if (aNewFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, true)) { + displayText = this.getString("movedFolderToTrash"); + displayText = displayText.replace("#1", aOrigFolder.prettyName); + } else { + displayText = this.getString("renamedFolder"); + displayText = displayText.replace("#1", aOrigFolder.prettyName); + displayText = displayText.replace("#2", aNewFolder.prettyName); + } + + // When renaming a folder, a delete event is always fired first + if (this.lastFolder.URI == aOrigFolder.URI) { + this.activityMgr.removeActivity(this.lastFolder.event); + } + + // create an activity event + let event = new nsActEvent( + displayText, + aOrigFolder.server, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.addSubject(aOrigFolder); + event.addSubject(aNewFolder); + + this.activityMgr.addActivity(event); + }, + + msgUnincorporatedMoved(srcFolder, msgHdr) { + try { + this.log.info("in msgUnincorporatedMoved"); + + // get the folder of the moved/copied messages + let destFolder = msgHdr.folder; + this.log.info("got folder"); + + let displayCount = 1; + + let activities = this.activityMgr.getActivities(); + if ( + activities.length > 0 && + activities[activities.length - 1].id == this.lastMessage.id && + this.lastMessage.type == "moveMail" && + this.lastMessage.sourceFolder == srcFolder.prettyName && + this.lastMessage.destFolder == destFolder.prettyName + ) { + displayCount += this.lastMessage.count; + this.activityMgr.removeActivity(this.lastMessage.id); + } + + let statusText = ""; + if (srcFolder.server != destFolder.server) { + statusText = this.getString("fromServerToServer"); + statusText = statusText.replace("#1", srcFolder.server.prettyName); + statusText = statusText.replace("#2", destFolder.server.prettyName); + } else { + statusText = srcFolder.server.prettyName; + } + + this.lastMessage = {}; + let displayText; + displayText = PluralForm.get( + displayCount, + this.getString("movedMessages") + ); + + displayText = displayText.replace("#1", displayCount); + this.lastMessage.count = displayCount; + displayText = displayText.replace("#2", srcFolder.prettyName); + this.lastMessage.sourceFolder = srcFolder.prettyName; + displayText = displayText.replace("#3", destFolder.prettyName); + this.lastMessage.destFolder = destFolder.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + srcFolder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "moveMail"; + this.lastMessage.type = event.iconClass; + event.addSubject(msgHdr.messageId); + this.lastMessage.id = this.activityMgr.addActivity(event); + } catch (e) { + this.log.error("Exception: " + e); + } + }, + + init() { + // XXX when do we need to remove ourselves? + MailServices.mfn.addListener( + this, + MailServices.mfn.msgsDeleted | + MailServices.mfn.msgsMoveCopyCompleted | + MailServices.mfn.folderDeleted | + MailServices.mfn.folderMoveCopyCompleted | + MailServices.mfn.folderRenamed | + MailServices.mfn.msgUnincorporatedMoved + ); + }, +}; diff --git a/comm/mail/components/activity/modules/pop3Download.jsm b/comm/mail/components/activity/modules/pop3Download.jsm new file mode 100644 index 0000000000..f203b33212 --- /dev/null +++ b/comm/mail/components/activity/modules/pop3Download.jsm @@ -0,0 +1,154 @@ +/* -*- 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 EXPORTED_SYMBOLS = ["pop3DownloadModule"]; + +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +// This module provides a link between the pop3 service code and the activity +// manager. +var pop3DownloadModule = { + // hash table of most recent download items per folder + _mostRecentActivityForFolder: new Map(), + // hash table of prev download items per folder, so we can + // coalesce consecutive no new message events. + _prevActivityForFolder: new Map(), + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + onDownloadStarted(aFolder) { + this.log.info("in onDownloadStarted"); + + let displayText = this.bundle.formatStringFromName( + "pop3EventStartDisplayText2", + [ + aFolder.server.prettyName, // account name + aFolder.prettyName, + ] + ); // folder name + // remember the prev activity for this folder, if any. + this._prevActivityForFolder.set( + aFolder.URI, + this._mostRecentActivityForFolder.get(aFolder.URI) + ); + let statusText = aFolder.server.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + aFolder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "syncMail"; + + let downloadItem = {}; + downloadItem.eventID = this.activityMgr.addActivity(event); + this._mostRecentActivityForFolder.set(aFolder.URI, downloadItem); + }, + + onDownloadProgress(aFolder, aNumMsgsDownloaded, aTotalMsgs) { + this.log.info("in onDownloadProgress"); + }, + + onDownloadCompleted(aFolder, aNumMsgsDownloaded) { + this.log.info("in onDownloadCompleted"); + + // Remove activity if there was any. + // It can happen that download never started (e.g. couldn't connect to server), + // with onDownloadStarted, but we still get a onDownloadCompleted event + // when the connection is given up. + let recentActivity = this._mostRecentActivityForFolder.get(aFolder.URI); + if (recentActivity) { + this.activityMgr.removeActivity(recentActivity.eventID); + } + + let displayText; + if (aNumMsgsDownloaded > 0) { + displayText = PluralForm.get( + aNumMsgsDownloaded, + this.getString("pop3EventStatusText") + ); + displayText = displayText.replace("#1", aNumMsgsDownloaded); + } else { + displayText = this.getString("pop3EventStatusTextNoMsgs"); + } + + let statusText = aFolder.server.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + aFolder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "syncMail"; + + let downloadItem = { numMsgsDownloaded: aNumMsgsDownloaded }; + this._mostRecentActivityForFolder.set(aFolder.URI, downloadItem); + downloadItem.eventID = this.activityMgr.addActivity(event); + if (!aNumMsgsDownloaded) { + // If we didn't download any messages this time, and the prev event + // for this folder also didn't download any messages, remove the + // prev event from the activity manager. + let prevItem = this._prevActivityForFolder.get(aFolder.URI); + if (prevItem != undefined && !prevItem.numMsgsDownloaded) { + if (this.activityMgr.containsActivity(prevItem.eventID)) { + this.activityMgr.removeActivity(prevItem.eventID); + } + } + } + }, + init() { + // XXX when do we need to remove ourselves? + MailServices.pop3.addListener(this); + }, +}; diff --git a/comm/mail/components/activity/modules/sendLater.jsm b/comm/mail/components/activity/modules/sendLater.jsm new file mode 100644 index 0000000000..37027d96f1 --- /dev/null +++ b/comm/mail/components/activity/modules/sendLater.jsm @@ -0,0 +1,298 @@ +/* -*- 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 EXPORTED_SYMBOLS = ["sendLaterModule"]; + +var nsActProcess = Components.Constructor( + "@mozilla.org/activity-process;1", + "nsIActivityProcess", + "init" +); +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); +var nsActWarning = Components.Constructor( + "@mozilla.org/activity-warning;1", + "nsIActivityWarning", + "init" +); + +/** + * This really, really, sucks. Due to mailnews widespread use of + * nsIMsgStatusFeedback we're bound to the UI to get any sensible feedback of + * mail sending operations. The current send later code can't hook into the + * progress listener easily to get the state of messages being sent, so we'll + * just have to do it here. + */ +var sendMsgProgressListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgStatusFeedback", + "nsISupportsWeakReference", + ]), + + showStatusString(aStatusText) { + sendLaterModule.onMsgStatus(aStatusText); + }, + + startMeteors() {}, + + stopMeteors() {}, + + showProgress(aPercentage) { + sendLaterModule.onMessageSendProgress(0, 0, aPercentage, 0); + }, +}; + +// This module provides a link between the send later service and the activity +// manager. +var sendLaterModule = { + _sendProcess: null, + _copyProcess: null, + _identity: null, + _subject: null, + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIMsgSendLaterListener"]), + + _displayTextForHeader(aLocaleStringBase, aSubject) { + return aSubject + ? this.bundle.formatStringFromName(aLocaleStringBase + "WithSubject", [ + aSubject, + ]) + : this.bundle.GetStringFromName(aLocaleStringBase); + }, + + _newProcess(aLocaleStringBase, aAddSubject) { + let process = new nsActProcess( + this._displayTextForHeader( + aLocaleStringBase, + aAddSubject ? this._subject : "" + ), + this.activityMgr + ); + + process.iconClass = "sendMail"; + process.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + process.contextObj = this; + process.contextType = "SendLater"; + process.contextDisplayText = + this.bundle.GetStringFromName("sendingMessages"); + + return process; + }, + + // Use this to group an activity by the identity if we have one. + _applyIdentityGrouping(aActivity) { + if (this._identity) { + aActivity.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + aActivity.contextType = this._identity.key; + aActivity.contextObj = this._identity; + let contextDisplayText = this._identity.identityName; + if (!contextDisplayText) { + contextDisplayText = this._identity.email; + } + + aActivity.contextDisplayText = contextDisplayText; + } else { + aActivity.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; + } + }, + + // Replaces the process with an event that reflects a completed process. + _replaceProcessWithEvent(aProcess) { + this.activityMgr.removeActivity(aProcess.id); + + let event = new nsActEvent( + this._displayTextForHeader("sentMessage", this._subject), + this.activityMgr, + "", + aProcess.startTime, + new Date() + ); + + event.iconClass = "sendMail"; + this._applyIdentityGrouping(event); + + this.activityMgr.addActivity(event); + }, + + // Replaces the process with a warning that reflects the failed process. + _replaceProcessWithWarning( + aProcess, + aCopyOrSend, + aStatus, + aMsg, + aMessageHeader + ) { + this.activityMgr.removeActivity(aProcess.id); + + let warning = new nsActWarning( + this._displayTextForHeader("failedTo" + aCopyOrSend, this._subject), + this.activityMgr, + "" + ); + + warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; + this._applyIdentityGrouping(warning); + + this.activityMgr.addActivity(warning); + }, + + onStartSending(aTotalMessageCount) { + if (!aTotalMessageCount) { + this.log.error("onStartSending called with zero messages\n"); + } + }, + + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) { + // We want to use the identity and subject later, so store them for now. + this._identity = aIdentity; + if (aMessageHeader) { + this._subject = aMessageHeader.mime2DecodedSubject; + } + + // Create the process to display the send activity. + let process = this._newProcess("sendingMessage", true); + this._sendProcess = process; + this.activityMgr.addActivity(process); + + // Now the one for the copy process. + process = this._newProcess("copyMessage", false); + this._copyProcess = process; + this.activityMgr.addActivity(process); + }, + + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) { + if (aMessageSendPercent < 100) { + // Ensure we are in progress... + if (this._sendProcess.state != Ci.nsIActivityProcess.STATE_INPROGRESS) { + this._sendProcess.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + } + + // ... and update the progress. + this._sendProcess.setProgress( + this._sendProcess.lastStatusText, + aMessageSendPercent, + 100 + ); + } else if (aMessageSendPercent == 100) { + if (aMessageCopyPercent == 0) { + // Set send state to completed + if (this._sendProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED) { + this._sendProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + } + this._replaceProcessWithEvent(this._sendProcess); + + // Set copy state to in progress. + if (this._copyProcess.state != Ci.nsIActivityProcess.STATE_INPROGRESS) { + this._copyProcess.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + } + + // We don't know the progress of the copy, so just set to 0, and we'll + // display an undetermined progress meter. + this._copyProcess.setProgress(this._copyProcess.lastStatusText, 0, 0); + } else if (aMessageCopyPercent >= 100) { + // We need to set this to completed otherwise activity manager + // complains. + if (this._copyProcess) { + this._copyProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + this.activityMgr.removeActivity(this._copyProcess.id); + this._copyProcess = null; + } + + this._sendProcess = null; + } + } + }, + + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + if ( + this._sendProcess && + this._sendProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED + ) { + this._sendProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + this._replaceProcessWithWarning( + this._sendProcess, + "SendMessage", + aStatus, + aMsg, + aMessageHeader + ); + this._sendProcess = null; + + if ( + this._copyProcess && + this._copyProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED + ) { + this._copyProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + this.activityMgr.removeActivity(this._copyProcess.id); + this._copyProcess = null; + } + } + }, + + onMsgStatus(aStatusText) { + this._sendProcess.setProgress( + aStatusText, + this._sendProcess.workUnitComplete, + this._sendProcess.totalWorkUnits + ); + }, + + onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {}, + + init() { + // We should need to remove the listener as we're not being held by anyone + // except by the send later instance. + let sendLaterService = Cc[ + "@mozilla.org/messengercompose/sendlater;1" + ].getService(Ci.nsIMsgSendLater); + + sendLaterService.addListener(this); + + // Also add the nsIMsgStatusFeedback object. + let statusFeedback = Cc[ + "@mozilla.org/messenger/statusfeedback;1" + ].createInstance(Ci.nsIMsgStatusFeedback); + + statusFeedback.setWrappedStatusFeedback(sendMsgProgressListener); + + sendLaterService.statusFeedback = statusFeedback; + }, +}; diff --git a/comm/mail/components/activity/moz.build b/comm/mail/components/activity/moz.build new file mode 100644 index 0000000000..efceaacf9f --- /dev/null +++ b/comm/mail/components/activity/moz.build @@ -0,0 +1,34 @@ +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIActivity.idl", + "nsIActivityManager.idl", + "nsIActivityManagerUI.idl", +] + +XPIDL_MODULE = "activity" + +EXTRA_JS_MODULES.activity += [ + "modules/activityModules.jsm", + "modules/alertHook.jsm", + "modules/autosync.jsm", + "modules/glodaIndexer.jsm", + "modules/moveCopy.jsm", + "modules/pop3Download.jsm", + "modules/sendLater.jsm", +] + +EXTRA_JS_MODULES += [ + "Activity.jsm", + "ActivityManager.jsm", + "ActivityManagerUI.jsm", +] + +JAR_MANIFESTS += ["jar.mn"] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mail/components/activity/nsIActivity.idl b/comm/mail/components/activity/nsIActivity.idl new file mode 100644 index 0000000000..d80e69088f --- /dev/null +++ b/comm/mail/components/activity/nsIActivity.idl @@ -0,0 +1,492 @@ +/* -*- Mode: C++; 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/. */ + +#include "nsISupports.idl" +#include "nsISupportsPrimitives.idl" + +interface nsIActivityListener; +interface nsIActivity; +interface nsIActivityProcess; +interface nsIActivityEvent; +interface nsIActivityWarning; +interface nsIVariant; +interface nsISupportsPRTime; + +/** + * See https://wiki.mozilla.org/Thunderbird:Activity_Manager/Developer for UML + * diagram and sample codes. + */ + +/** + * Background: + * Activity handlers define the behavioral capabilities of the activities. They + * are used by the Activity Manager to change the execution flow of the activity + * based on the user interaction. They are not mandatory, but when set, causes + * behavioral changes on the binding representing the activity, such as showing + * a cancel button, etc. The following handlers are currently supported; + */ + +/** + * The handler to invoke when the recover button is pressed. Used by a Warning + * to recover from the situation causing the warning. For instance, recovery + * action for a "Over Quota Limit" warning, would be to cleanup some disk space, + * and this operation can be implemented and set by the activity developer in + * form of nsIActivityRecoveryHandler component. + */ +[scriptable, uuid(30E0A76F-880A-4093-8F3C-AF2239977A3D)] +interface nsIActivityRecoveryHandler : nsISupports { + nsresult recover(in nsIActivityWarning aActivity); +}; + +/** + * The handler to invoke when the cancel button is pressed. Used by a Process to + * cancel the operation. + */ +[scriptable, uuid(35ee2461-70db-4b3a-90d0-7a68c856e911)] +interface nsIActivityCancelHandler : nsISupports { + nsresult cancel(in nsIActivityProcess aActivity); +}; + +/** + * The handler to invoke when the pause button is pressed. Used by a Process to + * pause/resume the operation. + */ +[scriptable, uuid(9eee22bf-5378-460e-83a7-781cdcc9050b)] +interface nsIActivityPauseHandler : nsISupports { + nsresult pause(in nsIActivityProcess aActivity); + nsresult resume(in nsIActivityProcess aActivity); +}; + +/** + * The handler to invoke when the retry button is pressed. Used by a Process to + * retry the operation in case of failure. + */ +[scriptable, uuid(8ec42517-951f-4bc0-aba5-fde7258b1705)] +interface nsIActivityRetryHandler : nsISupports { + nsresult retry(in nsIActivityProcess aActivity); +}; + +/** + * The handler to invoke when the undo button is pressed. Used by a Event to + * undo the operation generated the event. + */ +[scriptable, uuid(b8632ac7-9d8b-4341-a349-ef000e8c89ac)] +interface nsIActivityUndoHandler : nsISupports { + nsresult undo(in nsIActivityEvent aActivity); +}; + +/** + * Base interface of all activity interfaces. It is abstract in a sense that + * there is no component in the activity management system that solely + * implements this interface. + */ +[scriptable, uuid(6CD33E65-B2D8-4634-9B6D-B80BF1273E99)] +interface nsIActivity : nsISupports { + + /** + * Shows the activity as a standalone item. + */ + const short GROUPING_STYLE_STANDALONE = 1; + + /** + * Groups activity by its context. + */ + const short GROUPING_STYLE_BYCONTEXT = 2; + + /** + * Internal ID given by the activity manager when + * added into the activity list. Not readonly so that + * the activity manager can write to them, but not to be written to + * by anyone else. + */ + attribute unsigned long id; + + // Following attributes change the UI characteristics of the activity + + /** + * A brief description of the activity, to be shown by the + * associated binding (XBL) in the Activity Manager window. + */ + readonly attribute AString displayText; + + /** + * Changes the default icon associated with the activity. Core activity + * icons are declared in |mail/themes/<themename>/mail/activity/activity.css| + * files. + * + * Extension developers can add and assign their own icons by setting + * this attribute. + */ + attribute AString iconClass; + + /** + * Textual id of the XBL binding that will be used to represent the + * activity in the Activity Manager window. + * + * This attribute allows to associate default activity components + * with custom XBL bindings. See |activity.xml| file for default + * activity XBL bindings, and |activity.css| file for default binding + * associations. + */ + attribute AString bindingName; + + /** + * Defines the grouping style of the activity when being shown in the + * activity manager window: + * GROUPING_STYLE_STANDALONE or GROUPING_STYLE_BYCONTEXT + */ + attribute short groupingStyle; + + /** + * A text value to associate a facet type with the activity. If empty, + * the activity will be shown in the 'Misc' section. + */ + attribute AString facet; + + // UI related attributes end. + + /** + * Gets the initiator of the activity. An initiator represents an object + * that generates and controls the activity. For example, Copy Service can be + * the initiator of the copy, and move activities. Similarly Gloda can be the + * initiator of indexing activity, etc. + * + * This attribute is used mostly by handler components to change the execution + * flow of the activity such as canceling, pausing etc. Since not used by the + * Activity Manager, it is not mandatory to set it. + * + * An initiator can be any JS Object or an XPCOM component that provides an + * nsIVariant interface. + */ + readonly attribute nsIVariant initiator; + + /** + * Adds an object to the activity's internal subject list. Subject list + * provides argument(s) to activity handlers to complete their operation. + * For example, nsIActivityUndoHandler needs the source and destination + * folders to undo a move operation. + * + * Since subjects are not used by the Activity Manager, it is up to the + * activity developer to provide these objects. + * + * A subject can be any JS object or XPCOM component that supports nsIVariant + * interface. + */ + void addSubject(in nsIVariant aSubject); + + /** + * Retrieves all subjects associated with this activity. + * + * @return The list of subject objects associated by the activity. + */ + Array<nsIVariant> getSubjects(); + + /* + * Background: + * A context is a generic concept that is used to group the processes and + * warnings having similar properties such as same imap server, same smtp + * server etc. + * A context is uniquely identified by its "type" and "object" attributes. + * Each activity that has the same context type and object are considered + * belong to the same logical group, context. + * + * There are 4 predefined context types known by the Activity Manager: + * Account, Smtp, Calendar, and Addressbook. The most common context type + * for activities is the "Account Context" and when combined with an account + * server instance, it allows to group different activities happening on the + * the same account server. + */ + + /** + * Sets and gets the context object of the activity. A context object can be + * any JS object or XPCOM component that supports nsIVariant interface. + */ + attribute nsIVariant contextObj; + + /** + * Sets and gets the context type of the activity. If this is set, then + * the contextDisplayText should also be set. + */ + attribute AString contextType; + + /** + * Return the displayText to be used for the context + **/ + attribute AString contextDisplayText; + + /** + * Adds a listener. See nsIActivityListener below. + */ + void addListener(in nsIActivityListener aListener); + + /** + * Removes the given listener. See nsIActivityListener below. + */ + void removeListener(in nsIActivityListener aListener); +}; + + +/** + * A Process represents an on-going activity. + */ +[scriptable, uuid(9DC7CA67-828D-4AFD-A5C6-3ECE091A98B8)] +interface nsIActivityProcess : nsIActivity { + + /** + * Default state for uninitialized process activity + * object. + */ + const short STATE_NOTSTARTED = -1; + + /** + * Activity is currently in progress. + */ + const short STATE_INPROGRESS = 0; + + /** + * Activity is completed. + */ + const short STATE_COMPLETED = 1; + + /** + * Activity was canceled by the user. + * (same as completed) + */ + const short STATE_CANCELED = 2; + + /** + * Activity was paused by the user. + */ + const short STATE_PAUSED = 3; + + /** + * Activity waits for the user input's to retry. + * (i.e. login password) + */ + const short STATE_WAITINGFORINPUT = 4; + + /** + * Activity is ready for an automatic or manual retry. + */ + const short STATE_WAITINGFORRETRY = 5; + + /** + * The state of the activity. + * See above for possible values. + * @exception NS_ERROR_ILLEGAL_VALUE if the state isn't one of the states + * defined above. + */ + attribute short state; + + /** + * The percentage of activity completed. + * If the max value is unknown it'll be -1 here. + */ + readonly attribute long percentComplete; + + /** + * A brief text about the process' status. + */ + readonly attribute AString lastStatusText; + + /** + * The amount of work units completed so far. + */ + readonly attribute unsigned long workUnitComplete; + + /** + * Total amount of work units. + */ + readonly attribute unsigned long totalWorkUnits; + + /** + * The starting time of the process. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long startTime; + + /** + * The handler to invoke when the cancel button is pressed. If present + * (non-null), the activity can be canceled and a cancel button will be + * displayed to the user. If null, it cannot be canceled and no button will + * be displayed. + */ + attribute nsIActivityCancelHandler cancelHandler; + + /** + * The handler to invoke when the pause button is pressed. If present + * (non-null), the activity can be pauseable and a pause button will be + * displayed to the user. If null, it cannot be paused and no button will + * be displayed. + */ + attribute nsIActivityPauseHandler pauseHandler; + + /** + * The handler to invoke when the retry button is pressed. If present + * (non-null), the activity can be retryable and a retry button will be + * displayed to the user. If null, it cannot be retried and no button will + * be displayed. + */ + attribute nsIActivityRetryHandler retryHandler; + + /** + * Updates the activity progress info. + * + * @param aStatusText A localized text describing the current status of the + * process + * @param aWorkUnitComplete The amount of work units completed. Not used by + * Activity Manager or default binding for any + * purpose. + * @param aTotalWorkUnits Total amount of work units. Not used by + * Activity Manager or default binding for any + * purpose. If set to zero, this indicates that the + * number of work units is unknown, and the percentage + * attribute will be set to -1. + */ + void setProgress(in AString aStatusText, + in unsigned long aWorkUnitComplete, + in unsigned long aTotalWorkUnits); + + /** + * Component initialization method. + * + * @param aDisplayText A localized text to be shown on the Activity Manager + * window + * @param aInitiator The initiator of the process + */ + void init(in AString aDisplayText, in nsIVariant aInitiator); +}; + +/** + * Historical actions performed by the user, by extensions or by the system. + */ +[scriptable, uuid(5B1B0D03-2820-4E37-8BF8-102AFDE4FC45)] +interface nsIActivityEvent : nsIActivity { + + /** + * Any localized textual information related to this event. + * It is shown at the bottom of the displayText area. + */ + readonly attribute AString statusText; + + /** + * The starting time of the event. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long startTime; + + /** + * The completion time of the event in microseconds. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long completionTime; + + /** + * The handler to invoke when the undo button is pressed. If present + * (non-null), the activity can be undoable and an undo button will be + * displayed to the user. If null, it cannot be undone and no button will + * be displayed. + */ + attribute nsIActivityUndoHandler undoHandler; + + /** + * Component initialization method. + * + * @param aDisplayText Any localized text describing the event and its context + * @param aInitiator The initiator of the event + * @param aStatusText Any localized additional information about the event + * @param aStartTime The starting time of the event + * @param aCompletionTime The completion time of the event + */ + void init(in AString aDisplayText, in nsIVariant aInitiator, + in AString aStatusText, in long long aStartTime, + in long long aCompletionTime); +}; + +[scriptable, uuid(8265833e-c604-4585-a43c-a76bd8ed3a8c)] +interface nsIActivityWarning : nsIActivity { + + /** + * Any localized textual information related to this warning. + */ + readonly attribute AString warningText; + + /** + * The time of the warning. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long time; + + /** + * Recovery tip of the warning, localized. + */ + readonly attribute AString recoveryTipText; + + /** + * The handler to invoke when the recover button is pressed. If present + * (non-null), the activity can be recoverable and a recover button will be + * displayed to the user. If null, it cannot be recovered and no button will + * be displayed. + */ + attribute nsIActivityRecoveryHandler recoveryHandler; + + /** + * Component initialization method. + * + * @param aWarningText The localized text that will be shown on the display + * area + * @param aInitiator The initiator of the warning + * @param aRecoveryTip A localized textual information to guide the user in + * order to recover from the warning situation. + */ + void init(in AString aWarningText, in nsIVariant aInitiator, + in AString aRecoveryTip); +}; + +[scriptable, uuid(bd11519f-b297-4b34-a793-1861dc90d5e9)] +interface nsIActivityListener : nsISupports { + /** + * Triggered after activity state is changed. + */ + void onStateChanged(in nsIActivity aActivity, in short aOldState); + + /** + * Triggered after the progress of the process activity is changed. + */ + void onProgressChanged(in nsIActivity aActivity, + in AString aStatusText, + in unsigned long aWorkUnitsCompleted, + in unsigned long aTotalWorkUnits); + + /** + * Triggered after one of the activity handler is set. + * + * This is mostly used to update the UI of the activity when + * one of the handler is set to null after the operation is completed. + * For example after the activity is undone, to make the undo button + * invisible. + */ + void onHandlerChanged(in nsIActivity aActivity); +}; diff --git a/comm/mail/components/activity/nsIActivityManager.idl b/comm/mail/components/activity/nsIActivityManager.idl new file mode 100644 index 0000000000..860b4e1e2b --- /dev/null +++ b/comm/mail/components/activity/nsIActivityManager.idl @@ -0,0 +1,135 @@ +/* -*- Mode: C++; 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/. */ + +#include "nsISupports.idl" + +interface mozIStorageConnection; +interface nsIActivity; +interface nsIActivityProcess; +interface nsIVariant; + +/** + * See https://wiki.mozilla.org/Thunderbird:Activity_Manager/Developer for UML + * diagram and sample codes. + */ + +/** + * An interface to get notified by the major Activity Manager events. + * Mostly used by UI glue code in activity.js. + */ +[scriptable, uuid(14cfad1c-3401-4c44-ab04-4a11b6662663)] +interface nsIActivityMgrListener : nsISupports { + /** + * Called _after_ activity manager adds an activity into + * the managed list. + */ + void onAddedActivity(in unsigned long aID, in nsIActivity aActivity); + + /** + * Called _after_ activity manager removes an activity from + * the managed list. + */ + void onRemovedActivity(in unsigned long aID); +}; + +/** + * Activity Manager is a simple component that understands how do display a + * combination of user activity and history. The activity manager works in + * conjunction with the 'Interactive Status Bar' to give the user the right + * level of notifications concerning what Thunderbird is doing on it's own and + * how Thunderbird has handled user requests. + * + * There are 3 different classifications of activity items which can be + * displayed in the Activity Manager Window: + * o Process: Processes are transient in the display. They are not written to + * disk as they are always acting on some data that already exists + * locally or remotely. If a process has finished and needs to keep + * some state for the user (like last sync time) it can convert + * itself into an event. + * o Event: Historical actions performed by the user and created by a process + * for the Activity Manager Window. Events can show up in the + * 'Interactive Status Bar' and be displayed to users as they are + * created. + * o Warning: Alerts sent by Thunderbird or servers (i.e. imap server) that need + * attention by the user. For example a Quota Alert from the imap + * server can be represented as a Warning to the user. They are not + * written to disk. + */ +[scriptable, uuid(9BFCC031-50E1-4D30-A35F-23509ABCB8D1)] +interface nsIActivityManager : nsISupports { + + /** + * Adds the given activity into the managed activities list. + * + * @param aActivity The activity that will be added. + * + * @return Unique activity identifier. + */ + unsigned long addActivity(in nsIActivity aActivity); + + /** + * Removes the activity with the given id if it's not currently + * in-progress. + * + * @param aID The unique ID of the activity. + * + * @throws NS_ERROR_FAILURE if the activity is in-progress. + */ + void removeActivity(in unsigned long aID); + + /** + * Retrieves an activity managed by the activity manager. This can be one that + * is in progress, or one that has completed in the past and is stored in the + * persistent store. + * + * @param aID The unique ID of the activity. + * + * @return The activity with the specified ID, or null if not found. + */ + nsIActivity getActivity(in unsigned long aID); + + /** + * Tests whether the activity in question in the activity list or not. + */ + boolean containsActivity(in unsigned long aID); + + /** + * Retrieves all activities managed by the activity manager. This can be one + * that is in progress (process), one that is represented as a warning, or one + * that has completed (event) in the past and is stored in the persistent + * store. + * + * @return A read-only list of activities managed by the activity manager. + */ + Array<nsIActivity> getActivities(); + + /** + * Retrieves processes with given context type and object. + * + * @return A read-only list of processes matching to given criteria. + */ + Array<nsIActivityProcess> getProcessesByContext(in AString aContextType, + in nsIVariant aContextObject); + + /** + * Call to remove all activities apart from those that are in progress. + */ + void cleanUp(); + + /** + * The number of processes in the activity list. + */ + readonly attribute long processCount; + + /** + * Adds a listener. + */ + void addListener(in nsIActivityMgrListener aListener); + + /** + * Removes the given listener. + */ + void removeListener(in nsIActivityMgrListener aListener); +}; diff --git a/comm/mail/components/activity/nsIActivityManagerUI.idl b/comm/mail/components/activity/nsIActivityManagerUI.idl new file mode 100644 index 0000000000..07a2a30394 --- /dev/null +++ b/comm/mail/components/activity/nsIActivityManagerUI.idl @@ -0,0 +1,50 @@ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIInterfaceRequestor; + +[scriptable, uuid(ae7853b0-2e1f-4dc1-89cd-f8bbfb745d4d)] +interface nsIActivityManagerUI : nsISupports { + /** + * The reason that should be passed when the user requests to show the + * activity manager's UI. + */ + const short REASON_USER_INTERACTED = 0; + + /** + * The reason that should be passed to the show method when we are displaying + * the UI because a new activity is being added to it. + */ + const short REASON_NEW_ACTIVITY = 1; + + /** + * Shows the Activity Manager's UI to the user. + * + * @param [optional] aWindowContext + * The parent window context to show the UI. + * @param [optional] aID + * The id of the activity to be preselected upon opening. + * @param [optional] aReason + * The reason to show the activity manager's UI. This defaults to + * REASON_USER_INTERACTED, and should be one of the previously listed + * constants. + */ + void show([optional] in nsIInterfaceRequestor aWindowContext, + [optional] in unsigned long aID, + [optional] in short aReason); + + /** + * Indicates if the UI is visible or not. + */ + readonly attribute boolean visible; + + /** + * Brings attention to the UI if it is already visible + * + * @throws NS_ERROR_UNEXPECTED if the UI is not visible. + */ + void getAttention(); +}; |