summaryrefslogtreecommitdiffstats
path: root/comm/mail/modules/DBViewWrapper.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/modules/DBViewWrapper.jsm')
-rw-r--r--comm/mail/modules/DBViewWrapper.jsm2250
1 files changed, 2250 insertions, 0 deletions
diff --git a/comm/mail/modules/DBViewWrapper.jsm b/comm/mail/modules/DBViewWrapper.jsm
new file mode 100644
index 0000000000..e88ac3dc05
--- /dev/null
+++ b/comm/mail/modules/DBViewWrapper.jsm
@@ -0,0 +1,2250 @@
+/* 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 = ["DBViewWrapper", "IDBViewWrapperListener"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { MailViewConstants, MailViewManager } = ChromeUtils.import(
+ "resource:///modules/MailViewManager.jsm"
+);
+const { SearchSpec } = ChromeUtils.import("resource:///modules/SearchSpec.jsm");
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+var MSG_VIEW_FLAG_DUMMY = 0x20000000;
+
+var nsMsgViewIndex_None = 0xffffffff;
+
+/**
+ * Helper singleton for DBViewWrapper that tells instances when something
+ * interesting is happening to the folder(s) they care about. The rationale
+ * for this is to:
+ * - reduce listener overhead (although arguably the events we listen to are
+ * fairly rare)
+ * - make testing / verification easier by centralizing and exposing listeners.
+ *
+ */
+var FolderNotificationHelper = {
+ /**
+ * Maps URIs of pending folder loads to the DBViewWrapper instances that
+ * are waiting on the loads. The value is a list of instances in case
+ * a quick-clicking user is able to do something unexpected.
+ */
+ _pendingFolderUriToViewWrapperLists: {},
+
+ /**
+ * Map URIs of folders to view wrappers interested in hearing about their
+ * deletion.
+ */
+ _interestedWrappers: {},
+
+ /**
+ * Array of wrappers that are interested in all folders, used for
+ * search results wrappers.
+ */
+ _curiousWrappers: [],
+
+ /**
+ * Initialize our listeners. We currently don't bother cleaning these up
+ * because we are a singleton and if anyone imports us, they probably want
+ * us for as long as their application so shall live.
+ */
+ _init() {
+ // register with the session for our folded loaded notifications
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.event | Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // register with the notification service for deleted folder notifications
+ MailServices.mfn.addListener(
+ this,
+ Ci.nsIMsgFolderNotificationService.folderDeleted |
+ // we need to track renames because we key off of URIs. frick.
+ Ci.nsIMsgFolderNotificationService.folderRenamed |
+ Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted
+ );
+ },
+
+ /**
+ * Call updateFolder, and assuming all goes well, request that the provided
+ * FolderDisplayWidget be notified when the folder is loaded. This method
+ * performs the updateFolder call for you so there is less chance of leaking.
+ * In the event the updateFolder call fails, we will propagate the exception.
+ */
+ updateFolderAndNotifyOnLoad(aFolder, aFolderDisplay, aMsgWindow) {
+ // set up our datastructure first in case of wacky event sequences
+ let folderURI = aFolder.URI;
+ let wrappers = this._pendingFolderUriToViewWrapperLists[folderURI];
+ if (wrappers == null) {
+ wrappers = this._pendingFolderUriToViewWrapperLists[folderURI] = [];
+ }
+ wrappers.push(aFolderDisplay);
+ try {
+ aFolder.updateFolder(aMsgWindow);
+ } catch (ex) {
+ // uh-oh, that didn't work. tear down the data structure...
+ wrappers.pop();
+ if (wrappers.length == 0) {
+ delete this._pendingFolderUriToViewWrapperLists[folderURI];
+ }
+ throw ex;
+ }
+ },
+
+ /**
+ * Request notification of every little thing these folders do.
+ *
+ * @param aFolders The folders.
+ * @param aNotherFolder A folder that may or may not be in aFolders.
+ * @param aViewWrapper The view wrapper that is up to no good.
+ */
+ stalkFolders(aFolders, aNotherFolder, aViewWrapper) {
+ let folders = aFolders ? aFolders.concat() : [];
+ if (aNotherFolder && !folders.includes(aNotherFolder)) {
+ folders.push(aNotherFolder);
+ }
+ for (let folder of folders) {
+ let wrappers = this._interestedWrappers[folder.URI];
+ if (wrappers == null) {
+ wrappers = this._interestedWrappers[folder.URI] = [];
+ }
+ wrappers.push(aViewWrapper);
+ }
+ },
+
+ /**
+ * Request notification of every little thing every folder does.
+ *
+ * @param aViewWrapper - the viewWrapper interested in every notification.
+ * This will be a search results view of some sort.
+ */
+ noteCuriosity(aViewWrapper) {
+ this._curiousWrappers.push(aViewWrapper);
+ },
+
+ /**
+ * Removal helper for use by removeNotifications.
+ *
+ * @param aTable The table mapping URIs to list of view wrappers.
+ * @param aFolder The folder we care about.
+ * @param aViewWrapper The view wrapper of interest.
+ */
+ _removeWrapperFromListener(aTable, aFolder, aViewWrapper) {
+ let wrappers = aTable[aFolder.URI];
+ if (wrappers) {
+ let index = wrappers.indexOf(aViewWrapper);
+ if (index >= 0) {
+ wrappers.splice(index, 1);
+ }
+ if (wrappers.length == 0) {
+ delete aTable[aFolder.URI];
+ }
+ }
+ },
+ /**
+ * Remove notification requests on the provided folders by the given view
+ * wrapper.
+ */
+ removeNotifications(aFolders, aViewWrapper) {
+ if (!aFolders) {
+ this._curiousWrappers.splice(
+ this._curiousWrappers.indexOf(aViewWrapper),
+ 1
+ );
+ return;
+ }
+ for (let folder of aFolders) {
+ this._removeWrapperFromListener(
+ this._interestedWrappers,
+ folder,
+ aViewWrapper
+ );
+ this._removeWrapperFromListener(
+ this._pendingFolderUriToViewWrapperLists,
+ folder,
+ aViewWrapper
+ );
+ }
+ },
+
+ /**
+ * @returns true if there are any listeners still registered. This is intended
+ * to support debugging code. If you are not debug code, you are a bad
+ * person/code.
+ */
+ haveListeners() {
+ if (Object.keys(this._pendingFolderUriToViewWrapperLists).length > 0) {
+ return true;
+ }
+ if (Object.keys(this._interestedWrappers).length > 0) {
+ return true;
+ }
+ return this._curiousWrappers.length != 0;
+ },
+
+ /* ***** Notifications ***** */
+ _notifyHelper(aFolder, aHandlerName) {
+ let wrappers = this._interestedWrappers[aFolder.URI];
+ if (wrappers) {
+ // clone the list to avoid confusing mutation by listeners
+ for (let wrapper of wrappers.concat()) {
+ wrapper[aHandlerName](aFolder);
+ }
+ }
+ for (let wrapper of this._curiousWrappers) {
+ wrapper[aHandlerName](aFolder);
+ }
+ },
+
+ onFolderEvent(aFolder, aEvent) {
+ if (aEvent == "FolderLoaded") {
+ let folderURI = aFolder.URI;
+ let widgets = this._pendingFolderUriToViewWrapperLists[folderURI];
+ if (widgets) {
+ for (let widget of widgets) {
+ // we are friends, this is an explicit relationship.
+ // (we don't use a generic callback mechanism because the 'this' stuff
+ // gets ugly and no one else should be hooking in at this level.)
+ try {
+ widget._folderLoaded(aFolder);
+ } catch (ex) {
+ dump(
+ "``` EXCEPTION DURING NOTIFY: " +
+ ex.fileName +
+ ":" +
+ ex.lineNumber +
+ ": " +
+ ex +
+ "\n"
+ );
+ if (ex.stack) {
+ dump("STACK: " + ex.stack + "\n");
+ }
+ console.error(ex);
+ }
+ }
+ delete this._pendingFolderUriToViewWrapperLists[folderURI];
+ }
+ } else if (aEvent == "AboutToCompact") {
+ this._notifyHelper(aFolder, "_aboutToCompactFolder");
+ } else if (aEvent == "CompactCompleted") {
+ this._notifyHelper(aFolder, "_compactedFolder");
+ } else if (aEvent == "DeleteOrMoveMsgCompleted") {
+ this._notifyHelper(aFolder, "_deleteCompleted");
+ } else if (aEvent == "DeleteOrMoveMsgFailed") {
+ this._notifyHelper(aFolder, "_deleteFailed");
+ } else if (aEvent == "RenameCompleted") {
+ this._notifyHelper(aFolder, "_renameCompleted");
+ }
+ },
+
+ onFolderIntPropertyChanged(aFolder, aProperty, aOldValue, aNewValue) {
+ if (aProperty == "TotalMessages" || aProperty == "TotalUnreadMessages") {
+ this._notifyHelper(aFolder, "_messageCountsChanged");
+ }
+ },
+
+ _folderMoveHelper(aOldFolder, aNewFolder) {
+ let oldURI = aOldFolder.URI;
+ let newURI = aNewFolder.URI;
+ // fix up our listener tables.
+ if (oldURI in this._pendingFolderUriToViewWrapperLists) {
+ this._pendingFolderUriToViewWrapperLists[newURI] =
+ this._pendingFolderUriToViewWrapperLists[oldURI];
+ delete this._pendingFolderUriToViewWrapperLists[oldURI];
+ }
+ if (oldURI in this._interestedWrappers) {
+ this._interestedWrappers[newURI] = this._interestedWrappers[oldURI];
+ delete this._interestedWrappers[oldURI];
+ }
+
+ let wrappers = this._interestedWrappers[newURI];
+ if (wrappers) {
+ // clone the list to avoid confusing mutation by listeners
+ for (let wrapper of wrappers.concat()) {
+ wrapper._folderMoved(aOldFolder, aNewFolder);
+ }
+ }
+ },
+
+ /**
+ * Update our URI mapping tables when renames happen.
+ */
+ folderRenamed(aOrigFolder, aNewFolder) {
+ this._folderMoveHelper(aOrigFolder, aNewFolder);
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ if (aMove) {
+ let aNewFolder = aDestFolder.getChildNamed(aSrcFolder.prettyName);
+ this._folderMoveHelper(aSrcFolder, aNewFolder);
+ }
+ },
+
+ folderDeleted(aFolder) {
+ let wrappers = this._interestedWrappers[aFolder.URI];
+ if (wrappers) {
+ // clone the list to avoid confusing mutation by listeners
+ for (let wrapper of wrappers.concat()) {
+ wrapper._folderDeleted(aFolder);
+ }
+ // if the folder is deleted, it's not going to ever do anything again
+ delete this._interestedWrappers[aFolder.URI];
+ }
+ },
+};
+FolderNotificationHelper._init();
+
+/**
+ * Defines the DBViewWrapper listener interface. This class exists exclusively
+ * for documentation purposes and should never be instantiated.
+ */
+function IDBViewWrapperListener() {}
+IDBViewWrapperListener.prototype = {
+ // uh, this is secretly exposed for debug purposes. DO NOT LOOK AT ME!
+ _FNH: FolderNotificationHelper,
+
+ /* ===== Exposure of UI Globals ===== */
+ messenger: null,
+ msgWindow: null,
+ threadPaneCommandUpdater: null,
+
+ /* ===== Guidance ===== */
+ /**
+ * Indicate whether mail view settings should be used/honored. A UI oddity
+ * is that we only have mail views be sticky if its combo box UI is visible.
+ * (Without the view combobox, it may not be obvious that the mail is
+ * filtered.)
+ */
+ get shouldUseMailViews() {
+ return false;
+ },
+
+ /**
+ * Should we defer displaying the messages in this folder until after we have
+ * talked to the server? This is for our poor man's password protection
+ * via the "mail.password_protect_local_cache" pref. We add this specific
+ * check rather than internalizing the logic in the wrapper because the
+ * password protection is a shoddy UI-only protection.
+ */
+ get shouldDeferMessageDisplayUntilAfterServerConnect() {
+ return false;
+ },
+
+ /**
+ * Should we mark all messages in a folder as read on exit?
+ * This is nominally controlled by the "mailnews.mark_message_read.SERVERTYPE"
+ * preference (on a per-server-type basis).
+ * For the record, this functionality should not remotely be in the core.
+ *
+ * @param aMsgFolder The folder we are leaving and are unsure if we should
+ * mark all its messages read. I pass the folder instead of the server
+ * type because having a crazy feature like this will inevitably lead to
+ * a more full-featured crazy feature (why not on a per-folder basis, eh?)
+ * @returns true if we should mark all the dudes as read, false if not.
+ */
+ shouldMarkMessagesReadOnLeavingFolder(aMsgFolder) {
+ return false;
+ },
+
+ /* ===== Event Notifications ===== */
+ /* === Status Changes === */
+ /**
+ * We tell you when we start and stop loading the folder. This is a good
+ * time to mess with the hour-glass cursor machinery if you are inclined to
+ * do so.
+ */
+ onFolderLoading(aIsFolderLoading) {},
+
+ /**
+ * We tell you when we start and stop searching. This is a good time to mess
+ * with progress spinners (meteors) and the like, if you are so inclined.
+ */
+ onSearching(aIsSearching) {},
+
+ /**
+ * This event is generated when a new view has been created. It is intended
+ * to be used to provide the MsgCreateDBView notification so that custom
+ * columns can add themselves to the view.
+ * The notification is not generated by the DBViewWrapper itself because this
+ * is fundamentally a UI issue. Additionally, because the MsgCreateDBView
+ * notification consumers assume gDBView whose exposure is affected by tabs,
+ * the tab logic needs to be involved.
+ */
+ onCreatedView() {},
+
+ /**
+ * This event is generated just before we close/destroy a message view.
+ *
+ * @param aFolderIsComingBack Indicates whether we are planning to create a
+ * new view to display the same folder after we destroy this view. This
+ * will be the case unless we are switching to display a new folder or
+ * closing the view wrapper entirely.
+ */
+ onDestroyingView(aFolderIsComingBack) {},
+
+ /**
+ * Generated when we are loading information about the folder from its
+ * dbFolderInfo. The dbFolderInfo object is passed in.
+ * The DBViewWrapper has already restored its state when this function is
+ * called, but has not yet created the dbView. A view update is in process,
+ * so the view settings can be changed and will take effect when the update
+ * is closed.
+ * |onDisplayingFolder| is the next expected notification following this
+ * notification.
+ */
+ onLoadingFolder(aDbFolderInfo) {},
+
+ /**
+ * Generated when the folder is being entered for display. This is the chance
+ * for the listener to affect any UI-related changes to the folder required.
+ * Currently, this just means setting the header cache size (which needs to
+ * be proportional to the number of lines in the tree view, and is thus a
+ * UI issue.)
+ * The dbView has already been created and is valid when this function is
+ * called.
+ * |onLoadingFolder| is called before this notification.
+ */
+ onDisplayingFolder() {},
+
+ /**
+ * Generated when we are leaving a folder.
+ */
+ onLeavingFolder() {},
+
+ /**
+ * Things to do once all the messages that should show up in a folder have
+ * shown up. For a real folder, this happens when the folder is entered.
+ * For a (multi-folder) virtual folder, this happens when the search
+ * completes.
+ * You may get onMessagesLoaded called with aAll false immediately after
+ * the view is opened. You will definitely get onMessagesLoaded(true)
+ * when we've finished getting the headers for the view.
+ */
+ onMessagesLoaded(aAll) {},
+
+ /**
+ * The mail view changed. The mail view widget is likely to care about this.
+ */
+ onMailViewChanged() {},
+
+ /**
+ * The active sort changed, and that is all that changed. If the sort is
+ * changing because the view is being destroyed and re-created, this event
+ * will not be generated.
+ */
+ onSortChanged() {},
+
+ /**
+ * This event is generated when messages in one of the folders backing the
+ * view have been removed by message moves / deletion. If there is a search
+ * in effect, it is possible that the removed messages were not visible in
+ * the view in the first place.
+ */
+ onMessagesRemoved() {},
+
+ /**
+ * Like onMessagesRemoved, but something went awry in the move/deletion and
+ * it failed. Although this is not a very interesting event on its own,
+ * it is useful in cases where the listener was expecting an
+ * onMessagesRemoved and might need to clean some state up.
+ */
+ onMessageRemovalFailed() {},
+
+ /**
+ * The total message count or total unread message counts changed.
+ */
+ onMessageCountsChanged() {},
+};
+
+/**
+ * Encapsulates everything related to working with our nsIMsgDBView
+ * implementations.
+ *
+ * Things we do not do and why we do not do them:
+ * - Selection. This depends on having an nsITreeSelection object and we choose
+ * to avoid entanglement with XUL/layout code. Selection accordingly must be
+ * handled a layer up in the FolderDisplayWidget.
+ */
+function DBViewWrapper(aListener) {
+ this.displayedFolder = null;
+ this.listener = aListener;
+
+ this._underlyingData = this.kUnderlyingNone;
+ this._underlyingFolders = null;
+ this._syntheticView = null;
+
+ this._viewUpdateDepth = 0;
+
+ this._mailViewIndex = MailViewConstants.kViewItemAll;
+ this._mailViewData = null;
+
+ this._specialView = null;
+
+ this._sort = [];
+ // see the _viewFlags getter and setter for info on our use of __viewFlags.
+ this.__viewFlags = null;
+
+ /**
+ * It's possible to support grouped view thread expand/collapse, and also sort
+ * by thread despite the back end (see nsMsgQuickSearchDBView::SortThreads).
+ * Also, nsMsgQuickSearchDBView does not respect the kExpandAll flag, fix that.
+ */
+ this._threadExpandAll = true;
+
+ this.dbView = null;
+ this.search = null;
+
+ this._folderLoading = false;
+ this._searching = false;
+}
+DBViewWrapper.prototype = {
+ /* = constants explaining the nature of the underlying data = */
+ /**
+ * We currently don't have any underlying data.
+ */
+ kUnderlyingNone: 0,
+ /**
+ * The underlying data source is a single folder.
+ */
+ kUnderlyingRealFolder: 1,
+ /**
+ * The underlying data source is a virtual folder that is operating over
+ * multiple underlying folders.
+ */
+ kUnderlyingMultipleFolder: 2,
+ /**
+ * Our data source is transient, most likely a gloda search that crammed the
+ * results into us. This is different from a search view.
+ */
+ kUnderlyingSynthetic: 3,
+ /**
+ * We are a search view, which translates into a search that has underlying
+ * folders, just like kUnderlyingMultipleFolder, but we have no
+ * displayedFolder. We differ from kUnderlyingSynthetic in that we are
+ * not just a bunch of message headers randomly crammed in.
+ */
+ kUnderlyingSearchView: 4,
+
+ /**
+ * @returns true if the folder being displayed is backed by a single 'real'
+ * folder. This folder can be a saved search on that folder or just
+ * an outright un-filtered display of that folder.
+ */
+ get isSingleFolder() {
+ return this._underlyingData == this.kUnderlyingRealFolder;
+ },
+
+ /**
+ * @returns true if the folder being displayed is a virtual folder backed by
+ * multiple 'real' folders or a search view. This corresponds to a
+ * cross-folder saved search.
+ */
+ get isMultiFolder() {
+ return (
+ this._underlyingData == this.kUnderlyingMultipleFolder ||
+ this._underlyingData == this.kUnderlyingSearchView
+ );
+ },
+
+ /**
+ * @returns true if the folder being displayed is not a real folder at all,
+ * but rather the result of an un-scoped search, such as a gloda search.
+ */
+ get isSynthetic() {
+ return this._underlyingData == this.kUnderlyingSynthetic;
+ },
+
+ /**
+ * @returns true if the folder being displayed is not a real folder at all,
+ * but rather the result of a search.
+ */
+ get isSearch() {
+ return this._underlyingData == this.kUnderlyingSearchView;
+ },
+
+ /**
+ * Check if the folder in question backs the currently displayed folder. For
+ * a virtual folder, this is a test of whether the virtual folder includes
+ * messages from the given folder. For a 'real' single folder, this is
+ * effectively a test against displayedFolder.
+ * If you want to see if the displayed folder is a folder, just compare
+ * against the displayedFolder attribute.
+ */
+ isUnderlyingFolder(aFolder) {
+ return this._underlyingFolders.some(
+ underlyingFolder => aFolder == underlyingFolder
+ );
+ },
+
+ /**
+ * Refresh the view by re-creating the view. You would do this to get rid of
+ * messages that no longer match the view but are kept around for view
+ * stability reasons. (In other words, in an unread-messages view, you would
+ * go insane if when you clicked on a message it immediately disappeared
+ * because it no longer matched.)
+ * This method was adding for testing purposes and does not have a (legacy) UI
+ * reason for existing. (The 'open' method is intended to behave identically
+ * to the legacy UI if you click on the currently displayed folder.)
+ */
+ refresh() {
+ this._applyViewChanges();
+ },
+
+ /**
+ * Null out the folder's database to avoid memory bloat if we don't have a
+ * reason to keep the database around. Currently, we keep all Inboxes
+ * around and null out everyone else. This is a standard stopgap measure
+ * until we have something more clever going on.
+ * In general, there is little potential downside to nulling out the message
+ * database reference when it is in use. As long as someone is holding onto
+ * a message header from the database, the database will be kept open, and
+ * therefore the database service will still have a reference to the db.
+ * When the folder goes to ask for the database again, the service will have
+ * it, and it will not need to be re-opened.
+ *
+ * Another heuristic we could theoretically use is use the mail session's
+ * isFolderOpenInWindow call, except that uses the outmoded concept that each
+ * window will have at most one folder open. So nuts to that.
+ *
+ * Note: regrettably a unit test cannot verify that we did this; msgDatabase
+ * is a getter that will always try and load the message database!
+ */
+ _releaseFolderDatabase(aFolder) {
+ if (!aFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Inbox, false)) {
+ aFolder.msgDatabase = null;
+ }
+ },
+
+ /**
+ * Clone this DBViewWrapper and its underlying nsIMsgDBView.
+ *
+ * @param aListener {IDBViewWrapperListener} The listener to use on the new view.
+ */
+ clone(aListener) {
+ let doppel = new DBViewWrapper(aListener);
+
+ // -- copy attributes
+ doppel.displayedFolder = this.displayedFolder;
+ doppel._underlyingData = this._underlyingData;
+ doppel._underlyingFolders = this._underlyingFolders
+ ? this._underlyingFolders.concat()
+ : null;
+ doppel._syntheticView = this._syntheticView;
+
+ // _viewUpdateDepth should stay at its initial value of zero
+ doppel._mailViewIndex = this._mailViewIndex;
+ doppel._mailViewData = this._mailViewData;
+
+ doppel._specialView = this._specialView;
+ // a shallow copy is all that is required for sort; we do not mutate entries
+ doppel._sort = this._sort.concat();
+
+ // -- register listeners...
+ // note: this does not get us a folder loaded notification. Our expected
+ // use case for cloning is displaying a single message already visible in
+ // the original view, which implies we don't need to hang about for folder
+ // loaded notification messages.
+ FolderNotificationHelper.stalkFolders(
+ doppel._underlyingFolders,
+ doppel.displayedFolder,
+ doppel
+ );
+
+ // -- clone the view
+ if (this.dbView) {
+ doppel.dbView = this.dbView
+ .cloneDBView(
+ aListener.messenger,
+ aListener.msgWindow,
+ aListener.threadPaneCommandUpdater
+ )
+ .QueryInterface(Ci.nsITreeView);
+ }
+ // -- clone the search
+ if (this.search) {
+ doppel.search = this.search.clone(doppel);
+ }
+
+ if (
+ doppel._underlyingData == this.kUnderlyingSearchView ||
+ doppel._underlyingData == this.kUnderlyingSynthetic
+ ) {
+ FolderNotificationHelper.noteCuriosity(doppel);
+ }
+
+ return doppel;
+ },
+
+ /**
+ * Close the current view. You would only do this if you want to clean up all
+ * the resources associated with this view wrapper. You would not do this
+ * for UI reasons like the user de-selecting the node in the tree; we should
+ * always be displaying something when used in a UI context!
+ *
+ * @param {boolean} folderIsDead - If true, tells us not to try and tidy up
+ * on our way out by virtue of the fact that the folder is dead and should
+ * not be messed with.
+ */
+ close(folderIsDead) {
+ if (this.displayedFolder != null) {
+ // onLeavingFolder does all the application-level stuff related to leaving
+ // the folder (marking as read, etc.) We only do this when the folder
+ // is not dead (for obvious reasons).
+ if (!folderIsDead) {
+ // onLeavingFolder must be called before we potentially null out its
+ // msgDatabase, which we will do in the upcoming underlyingFolders loop
+ this.onLeavingFolder(); // application logic
+ this.listener.onLeavingFolder(); // display logic
+ }
+ // (potentially) zero out the display folder if we are dealing with a
+ // virtual folder and so the next loop won't take care of it.
+ if (this.isVirtual) {
+ FolderNotificationHelper.removeNotifications(
+ [this.displayedFolder],
+ this
+ );
+ this._releaseFolderDatabase(this.displayedFolder);
+ }
+
+ this.folderLoading = false;
+ this.displayedFolder = null;
+ }
+
+ FolderNotificationHelper.removeNotifications(this._underlyingFolders, this);
+ if (this.isSearch || this.isSynthetic) {
+ // Opposite of FolderNotificationHelper.noteCuriosity(this)
+ FolderNotificationHelper.removeNotifications(null, this);
+ }
+
+ if (this._underlyingFolders) {
+ // (potentially) zero out the underlying msgDatabase references
+ for (let folder of this._underlyingFolders) {
+ this._releaseFolderDatabase(folder);
+ }
+ }
+
+ // kill off the view and its search association
+ if (this.dbView) {
+ this.listener.onDestroyingView(false);
+ this.search.dissociateView(this.dbView);
+ this.dbView.setTree(null);
+ this.dbView.setJSTree(null);
+ this.dbView.selection = null;
+ this.dbView.close();
+ this.dbView = null;
+ }
+
+ // zero out the view update depth here. We don't do it on open because it's
+ // theoretically be nice to be able to start a view update before you open
+ // something so you can defer the open. In practice, that is not yet
+ // tested.
+ this._viewUpdateDepth = 0;
+
+ this._underlyingData = this.kUnderlyingNone;
+ this._underlyingFolders = null;
+ this._syntheticView = null;
+
+ this._mailViewIndex = MailViewConstants.kViewItemAll;
+ this._mailViewData = null;
+
+ this._specialView = null;
+
+ this._sort = [];
+ this.__viewFlags = null;
+
+ this.search = null;
+ },
+
+ /**
+ * Open the passed-in nsIMsgFolder folder. Use openSynthetic for synthetic
+ * view providers.
+ */
+ open(aFolder) {
+ if (aFolder == null) {
+ this.close();
+ return;
+ }
+
+ // If we are in the same folder, there is nothing to do unless we are a
+ // virtual folder. Virtual folders apparently want to try and get updated.
+ if (this.displayedFolder == aFolder) {
+ if (!this.isVirtual) {
+ return;
+ }
+ // note: we intentionally (for consistency with old code, not that the
+ // code claimed to have a good reason) fall through here and call
+ // onLeavingFolder via close even though that's debatable in this case.
+ }
+ this.close();
+
+ this.displayedFolder = aFolder;
+ this._enteredFolder = false;
+
+ this.search = new SearchSpec(this);
+ this._sort = [];
+
+ if (aFolder.isServer) {
+ this._showServer();
+ return;
+ }
+
+ let typeForTelemetry =
+ [
+ "Inbox",
+ "Drafts",
+ "Trash",
+ "SentMail",
+ "Templates",
+ "Junk",
+ "Archive",
+ "Queue",
+ "Virtual",
+ ].find(x => aFolder.getFlag(Ci.nsMsgFolderFlags[x])) || "Other";
+ Services.telemetry.keyedScalarAdd(
+ "tb.mails.folder_opened",
+ typeForTelemetry,
+ 1
+ );
+
+ this.beginViewUpdate();
+ let msgDatabase;
+ try {
+ // This will throw an exception if the .msf file is missing,
+ // out of date (e.g., the local folder has changed), or corrupted.
+ msgDatabase = this.displayedFolder.msgDatabase;
+ } catch (e) {}
+ if (msgDatabase) {
+ this._prepareToLoadView(msgDatabase, aFolder);
+ }
+
+ if (!this.isVirtual) {
+ this.folderLoading = true;
+ FolderNotificationHelper.updateFolderAndNotifyOnLoad(
+ this.displayedFolder,
+ this,
+ this.listener.msgWindow
+ );
+ }
+
+ // we do this after kicking off the update because this could initiate a
+ // search which could fight our explicit updateFolder call if the search
+ // is already outstanding.
+ if (this.shouldShowMessagesForFolderImmediately()) {
+ this._enterFolder();
+ }
+ },
+
+ /**
+ * Open a synthetic view provider as backing our view.
+ */
+ openSynthetic(aSyntheticView) {
+ this.close();
+
+ this._underlyingData = this.kUnderlyingSynthetic;
+ this._syntheticView = aSyntheticView;
+
+ this.search = new SearchSpec(this);
+ this._sort = this._syntheticView.defaultSort.concat();
+
+ this._applyViewChanges();
+ FolderNotificationHelper.noteCuriosity(this);
+ this.listener.onDisplayingFolder();
+ },
+
+ /**
+ * Makes us irrevocavbly be a search view, for use in search windows.
+ * Once you call this, you are not allowed to use us for anything
+ * but a search view!
+ * We add a 'searchFolders' property that allows you to control what
+ * folders we are searching over.
+ */
+ openSearchView() {
+ this.close();
+
+ this._underlyingData = this.kUnderlyingSearchView;
+ this._underlyingFolders = [];
+
+ let dis = this;
+ this.__defineGetter__("searchFolders", function () {
+ return dis._underlyingFolders;
+ });
+ this.__defineSetter__("searchFolders", function (aSearchFolders) {
+ dis._underlyingFolders = aSearchFolders;
+ dis._applyViewChanges();
+ });
+
+ this.search = new SearchSpec(this);
+ // the search view uses the order in which messages are added as the
+ // order by default.
+ this._sort = [
+ [Ci.nsMsgViewSortType.byNone, Ci.nsMsgViewSortOrder.ascending],
+ ];
+ this.__viewFlags = Ci.nsMsgViewFlagsType.kNone;
+
+ FolderNotificationHelper.noteCuriosity(this);
+ this._applyViewChanges();
+ },
+
+ get folderLoading() {
+ return this._folderLoading;
+ },
+ set folderLoading(aFolderLoading) {
+ if (this._folderLoading == aFolderLoading) {
+ return;
+ }
+ this._folderLoading = aFolderLoading;
+ // tell the folder about what is going on so it can remove its db change
+ // listener and restore it, respectively.
+ if (aFolderLoading) {
+ this.displayedFolder.startFolderLoading();
+ } else {
+ this.displayedFolder.endFolderLoading();
+ }
+ this.listener.onFolderLoading(aFolderLoading);
+ },
+
+ get searching() {
+ return this._searching;
+ },
+ set searching(aSearching) {
+ if (aSearching == this._searching) {
+ return;
+ }
+ this._searching = aSearching;
+ this.listener.onSearching(aSearching);
+ // notify that all messages are loaded if searching has concluded
+ if (!aSearching) {
+ this.listener.onMessagesLoaded(true);
+ }
+ },
+
+ /**
+ * Do we want to show the messages immediately, or should we wait for
+ * updateFolder to complete? The historical heuristic is:
+ * - Virtual folders get shown immediately (and updateFolder has no
+ * meaning for them anyways.)
+ * - If _underlyingFolders == null, we failed to open the database,
+ * so we need to wait for UpdateFolder to reparse the folder (in the
+ * local folder case).
+ * - Wait on updateFolder if our poor man's security via
+ * "mail.password_protect_local_cache" preference is enabled and the
+ * server requires a password to login. This is accomplished by asking our
+ * listener via shouldDeferMessageDisplayUntilAfterServerConnect. Note that
+ * there is an obvious hole in this logic because of the virtual folder case
+ * above.
+ *
+ * @pre this.folderDisplayed is the folder we are talking about.
+ *
+ * @returns true if the folder should be shown immediately, false if we should
+ * wait for updateFolder to complete.
+ */
+ shouldShowMessagesForFolderImmediately() {
+ return (
+ this.isVirtual ||
+ !(
+ this._underlyingFolders == null ||
+ this.listener.shouldDeferMessageDisplayUntilAfterServerConnect
+ )
+ );
+ },
+ /**
+ * Extract information about the view from the dbFolderInfo (e.g., sort type,
+ * sort order, current view flags, etc), and save in the view wrapper.
+ */
+ _prepareToLoadView(msgDatabase, aFolder) {
+ let dbFolderInfo = msgDatabase.dBFolderInfo;
+ // - retrieve persisted sort information
+ this._sort = [[dbFolderInfo.sortType, dbFolderInfo.sortOrder]];
+
+ // - retrieve persisted display settings
+ this.__viewFlags = dbFolderInfo.viewFlags;
+ // - retrieve persisted thread last expanded state.
+ this._threadExpandAll = Boolean(
+ this.__viewFlags & Ci.nsMsgViewFlagsType.kExpandAll
+ );
+
+ // Make sure the threaded bit is set if group-by-sort is set. The views
+ // encode 3 states in 2-bits, and we want to avoid that odd-man-out
+ // state.
+ // The expand flag must be set when opening a single virtual folder
+ // (quicksearch) in grouped view. The user's last set expand/collapse state
+ // for grouped/threaded in this use case is restored later.
+ if (this.__viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort) {
+ this.__viewFlags |= Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ this.__viewFlags |= Ci.nsMsgViewFlagsType.kExpandAll;
+ this._ensureValidSort();
+ }
+
+ // See if the last-used view was one of the special views. If so, put us in
+ // that special view mode. We intentionally do this after restoring the
+ // view flags because _setSpecialView enforces threading.
+ // The nsMsgDBView is the one who persists this information for us. In this
+ // case the nsMsgThreadedDBView superclass of the special views triggers it
+ // when opened.
+ let viewType = dbFolderInfo.viewType;
+ if (
+ viewType == Ci.nsMsgViewType.eShowThreadsWithUnread ||
+ viewType == Ci.nsMsgViewType.eShowWatchedThreadsWithUnread
+ ) {
+ this._setSpecialView(viewType);
+ }
+
+ // - retrieve virtual folder configuration
+ if (aFolder.flags & Ci.nsMsgFolderFlags.Virtual) {
+ let virtFolder = VirtualFolderHelper.wrapVirtualFolder(aFolder);
+
+ if (virtFolder.searchFolderURIs == "*") {
+ // This is a special virtual folder that searches all folders in all
+ // accounts (except the unwanted types listed). Get those folders now.
+ let unwantedFlags =
+ Ci.nsMsgFolderFlags.Trash |
+ Ci.nsMsgFolderFlags.Junk |
+ Ci.nsMsgFolderFlags.Queue |
+ Ci.nsMsgFolderFlags.Virtual;
+ this._underlyingFolders = [];
+ for (let server of MailServices.accounts.allServers) {
+ for (let f of server.rootFolder.descendants) {
+ if (!f.isSpecialFolder(unwantedFlags, true)) {
+ this._underlyingFolders.push(f);
+ }
+ }
+ }
+ } else {
+ // Filter out the server roots; they only exist for UI reasons.
+ this._underlyingFolders = virtFolder.searchFolders.filter(
+ folder => !folder.isServer
+ );
+ }
+ this._underlyingData =
+ this._underlyingFolders.length > 1
+ ? this.kUnderlyingMultipleFolder
+ : this.kUnderlyingRealFolder;
+
+ // figure out if we are using online IMAP searching
+ this.search.onlineSearch = virtFolder.onlineSearch;
+
+ // retrieve and chew the search query
+ this.search.virtualFolderTerms = virtFolder.searchTerms;
+ } else {
+ this._underlyingData = this.kUnderlyingRealFolder;
+ this._underlyingFolders = [this.displayedFolder];
+ }
+
+ FolderNotificationHelper.stalkFolders(
+ this._underlyingFolders,
+ this.displayedFolder,
+ this
+ );
+
+ // - retrieve mail view configuration
+ if (this.listener.shouldUseMailViews) {
+ // if there is a view tag (basically ":tagname"), then it's a
+ // mailview tag. clearly.
+ let mailViewTag = dbFolderInfo.getCharProperty(
+ MailViewConstants.kViewCurrentTag
+ );
+ // "0" and "1" are all and unread views, respectively, from 2.0
+ if (mailViewTag && mailViewTag != "0" && mailViewTag != "1") {
+ // the tag gets stored with a ":" on the front, presumably done
+ // as a means of name-spacing that was never subsequently leveraged.
+ if (mailViewTag.startsWith(":")) {
+ mailViewTag = mailViewTag.substr(1);
+ }
+ // (the true is so we don't persist)
+ this.setMailView(MailViewConstants.kViewItemTags, mailViewTag, true);
+ } else {
+ // otherwise it's just an index. we kinda-sorta migrate from old-school
+ // $label tags, except someone reused one of the indices for
+ // kViewItemNotDeleted, which means that $label2 can no longer be
+ // migrated.
+ let mailViewIndex = dbFolderInfo.getUint32Property(
+ MailViewConstants.kViewCurrent,
+ MailViewConstants.kViewItemAll
+ );
+ // label migration per above
+ if (
+ mailViewIndex == MailViewConstants.kViewItemTags ||
+ (MailViewConstants.kViewItemTags + 2 <= mailViewIndex &&
+ mailViewIndex < MailViewConstants.kViewItemVirtual)
+ ) {
+ this.setMailView(
+ MailViewConstants.kViewItemTags,
+ "$label" + (mailViewIndex - 1)
+ );
+ } else {
+ this.setMailView(mailViewIndex);
+ }
+ }
+ }
+
+ this.listener.onLoadingFolder(dbFolderInfo);
+ },
+
+ /**
+ * Creates a view appropriate to the current settings of the folder display
+ * widget, returning it. The caller is responsible to assign the result to
+ * this.dbView (or whatever it wants to do with it.)
+ */
+ _createView() {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=";
+
+ // we will have saved these off when closing our view
+ let viewFlags =
+ this.__viewFlags ??
+ Services.prefs.getIntPref("mailnews.default_view_flags", 1);
+
+ // real folders are subject to the most interest set of possibilities...
+ if (this._underlyingData == this.kUnderlyingRealFolder) {
+ // quick-search inherits from threaded which inherits from group, so this
+ // is right to choose it first.
+ if (this.search.hasSearchTerms) {
+ dbviewContractId += "quicksearch";
+ } else if (this.showGroupedBySort) {
+ dbviewContractId += "group";
+ } else if (this.specialViewThreadsWithUnread) {
+ dbviewContractId += "threadswithunread";
+ } else if (this.specialViewWatchedThreadsWithUnread) {
+ dbviewContractId += "watchedthreadswithunread";
+ } else {
+ dbviewContractId += "threaded";
+ }
+ } else if (this._underlyingData == this.kUnderlyingMultipleFolder) {
+ // if we're dealing with virtual folders, the answer is always an xfvf
+ dbviewContractId += "xfvf";
+ } else {
+ // kUnderlyingSynthetic or kUnderlyingSearchView
+ dbviewContractId += "search";
+ }
+
+ // and now zero the saved-off flags.
+ this.__viewFlags = null;
+
+ let dbView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ dbView.init(
+ this.listener.messenger,
+ this.listener.msgWindow,
+ this.listener.threadPaneCommandUpdater
+ );
+ // Excluding Group By views, use the least-specific sort so we can clock
+ // them back through to build up the correct sort order,
+ const index =
+ viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort
+ ? 0
+ : this._sort.length - 1;
+ let [sortType, sortOrder, sortCustomCol] = this._getSortDetails(index);
+ let outCount = {};
+ // when the underlying folder is a single real folder (virtual or no), we
+ // tell the view about the underlying folder.
+ if (this.isSingleFolder) {
+ // If the folder is virtual, m_viewFolder needs to be set before the
+ // folder is opened, otherwise persisted sort info will not be restored
+ // from the right dbFolderInfo. The use case is for a single folder
+ // backed saved search. Currently, sort etc. changes in quick filter are
+ // persisted (gloda list and quick filter in gloda list are not involved).
+ if (this.isVirtual) {
+ dbView.viewFolder = this.displayedFolder;
+ }
+
+ // Open the folder.
+ dbView.open(
+ this._underlyingFolders[0],
+ sortType,
+ sortOrder,
+ viewFlags,
+ outCount
+ );
+
+ // If there are any search terms, we need to tell the db view about the
+ // the display (/virtual) folder so it can store all the view-specific
+ // data there (things like the active mail view and such that go in
+ // dbFolderInfo.) This also goes for cases where the quick search is
+ // active; the C++ code explicitly nulls out the view folder for no
+ // good/documented reason, so we need to set it again if we want changes
+ // made with the quick filter applied. (We don't just change the C++
+ // code because there could be SeaMonkey fallout.) See bug 502767 for
+ // info about the quick-search part of the problem.
+ if (this.search.hasSearchTerms) {
+ dbView.viewFolder = this.displayedFolder;
+ }
+ } else {
+ // when we're dealing with a multi-folder virtual folder, we just tell the
+ // db view about the display folder. (It gets its own XFVF view, so it
+ // knows what to do.)
+ // and for a synthetic folder, displayedFolder is null anyways
+ dbView.open(
+ this.displayedFolder,
+ sortType,
+ sortOrder,
+ viewFlags,
+ outCount
+ );
+ }
+ if (sortCustomCol) {
+ dbView.curCustomColumn = sortCustomCol;
+ }
+
+ // we all know it's a tree view, make sure the interface is available
+ // so no one else has to do this.
+ dbView.QueryInterface(Ci.nsITreeView);
+
+ // If Grouped By, the view has already been opened with the most specific
+ // sort (groups themselves are always sorted by date).
+ if (!(viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort)) {
+ // clock through the rest of the sorts, if there are any
+ for (let iSort = this._sort.length - 2; iSort >= 0; iSort--) {
+ [sortType, sortOrder, sortCustomCol] = this._getSortDetails(iSort);
+ if (sortCustomCol) {
+ dbView.curCustomColumn = sortCustomCol;
+ }
+ dbView.sort(sortType, sortOrder);
+ }
+ }
+
+ return dbView;
+ },
+
+ /**
+ * Callback method invoked by FolderNotificationHelper when our folder is
+ * loaded. Assuming we are still interested in the folder, we enter the
+ * folder via _enterFolder.
+ */
+ _folderLoaded(aFolder) {
+ if (aFolder == this.displayedFolder) {
+ this.folderLoading = false;
+ // If _underlyingFolders is null, DBViewWrapper_open probably got
+ // an exception trying to open the db, but after reparsing the local
+ // folder, we should have a db, so set up the view based on info
+ // from the db.
+ if (this._underlyingFolders == null) {
+ this._prepareToLoadView(aFolder.msgDatabase, aFolder);
+ }
+ this._enterFolder();
+ }
+ },
+
+ /**
+ * Enter this.displayedFolder if we have not yet entered it.
+ *
+ * Things we do on entering a folder:
+ * - clear the folder's biffState!
+ * - set the message database's header cache size
+ */
+ _enterFolder() {
+ if (this._enteredFolder) {
+ this.listener.onMessagesLoaded(true);
+ return;
+ }
+
+ this.displayedFolder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
+
+ // we definitely want a view at this point; force the view.
+ this._viewUpdateDepth = 0;
+ this._applyViewChanges();
+
+ this.listener.onDisplayingFolder();
+
+ this._enteredFolder = true;
+ },
+
+ /**
+ * Renames, moves to the trash, it's all crazy. We have to update all our
+ * references when this happens.
+ */
+ _folderMoved(aOldFolder, aNewFolder) {
+ if (aOldFolder == this.displayedFolder) {
+ this.displayedFolder = aNewFolder;
+ }
+
+ if (!this._underlyingFolders) {
+ // View is closed already.
+ return;
+ }
+
+ let i = this._underlyingFolders.findIndex(f => f == aOldFolder);
+ if (i >= 0) {
+ this._underlyingFolders[i] = aNewFolder;
+ }
+
+ // re-populate the view.
+ this._applyViewChanges();
+ },
+
+ /**
+ * FolderNotificationHelper tells us when folders we care about are deleted
+ * (because we asked it to in |open|). If it was the folder we were
+ * displaying (real or virtual), this closes it. If we are virtual and
+ * backed by a single folder, this closes us. If we are backed by multiple
+ * folders, we just update ourselves. (Currently, cross-folder views are
+ * not clever enough to purge the mooted messages, so we need to do this to
+ * help them out.)
+ * We do not update virtual folder definitions as a result of deletion; we are
+ * a display abstraction. That (hopefully) happens elsewhere.
+ */
+ _folderDeleted(aFolder) {
+ // XXX When we empty the trash, we're actually sending a folder deleted
+ // notification around. This check ensures we don't think we've really
+ // deleted the trash folder in the DBViewWrapper, and that stops nasty
+ // things happening, like forgetting we've got the trash folder selected.
+ if (aFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, false)) {
+ return;
+ }
+
+ if (aFolder == this.displayedFolder) {
+ this.close();
+ return;
+ }
+
+ // indexOf doesn't work for this (reliably)
+ for (let [i, underlyingFolder] of this._underlyingFolders.entries()) {
+ if (aFolder == underlyingFolder) {
+ this._underlyingFolders.splice(i, 1);
+ break;
+ }
+ }
+
+ if (this._underlyingFolders.length == 0) {
+ this.close();
+ return;
+ }
+ // if we are virtual, this will update the search session which draws its
+ // search scopes from this._underlyingFolders anyways.
+ this._applyViewChanges();
+ },
+
+ /**
+ * Compacting a local folder nukes its message keys, requiring the view to be
+ * rebuilt. If the folder is IMAP, it doesn't matter because the UIDs are
+ * the message keys and we can ignore it. In the local case we want to
+ * notify our listener so they have a chance to save the selected messages.
+ */
+ _aboutToCompactFolder(aFolder) {
+ // IMAP compaction does not affect us unless we are holding headers
+ if (aFolder.server.type == "imap") {
+ return;
+ }
+
+ // we will have to re-create the view, so nuke the view now.
+ if (this.dbView) {
+ this.listener.onDestroyingView(true);
+ this.search.dissociateView(this.dbView);
+ this.dbView.close();
+ this.dbView = null;
+ }
+ },
+
+ /**
+ * Compaction is all done, let's re-create the view! (Unless the folder is
+ * IMAP, in which case we are ignoring this event sequence.)
+ */
+ _compactedFolder(aFolder) {
+ // IMAP compaction does not affect us unless we are holding headers
+ if (aFolder.server.type == "imap") {
+ return;
+ }
+
+ this.refresh();
+ },
+
+ /**
+ * DB Views need help to know when their move / deletion operations complete.
+ * This happens in both single-folder and multiple-folder backed searches.
+ * In the latter case, there is potential danger that we tell a view that did
+ * not initiate the move / deletion but has kicked off its own about the
+ * completion and confuse it. However, that's on the view code.
+ */
+ _deleteCompleted(aFolder) {
+ if (this.dbView) {
+ this.dbView.onDeleteCompleted(true);
+ }
+ this.listener.onMessagesRemoved();
+ },
+
+ /**
+ * See _deleteCompleted for an explanation of what is going on.
+ */
+ _deleteFailed(aFolder) {
+ if (this.dbView) {
+ this.dbView.onDeleteCompleted(false);
+ }
+ this.listener.onMessageRemovalFailed();
+ },
+
+ _forceOpen(aFolder) {
+ this.displayedFolder = null;
+ this.open(aFolder);
+ },
+
+ _renameCompleted(aFolder) {
+ if (aFolder == this.displayedFolder) {
+ this._forceOpen(aFolder);
+ }
+ },
+
+ /**
+ * If the displayed folder had its total message count or total unread message
+ * count change, notify the listener. (Note: only for the display folder;
+ * not the underlying folders!)
+ */
+ _messageCountsChanged(aFolder) {
+ if (aFolder == this.displayedFolder) {
+ this.listener.onMessageCountsChanged();
+ }
+ },
+
+ /**
+ * @returns the current set of viewFlags. This may be:
+ * - A modified set of flags that are pending application because a view
+ * update is in effect and we don't want to modify the view when it's just
+ * going to get destroyed.
+ * - The live set of flags from the current dbView.
+ * - The 'limbo' set of flags because we currently lack a view but will have
+ * one soon (and then we will apply the flags).
+ */
+ get _viewFlags() {
+ if (this.__viewFlags != null) {
+ return this.__viewFlags;
+ }
+ if (this.dbView) {
+ return this.dbView.viewFlags;
+ }
+ return 0;
+ },
+ /**
+ * Update the view flags to use on the view. If we are in a view update or
+ * currently don't have a view, we save the view flags for later usage when
+ * the view gets (re)built. If we have a view, depending on what's happening
+ * we may re-create the view or just set the bits. The rules/reasons are:
+ * - XFVF views can handle the flag changes, just set the flags.
+ * - XFVF threaded/unthreaded change must re-sort, the backend forgot.
+ * - Single-folder virtual folders (quicksearch) can handle viewFlag changes,
+ * to/from grouped included, so set it.
+ * - Single-folder threaded/unthreaded can handle a change to/from unthreaded/
+ * threaded, so set it.
+ * - Single-folder can _not_ handle a change between grouped and not-grouped,
+ * so re-generate the view. Also it can't handle a change involving
+ * kUnreadOnly or kShowIgnored.
+ */
+ set _viewFlags(aViewFlags) {
+ if (this._viewUpdateDepth || !this.dbView) {
+ this.__viewFlags = aViewFlags;
+ return;
+ }
+
+ // For viewFlag changes, do not make a random selection if there is not
+ // actually anything selected; some views do this (looking at xfvf).
+ if (this.dbView.selection && this.dbView.selection.count == 0) {
+ this.dbView.selection.currentIndex = -1;
+ }
+
+ let setViewFlags = true;
+ let reSort = false;
+ let oldFlags = this.dbView.viewFlags;
+ let changedFlags = oldFlags ^ aViewFlags;
+
+ if (this.isVirtual) {
+ if (
+ this.isMultiFolder &&
+ changedFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay &&
+ !(changedFlags & Ci.nsMsgViewFlagsType.kGroupBySort)
+ ) {
+ reSort = true;
+ }
+ if (this.isSingleFolder) {
+ // ugh, and the single folder case needs us to re-apply his sort...
+ reSort = true;
+ }
+ } else {
+ // The regular single folder case.
+ if (
+ changedFlags &
+ (Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kUnreadOnly |
+ Ci.nsMsgViewFlagsType.kShowIgnored)
+ ) {
+ setViewFlags = false;
+ }
+ // ugh, and the single folder case needs us to re-apply his sort...
+ reSort = true;
+ }
+
+ if (setViewFlags) {
+ this.dbView.viewFlags = aViewFlags;
+ if (reSort) {
+ this.dbView.sort(this.dbView.sortType, this.dbView.sortOrder);
+ }
+ this.listener.onSortChanged();
+ } else {
+ this.__viewFlags = aViewFlags;
+ this._applyViewChanges();
+ }
+ },
+
+ /**
+ * Apply accumulated changes to the view. If we are in a batch, we do
+ * nothing, relying on endDisplayUpdate to call us.
+ */
+ _applyViewChanges() {
+ // if we are in a batch, wait for endDisplayUpdate to be called to get us
+ // out to zero.
+ if (this._viewUpdateDepth) {
+ return;
+ }
+ // make the dbView stop being a search listener if it is one
+ if (this.dbView) {
+ // save the view's flags if it has any and we haven't already overridden
+ // them.
+ if (this.__viewFlags == null) {
+ this.__viewFlags = this.dbView.viewFlags;
+ }
+ this.listener.onDestroyingView(true); // we will re-create it!
+ this.search.dissociateView(this.dbView);
+ this.dbView.close();
+ this.dbView = null;
+ }
+
+ this.dbView = this._createView();
+ // if the synthetic view defines columns, add those for it
+ if (this.isSynthetic) {
+ for (let customCol of this._syntheticView.customColumns) {
+ customCol.bindToView(this.dbView);
+ this.dbView.addColumnHandler(customCol.id, customCol);
+ }
+ }
+ this.listener.onCreatedView();
+
+ // this ends up being a no-op if there are no search terms
+ this.search.associateView(this.dbView);
+
+ // If we are searching, then the search will generate the all messages
+ // loaded notification. Although in some cases the search may have
+ // completed by now, that is not a guarantee. The search logic is
+ // time-slicing, which is why this can vary. (If it uses up its time
+ // slices, it will re-schedule itself, returning to us before completing.)
+ // Which is why we always defer to the search if one is active.
+ // If we are loading the folder, the load completion will also notify us,
+ // so we should not generate all messages loaded right now.
+ if (!this.searching && !this.folderLoading) {
+ this.listener.onMessagesLoaded(true);
+ } else if (this.dbView.numMsgsInView > 0) {
+ this.listener.onMessagesLoaded(false);
+ }
+ },
+
+ get isMailFolder() {
+ return Boolean(
+ this.displayedFolder &&
+ this.displayedFolder.flags & Ci.nsMsgFolderFlags.Mail
+ );
+ },
+
+ get isNewsFolder() {
+ return Boolean(
+ this.displayedFolder &&
+ this.displayedFolder.flags & Ci.nsMsgFolderFlags.Newsgroup
+ );
+ },
+
+ get isFeedFolder() {
+ return Boolean(
+ this.displayedFolder && this.displayedFolder.server.type == "rss"
+ );
+ },
+
+ OUTGOING_FOLDER_FLAGS:
+ Ci.nsMsgFolderFlags.SentMail |
+ Ci.nsMsgFolderFlags.Drafts |
+ Ci.nsMsgFolderFlags.Queue |
+ Ci.nsMsgFolderFlags.Templates,
+ /**
+ * @returns true if the folder is an outgoing folder by virtue of being a
+ * sent mail folder, drafts folder, queue folder, or template folder,
+ * or being a sub-folder of one of those types of folders.
+ */
+ get isOutgoingFolder() {
+ return (
+ this.displayedFolder &&
+ this.displayedFolder.isSpecialFolder(this.OUTGOING_FOLDER_FLAGS, true)
+ );
+ },
+ /**
+ * @returns true if the folder is not known to be a special outgoing folder
+ * or the descendent of a special outgoing folder.
+ */
+ get isIncomingFolder() {
+ return !this.isOutgoingFolder;
+ },
+
+ get isVirtual() {
+ return Boolean(
+ this.displayedFolder &&
+ this.displayedFolder.flags & Ci.nsMsgFolderFlags.Virtual
+ );
+ },
+
+ /**
+ * Prevent view updates from running until a paired |endViewUpdate| call is
+ * made. This is an advisory method intended to aid us in performing
+ * redundant view re-computations and does not forbid us from building the
+ * view earlier if we have a good reason.
+ * Since calling endViewUpdate will compel a view update when the update
+ * depth reaches 0, you should only call this method if you are sure that
+ * you will need the view to be re-built. If you are doing things like
+ * changing to/from threaded mode that do not cause the view to be rebuilt,
+ * you should just set those attributes directly.
+ */
+ beginViewUpdate() {
+ this._viewUpdateDepth++;
+ },
+
+ /**
+ * Conclude a paired call to |beginViewUpdate|. Assuming the view depth has
+ * reached 0 with this call, the view will be re-created with the current
+ * settings.
+ */
+ endViewUpdate(aForceLevel) {
+ if (--this._viewUpdateDepth == 0) {
+ this._applyViewChanges();
+ }
+ // Avoid pathological situations.
+ if (this._viewUpdateDepth < 0) {
+ this._viewUpdateDepth = 0;
+ }
+ },
+
+ /**
+ * @returns the primary sort type (as one of the numeric constants from
+ * nsMsgViewSortType).
+ */
+ get primarySortType() {
+ return this._sort[0][0];
+ },
+
+ /**
+ * @returns the primary sort order (as one of the numeric constants from
+ * nsMsgViewSortOrder.)
+ */
+ get primarySortOrder() {
+ return this._sort[0][1];
+ },
+
+ /**
+ * @returns true if the dominant sort is ascending.
+ */
+ get isSortedAscending() {
+ return (
+ this._sort.length && this._sort[0][1] == Ci.nsMsgViewSortOrder.ascending
+ );
+ },
+ /**
+ * @returns true if the dominant sort is descending.
+ */
+ get isSortedDescending() {
+ return (
+ this._sort.length && this._sort[0][1] == Ci.nsMsgViewSortOrder.descending
+ );
+ },
+ /**
+ * Indicate if we are sorting by time or something correlated with time.
+ *
+ * @returns true if the dominant sort is by time.
+ */
+ get sortImpliesTemporalOrdering() {
+ if (!this._sort.length) {
+ return false;
+ }
+ let sortType = this._sort[0][0];
+ return (
+ sortType == Ci.nsMsgViewSortType.byDate ||
+ sortType == Ci.nsMsgViewSortType.byReceived ||
+ sortType == Ci.nsMsgViewSortType.byId ||
+ sortType == Ci.nsMsgViewSortType.byThread
+ );
+ },
+
+ sortAscending() {
+ if (!this.isSortedAscending) {
+ this.magicSort(this._sort[0][0], Ci.nsMsgViewSortOrder.ascending);
+ }
+ },
+ sortDescending() {
+ if (!this.isSortedDescending) {
+ this.magicSort(this._sort[0][0], Ci.nsMsgViewSortOrder.descending);
+ }
+ },
+
+ /**
+ * Explicit sort command. We ignore all previous sort state and only apply
+ * what you tell us. If you want implied secondary sort, use |magicSort|.
+ * You must use this sort command, and never directly call the sort commands
+ * on the underlying db view! If you do not, make sure to fight us every
+ * step of the way, because we will keep clobbering your manually applied
+ * sort.
+ * For secondary and multiple custom column support, a byCustom aSortType and
+ * aSecondaryType must be the column name string.
+ */
+ sort(aSortType, aSortOrder, aSecondaryType, aSecondaryOrder) {
+ // For sort changes, do not make a random selection if there is not
+ // actually anything selected; some views do this (looking at xfvf).
+ if (this.dbView.selection && this.dbView.selection.count == 0) {
+ this.dbView.selection.currentIndex = -1;
+ }
+
+ this._sort = [[aSortType, aSortOrder]];
+ if (aSecondaryType != null && aSecondaryOrder != null) {
+ this._sort.push([aSecondaryType, aSecondaryOrder]);
+ }
+ // make sure the sort won't make the view angry...
+ this._ensureValidSort();
+ // if we are not in a view update, invoke the sort.
+ if (this._viewUpdateDepth == 0 && this.dbView) {
+ for (let iSort = this._sort.length - 1; iSort >= 0; iSort--) {
+ // apply them in the reverse order
+ let [sortType, sortOrder, sortCustomCol] = this._getSortDetails(iSort);
+ if (sortCustomCol) {
+ this.dbView.curCustomColumn = sortCustomCol;
+ }
+ this.dbView.sort(sortType, sortOrder);
+ }
+ // (only generate the event since we're not in a update batch)
+ this.listener.onSortChanged();
+ }
+ // (if we are in a view update, then a new view will be created when the
+ // update ends, and it will just use the new sort order anyways.)
+ },
+
+ /**
+ * Logic that compensates for custom column identifiers being provided as
+ * sort types.
+ *
+ * @returns [sort type, sort order, sort custom column name]
+ */
+ _getSortDetails(aIndex) {
+ let [sortType, sortOrder] = this._sort[aIndex];
+ let sortCustomColumn = null;
+ let sortTypeType = typeof sortType;
+ if (sortTypeType != "number") {
+ sortCustomColumn = sortTypeType == "string" ? sortType : sortType.id;
+ sortType = Ci.nsMsgViewSortType.byCustom;
+ }
+
+ return [sortType, sortOrder, sortCustomColumn];
+ },
+
+ /**
+ * Accumulates implied secondary sorts based on multiple calls to this method.
+ * This is intended to be hooked up to be controlled by the UI.
+ * Because we are lazy, we actually just poke the view's sort method and save
+ * the apparent secondary sort. This also allows perfect compliance with the
+ * way this used to be implemented!
+ * For secondary and multiple custom column support, a byCustom aSortType must
+ * be the column name string.
+ */
+ magicSort(aSortType, aSortOrder) {
+ if (this.dbView) {
+ // For sort changes, do not make a random selection if there is not
+ // actually anything selected; some views do this (looking at xfvf).
+ if (this.dbView.selection && this.dbView.selection.count == 0) {
+ this.dbView.selection.currentIndex = -1;
+ }
+
+ // so, the thing we just set obviously will be there
+ this._sort = [[aSortType, aSortOrder]];
+ // (make sure it is valid...)
+ this._ensureValidSort();
+ // get sort details, handle custom column as string sortType
+ let [sortType, sortOrder, sortCustomCol] = this._getSortDetails(0);
+ if (sortCustomCol) {
+ this.dbView.curCustomColumn = sortCustomCol;
+ }
+ // apply the sort to see what happens secondary-wise
+ this.dbView.sort(sortType, sortOrder);
+ // there is only a secondary sort if it's not none and not the same.
+ if (
+ this.dbView.secondarySortType != Ci.nsMsgViewSortType.byNone &&
+ (this.dbView.secondarySortType != sortType ||
+ (this.dbView.secondarySortType == Ci.nsMsgViewSortType.byCustom &&
+ this.dbView.secondaryCustomColumn != sortCustomCol))
+ ) {
+ this._sort.push([
+ this.dbView.secondaryCustomColumn || this.dbView.secondarySortType,
+ this.dbView.secondarySortOrder,
+ ]);
+ }
+ // only tell our listener if we're not in a view update batch
+ if (this._viewUpdateDepth == 0) {
+ this.listener.onSortChanged();
+ }
+ }
+ },
+
+ /**
+ * Make sure the current sort is valid under our other constraints, make it
+ * safe if it is not. Most specifically, some sorts are illegal when
+ * grouping by sort, and we reset the sort to date in those cases.
+ *
+ * @param aViewFlags Optional set of view flags to consider instead of the
+ * potentially live view flags.
+ */
+ _ensureValidSort(aViewFlags) {
+ if (
+ (aViewFlags != null ? aViewFlags : this._viewFlags) &
+ Ci.nsMsgViewFlagsType.kGroupBySort
+ ) {
+ // We cannot be sorting by thread, id, none, or size. If we are, switch
+ // to sorting by date.
+ for (let sortPair of this._sort) {
+ let sortType = sortPair[0];
+ if (
+ sortType == Ci.nsMsgViewSortType.byThread ||
+ sortType == Ci.nsMsgViewSortType.byId ||
+ sortType == Ci.nsMsgViewSortType.byNone ||
+ sortType == Ci.nsMsgViewSortType.bySize
+ ) {
+ this._sort = [[Ci.nsMsgViewSortType.byDate, this._sort[0][1]]];
+ break;
+ }
+ }
+ }
+ },
+
+ /**
+ * @returns {boolean} true if we are grouped-by-sort, false if not. If we are
+ * not grouped-by-sort, then we are either threaded or unthreaded; check
+ * the showThreaded property to find out which of those it is.
+ */
+ get showGroupedBySort() {
+ return Boolean(this._viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort);
+ },
+ /**
+ * Enable grouped-by-sort which is mutually exclusive with threaded display
+ * (as controlled/exposed by showThreaded). Grouped-by-sort is not legal
+ * for sorts by thread/id/size/none and enabling this will cause us to change
+ * our sort to by date in those cases.
+ */
+ set showGroupedBySort(aShowGroupBySort) {
+ if (this.showGroupedBySort != aShowGroupBySort) {
+ if (aShowGroupBySort) {
+ // For virtual single folders, the kExpandAll flag must be set.
+ // Do not apply the flag change until we have made the sort safe.
+ let viewFlags =
+ this._viewFlags |
+ Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kExpandAll |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ this._ensureValidSort(viewFlags);
+ this._viewFlags = viewFlags;
+ } else {
+ // maybe we shouldn't do anything in this case?
+ this._viewFlags &= ~(
+ Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay
+ );
+ }
+ }
+ },
+
+ /**
+ * Are we showing ignored/killed threads?
+ */
+ get showIgnored() {
+ return Boolean(this._viewFlags & Ci.nsMsgViewFlagsType.kShowIgnored);
+ },
+ /**
+ * Set whether we are showing ignored/killed threads.
+ */
+ set showIgnored(aShowIgnored) {
+ if (this.showIgnored == aShowIgnored) {
+ return;
+ }
+
+ if (aShowIgnored) {
+ this._viewFlags |= Ci.nsMsgViewFlagsType.kShowIgnored;
+ } else {
+ this._viewFlags &= ~Ci.nsMsgViewFlagsType.kShowIgnored;
+ }
+ },
+
+ /**
+ * @returns {boolean} true if we are in threaded mode (as opposed to unthreaded
+ * or grouped-by-sort).
+ */
+ get showThreaded() {
+ return Boolean(
+ this._viewFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay &&
+ !(this._viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort)
+ );
+ },
+ /**
+ * Set us to threaded display mode when set to true. If we are already in
+ * threaded display mode, we do nothing. If you want to set us to unthreaded
+ * mode, set |showUnthreaded| to true. (Because we have three modes of
+ * operation: unthreaded, threaded, and grouped-by-sort, we are a tri-state
+ * and setting us to false is ambiguous. We should probably be using a
+ * single attribute with three constants...)
+ */
+ set showThreaded(aShowThreaded) {
+ if (this.showThreaded != aShowThreaded) {
+ let viewFlags = this._viewFlags;
+ if (aShowThreaded) {
+ viewFlags |= Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ } else {
+ // Maybe we shouldn't do anything in this case?
+ viewFlags &= ~Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ }
+ // lose the group bit...
+ viewFlags &= ~Ci.nsMsgViewFlagsType.kGroupBySort;
+ this._viewFlags = viewFlags;
+ }
+ },
+
+ /**
+ * @returns {boolean} true if we are in unthreaded mode (which means not
+ * threaded and not grouped-by-sort).
+ */
+ get showUnthreaded() {
+ return Boolean(
+ !(
+ this._viewFlags &
+ (Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay)
+ )
+ );
+ },
+ /**
+ * Set to true to put us in unthreaded mode (which means not threaded and
+ * not grouped-by-sort).
+ */
+ set showUnthreaded(aShowUnthreaded) {
+ if (this.showUnthreaded != aShowUnthreaded) {
+ if (aShowUnthreaded) {
+ this._viewFlags &= ~(
+ Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay
+ );
+ } else {
+ // Maybe we shouldn't do anything in this case?
+ this._viewFlags =
+ (this._viewFlags & ~Ci.nsMsgViewFlagsType.kGroupBySort) |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ }
+ }
+ },
+
+ /**
+ * @returns true if we are showing only unread messages.
+ */
+ get showUnreadOnly() {
+ return Boolean(this._viewFlags & Ci.nsMsgViewFlagsType.kUnreadOnly);
+ },
+ /**
+ * Enable/disable showing only unread messages using the view's flag-based
+ * mechanism. This functionality can also be approximated using a mail
+ * view (or other search) for unread messages. There also exist special
+ * views for showing messages with unread threads which is different and
+ * has serious limitations because of its nature.
+ * Setting anything to this value clears any active special view because the
+ * actual UI use case (the "View... Threads..." menu) uses this setter
+ * intentionally as a mutually exclusive UI choice from the special views.
+ */
+ set showUnreadOnly(aShowUnreadOnly) {
+ if (this._specialView || this.showUnreadOnly != aShowUnreadOnly) {
+ let viewRebuildRequired = this._specialView != null;
+ this._specialView = null;
+ if (viewRebuildRequired) {
+ this.beginViewUpdate();
+ }
+
+ if (aShowUnreadOnly) {
+ this._viewFlags |= Ci.nsMsgViewFlagsType.kUnreadOnly;
+ } else {
+ this._viewFlags &= ~Ci.nsMsgViewFlagsType.kUnreadOnly;
+ }
+
+ if (viewRebuildRequired) {
+ this.endViewUpdate();
+ }
+ }
+ },
+
+ /**
+ * Read-only attribute indicating if a 'special view' is in use. There are
+ * two special views in existence, both of which are concerned about
+ * showing you threads that have any unread messages in them. They are views
+ * rather than search predicates because the search mechanism is not capable
+ * of expressing such a thing. (Or at least it didn't use to be? We might
+ * be able to whip something up these days...)
+ */
+ get specialView() {
+ return this._specialView != null;
+ },
+ /**
+ * Private helper for use by the specialView* setters that handles the common
+ * logic. We don't want this method to be public because we want it to be
+ * feasible for the view hierarchy and its enumerations to go away without
+ * code outside this class having to care so much.
+ */
+ _setSpecialView(aViewEnum) {
+ // special views simply cannot work for virtual folders. explode.
+ if (this.isVirtual) {
+ throw new Error("Virtual folders cannot use special views!");
+ }
+ this.beginViewUpdate();
+ // all special views imply a threaded view
+ this.showThreaded = true;
+ this._specialView = aViewEnum;
+ // We clear the search for paranoia/correctness reasons. However, the UI
+ // layer is currently responsible for making sure these are already zeroed
+ // out.
+ this.search.clear();
+ this.endViewUpdate();
+ },
+ /**
+ * @returns true if the special view that shows threads with unread messages
+ * in them is active.
+ */
+ get specialViewThreadsWithUnread() {
+ return this._specialView == Ci.nsMsgViewType.eShowThreadsWithUnread;
+ },
+ /**
+ * If true is assigned, attempts to enable the special view that shows threads
+ * with unread messages in them. This will not work on virtual folders
+ * because of the inheritance hierarchy.
+ * Any mechanism that requires search terms (quick search, mailviews) will be
+ * reset/disabled when enabling this view.
+ */
+ set specialViewThreadsWithUnread(aSpecial) {
+ this._setSpecialView(Ci.nsMsgViewType.eShowThreadsWithUnread);
+ },
+ /**
+ * @returns true if the special view that shows watched threads with unread
+ * messages in them is active.
+ */
+ get specialViewWatchedThreadsWithUnread() {
+ return this._specialView == Ci.nsMsgViewType.eShowWatchedThreadsWithUnread;
+ },
+ /**
+ * If true is assigned, attempts to enable the special view that shows watched
+ * threads with unread messages in them. This will not work on virtual
+ * folders because of the inheritance hierarchy.
+ * Any mechanism that requires search terms (quick search, mailviews) will be
+ * reset/disabled when enabling this view.
+ */
+ set specialViewWatchedThreadsWithUnread(aSpecial) {
+ this._setSpecialView(Ci.nsMsgViewType.eShowWatchedThreadsWithUnread);
+ },
+
+ get mailViewIndex() {
+ return this._mailViewIndex;
+ },
+
+ get mailViewData() {
+ return this._mailViewData;
+ },
+
+ /**
+ * Set the current mail view to the given mail view index with the provided
+ * data (normally only used for the 'tag' mail views.) We persist the state
+ * change
+ *
+ * @param aMailViewIndex The view to use, one of the kViewItem* constants from
+ * msgViewPickerOverlay.js OR the name of a custom view. (It's really up
+ * to MailViewManager.getMailViewByIndex...)
+ * @param aData Some piece of data appropriate to the mail view, currently
+ * this is only used for the tag name for kViewItemTags (sans the ":").
+ * @param aDoNotPersist If true, we don't save this change to the db folder
+ * info. This is intended for internal use only.
+ */
+ setMailView(aMailViewIndex, aData, aDoNotPersist) {
+ let mailViewDef = MailViewManager.getMailViewByIndex(aMailViewIndex);
+
+ this._mailViewIndex = aMailViewIndex;
+ this._mailViewData = aData;
+
+ // - update the search terms
+ // (this triggers a view update if we are not in a batch)
+ this.search.viewTerms = mailViewDef.makeTerms(this.search.session, aData);
+
+ // - persist the view to the folder.
+ if (!aDoNotPersist && this.displayedFolder) {
+ let msgDatabase = this.displayedFolder.msgDatabase;
+ if (msgDatabase) {
+ let dbFolderInfo = msgDatabase.dBFolderInfo;
+ dbFolderInfo.setUint32Property(
+ MailViewConstants.kViewCurrent,
+ this._mailViewIndex
+ );
+ // _mailViewData attempts to be sane and be the tag name, as opposed to
+ // magic-value ":"-prefixed value historically stored on disk. Because
+ // we want to be forwards and backwards compatible, we put this back on
+ // when we persist it. It's not like the property is really generic
+ // anyways.
+ dbFolderInfo.setCharProperty(
+ MailViewConstants.kViewCurrentTag,
+ this._mailViewData ? ":" + this._mailViewData : ""
+ );
+ }
+ }
+
+ this.listener.onMailViewChanged();
+ },
+
+ /**
+ * @returns true if the row at the given index contains a collapsed thread,
+ * false if the row is a collapsed group or anything else.
+ */
+ isCollapsedThreadAtIndex(aViewIndex) {
+ let flags = this.dbView.getFlagsAt(aViewIndex);
+ return (
+ flags & Ci.nsMsgMessageFlags.Elided &&
+ !(flags & MSG_VIEW_FLAG_DUMMY) &&
+ this.dbView.isContainer(aViewIndex)
+ );
+ },
+
+ /**
+ * @returns true if the row at the given index is a grouped view dummy header
+ * row, false if anything else.
+ */
+ isGroupedByHeaderAtIndex(aViewIndex) {
+ if (
+ !this.dbView ||
+ aViewIndex < 0 ||
+ aViewIndex >= this.dbView.rowCount ||
+ !this.showGroupedBySort
+ ) {
+ return false;
+ }
+ return Boolean(this.dbView.getFlagsAt(aViewIndex) & MSG_VIEW_FLAG_DUMMY);
+ },
+
+ /**
+ * Perform application-level behaviors related to leaving a folder that have
+ * nothing to do with our abstraction.
+ *
+ * Things we do on leaving a folder:
+ * - Mark the folder's messages as no longer new
+ * - Mark all messages read in the folder _if so configured_.
+ */
+ onLeavingFolder() {
+ // Suppress useless InvalidateRange calls to the tree by the dbView.
+ if (this.dbView) {
+ this.dbView.suppressChangeNotifications = true;
+ }
+ this.displayedFolder.clearNewMessages();
+ this.displayedFolder.hasNewMessages = false;
+ try {
+ // For legacy reasons, we support marking all messages as read when we
+ // leave a folder based on the server type. It's this listener's job
+ // to do the legwork to figure out if this is desired.
+ //
+ // Mark all messages of aFolder as read:
+ // We can't use the command controller, because it is already tuned in to
+ // the new folder, so we just mimic its behaviour wrt
+ // goDoCommand('cmd_markAllRead').
+ if (
+ this.dbView &&
+ this.listener.shouldMarkMessagesReadOnLeavingFolder(
+ this.displayedFolder
+ )
+ ) {
+ this.dbView.doCommand(Ci.nsMsgViewCommandType.markAllRead);
+ }
+ } catch (e) {}
+ },
+
+ /**
+ * Returns the view index for this message header in this view.
+ *
+ * - If this is a single folder view, we first check whether the folder is the
+ * right one. If it is, we call the db view's findIndexOfMsgHdr. We do the
+ * first check because findIndexOfMsgHdr only checks for whether the message
+ * key matches, which might lead to false positives.
+ *
+ * - If this isn't, we trust findIndexOfMsgHdr to do the right thing.
+ *
+ * @param aMsgHdr The message header for which the view index should be
+ * returned.
+ * @param [aForceFind] If the message is not in the view and this is true, we
+ * will drop any applied view filters to look for the
+ * message. The dropping of view filters is persistent, so
+ * use with care. Defaults to false.
+ *
+ * @returns the view index for this header, or nsMsgViewIndex_None if it isn't
+ * found.
+ *
+ * @public
+ */
+ getViewIndexForMsgHdr(aMsgHdr, aForceFind) {
+ if (this.dbView) {
+ if (this.isSingleFolder && aMsgHdr.folder != this.dbView.msgFolder) {
+ return nsMsgViewIndex_None;
+ }
+
+ let viewIndex = this.dbView.findIndexOfMsgHdr(aMsgHdr, true);
+
+ if (aForceFind && viewIndex == nsMsgViewIndex_None) {
+ // Consider dropping view filters.
+ // - If we're not displaying all messages, switch to All
+ if (
+ viewIndex == nsMsgViewIndex_None &&
+ this.mailViewIndex != MailViewConstants.kViewItemAll
+ ) {
+ this.setMailView(MailViewConstants.kViewItemAll, null);
+ viewIndex = this.dbView.findIndexOfMsgHdr(aMsgHdr, true);
+ }
+
+ // - Don't just show unread only
+ if (viewIndex == nsMsgViewIndex_None) {
+ this.showUnreadOnly = false;
+ viewIndex = this.dbView.findIndexOfMsgHdr(aMsgHdr, true);
+ }
+ }
+
+ // We've done all we can.
+ return viewIndex;
+ }
+
+ // No db view, so we can't do anything
+ return nsMsgViewIndex_None;
+ },
+
+ /**
+ * Convenience function to retrieve the first nsIMsgDBHdr in any of the
+ * folders backing this view with the given message-id header. This
+ * is for the benefit of FolderDisplayWidget's selection logic.
+ * When thinking about using this, please keep in mind that, currently, this
+ * is O(n) for the total number of messages across all the backing folders.
+ * Since the folder database should already be in memory, this should
+ * ideally not involve any disk I/O.
+ * Additionally, duplicate message-ids can and will happen, but since we
+ * are using the message database's getMsgHdrForMessageID method to be fast,
+ * our semantics are limited to telling you about only the first one we find.
+ *
+ * @param aMessageId The message-id of the message you want.
+ * @returns The first nsIMsgDBHdr found in any of the underlying folders with
+ * the given message header, null if none are found. The fact that we
+ * return something does not guarantee that it is actually visible in the
+ * view. (The search may be filtering it out.)
+ */
+ getMsgHdrForMessageID(aMessageId) {
+ if (this._syntheticView) {
+ return this._syntheticView.getMsgHdrForMessageID(aMessageId);
+ }
+ if (!this._underlyingFolders) {
+ return null;
+ }
+ for (let folder of this._underlyingFolders) {
+ let msgHdr = folder.msgDatabase.getMsgHdrForMessageID(aMessageId);
+ if (msgHdr) {
+ return msgHdr;
+ }
+ }
+ return null;
+ },
+};