mirror of
https://github.com/ManchildProductions/binoc-central-mirror.git
synced 2026-06-16 10:18:26 +00:00
2059 lines
78 KiB
JavaScript
2059 lines
78 KiB
JavaScript
/* 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.EXPORTED_SYMBOLS = ['DBViewWrapper', 'IDBViewWrapperListener'];
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cr = Components.results;
|
|
var Cu = Components.utils;
|
|
|
|
Cu.import("resource:///modules/mailServices.js");
|
|
Cu.import("resource:///modules/mailViewManager.js");
|
|
Cu.import("resource:///modules/searchSpec.js");
|
|
Cu.import("resource:///modules/virtualFolderWrapper.js");
|
|
|
|
var nsMsgFolderFlags = Ci.nsMsgFolderFlags;
|
|
var nsMsgViewType = Ci.nsMsgViewType;
|
|
var nsMsgViewFlagsType = Ci.nsMsgViewFlagsType;
|
|
var nsMsgViewSortType = Ci.nsMsgViewSortType;
|
|
var nsMsgViewSortOrder = Ci.nsMsgViewSortOrder;
|
|
var nsMsgMessageFlags = Ci.nsMsgMessageFlags;
|
|
|
|
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: function FolderNotificationHelper__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:
|
|
function FolderNotificationHelper_notifyOnLoad(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: function FolderNotificationHelper_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: function FolderNotificationHelper_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: function(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: function FolderNotificationHelper_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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @return 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: function FolderNotificationHelper_haveListeners() {
|
|
for (let folderURI in this._pendingFolderUriToViewWrapperLists) {
|
|
let wrappers = this._pendingFolderUriToViewWrapperLists[folderURI];
|
|
return true;
|
|
}
|
|
for (let folderURI in this._interestedWrappers) {
|
|
let wrappers = this._interestedWrappers[folderURI];
|
|
return true;
|
|
}
|
|
return this._curiousWrappers.length != 0;
|
|
},
|
|
|
|
/* ***** Notifications ***** */
|
|
_notifyHelper: function FolderNotificationHelper__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);
|
|
},
|
|
|
|
OnItemEvent: function FolderNotificationHelper_OnItemEvent(
|
|
aFolder, aEvent) {
|
|
let eventType = aEvent.toString();
|
|
if (eventType == "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");
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
delete this._pendingFolderUriToViewWrapperLists[folderURI];
|
|
}
|
|
}
|
|
else if (eventType == "AboutToCompact") {
|
|
this._notifyHelper(aFolder, "_aboutToCompactFolder");
|
|
}
|
|
else if (eventType == "CompactCompleted") {
|
|
this._notifyHelper(aFolder, "_compactedFolder");
|
|
}
|
|
else if (eventType == "DeleteOrMoveMsgCompleted") {
|
|
this._notifyHelper(aFolder, "_deleteCompleted");
|
|
}
|
|
else if (eventType == "DeleteOrMoveMsgFailed") {
|
|
this._notifyHelper(aFolder, "_deleteFailed");
|
|
}
|
|
else if (eventType == "RenameCompleted") {
|
|
this._notifyHelper(aFolder, "_renameCompleted");
|
|
}
|
|
|
|
},
|
|
|
|
OnItemIntPropertyChanged: function(aFolder, aProperty, aOldValue, aNewValue) {
|
|
let propertyString = aProperty.toString();
|
|
if ((propertyString == "TotalMessages") ||
|
|
(propertyString == "TotalUnreadMessages"))
|
|
this._notifyHelper(aFolder, "_messageCountsChanged");
|
|
},
|
|
|
|
_folderMoveHelper: function(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: function FolderNotificationHelper_folderRenamed(aOrigFolder,
|
|
aNewFolder) {
|
|
this._folderMoveHelper(aOrigFolder, aNewFolder);
|
|
},
|
|
|
|
folderMoveCopyCompleted:
|
|
function FolderNotificationHelper_folderMoveCopyCompleted(aMove,
|
|
aSrcFolder,
|
|
aDestFolder) {
|
|
if (aMove) {
|
|
let aNewFolder = aDestFolder.getChildNamed(aSrcFolder.prettyName);
|
|
this._folderMoveHelper(aSrcFolder, aNewFolder);
|
|
}
|
|
},
|
|
|
|
folderDeleted: function FolderNotificationHelper_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?)
|
|
* @return true if we should mark all the dudes as read, false if not.
|
|
*/
|
|
shouldMarkMessagesReadOnLeavingFolder: function (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: function (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: function (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: function() {
|
|
},
|
|
|
|
/**
|
|
* 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: function(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: function(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: function() {
|
|
},
|
|
|
|
/**
|
|
* Generated when we are leaving a folder.
|
|
*/
|
|
onLeavingFolder: function() {
|
|
},
|
|
|
|
/**
|
|
* 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: function(aAll) {
|
|
},
|
|
|
|
/**
|
|
* The mail view changed. The mail view widget is likely to care about this.
|
|
*/
|
|
onMailViewChanged: function() {
|
|
},
|
|
|
|
/**
|
|
* 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: function() {
|
|
},
|
|
|
|
/**
|
|
* 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: function () {
|
|
},
|
|
|
|
/**
|
|
* 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: function () {
|
|
},
|
|
|
|
/**
|
|
* The total message count or total unread message counts changed.
|
|
*/
|
|
onMessageCountsChanged: function () {
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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,
|
|
|
|
/**
|
|
* @return 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;
|
|
},
|
|
|
|
/**
|
|
* @return 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);
|
|
},
|
|
|
|
/**
|
|
* @return 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;
|
|
},
|
|
|
|
/**
|
|
* 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: function DBViewWrapper_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: function DBViewWrapper_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: function DBViewWrapper__nullFolderDatabase(aFolder) {
|
|
if (!aFolder.isSpecialFolder(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: function DBViewWrapper_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(Components.interfaces.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 aFolderIsDead 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: function DBViewWrapper_close(aFolderIsDead) {
|
|
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 (!aFolderIsDead) {
|
|
// 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._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.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: function DBViewWrapper_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;
|
|
}
|
|
|
|
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: function DBViewWrapper_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: function DBViewWrapper_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 = [[nsMsgViewSortType.byNone, nsMsgViewSortOrder.ascending]];
|
|
|
|
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.
|
|
*
|
|
* @return true if the folder should be shown immediately, false if we should
|
|
* wait for updateFolder to complete.
|
|
*/
|
|
shouldShowMessagesForFolderImmediately:
|
|
function DBViewWrapper_showShowMessagesForFolderImmediately() {
|
|
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:
|
|
function DBViewWrapper_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 & 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 & nsMsgViewFlagsType.kGroupBySort) {
|
|
this.__viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
|
|
this.__viewFlags |= 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 == nsMsgViewType.eShowThreadsWithUnread) ||
|
|
(viewType == nsMsgViewType.eShowWatchedThreadsWithUnread))
|
|
this._setSpecialView(viewType);
|
|
|
|
// - retrieve virtual folder configuration
|
|
if (aFolder.flags & nsMsgFolderFlags.Virtual) {
|
|
let virtFolder = VirtualFolderHelper.wrapVirtualFolder(aFolder);
|
|
// 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);
|
|
}
|
|
// 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.
|
|
else {
|
|
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: function DBViewWrapper__createView() {
|
|
let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=";
|
|
|
|
// we will have saved these off when closing our view
|
|
let viewFlags = this.__viewFlags || 0;
|
|
|
|
// 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";
|
|
}
|
|
// if we're dealing with virtual folders, the answer is always an xfvf
|
|
else if (this._underlyingData == this.kUnderlyingMultipleFolder) {
|
|
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);
|
|
// use the least-specific sort so we can clock them back through to build up
|
|
// the correct sort order...
|
|
let [sortType, sortOrder, sortCustomCol] =
|
|
this._getSortDetails(this._sort.length-1);
|
|
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;
|
|
}
|
|
// 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
|
|
else {
|
|
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);
|
|
|
|
// 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: function DBViewWrapper__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();
|
|
this.listener.onMessagesLoaded(true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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: function DBViewWrapper__enterFolder() {
|
|
if (this._enteredFolder)
|
|
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: function DBViewWrapper__folderMoved(aOldFolder, aNewFolder) {
|
|
if (aOldFolder == this.displayedFolder)
|
|
this.displayedFolder = aNewFolder;
|
|
|
|
// indexOf doesn't work for this (reliably)
|
|
for (let [i, underlyingFolder] of this._underlyingFolders.entries()) {
|
|
if (aOldFolder == underlyingFolder) {
|
|
this._underlyingFolders[i] = aNewFolder;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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: function DBViewWrapper__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(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: function DBViewWrapper__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: function DBViewWrapper__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: function DBViewWrapper__deleteCompleted(aFolder) {
|
|
if (this.dbView)
|
|
this.dbView.onDeleteCompleted(true);
|
|
this.listener.onMessagesRemoved();
|
|
},
|
|
|
|
/**
|
|
* See _deleteCompleted for an explanation of what is going on.
|
|
*/
|
|
_deleteFailed: function DBViewWrapper__deleteFailed(aFolder) {
|
|
if (this.dbView)
|
|
this.dbView.onDeleteCompleted(false);
|
|
this.listener.onMessageRemovalFailed();
|
|
},
|
|
|
|
_forceOpen: function DBViewWrapper__forceOpen(aFolder) {
|
|
this.displayedFolder = null;
|
|
this.open(aFolder);
|
|
},
|
|
|
|
_renameCompleted: function DBViewWrapper__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: function DBViewWrapper__messageCountsChanged(aFolder) {
|
|
if (aFolder == this.displayedFolder)
|
|
this.listener.onMessageCountsChanged();
|
|
},
|
|
|
|
/**
|
|
* @return 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 & nsMsgViewFlagsType.kThreadedDisplay &&
|
|
!(changedFlags & 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 & (nsMsgViewFlagsType.kGroupBySort |
|
|
nsMsgViewFlagsType.kUnreadOnly |
|
|
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: function DBViewWrapper__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] in Iterator(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 & nsMsgFolderFlags.Mail));
|
|
},
|
|
|
|
get isNewsFolder() {
|
|
return Boolean(this.displayedFolder &&
|
|
(this.displayedFolder.flags & nsMsgFolderFlags.Newsgroup));
|
|
},
|
|
|
|
get isFeedFolder() {
|
|
return Boolean(this.displayedFolder &&
|
|
this.displayedFolder.server.type == "rss");
|
|
},
|
|
|
|
OUTGOING_FOLDER_FLAGS: nsMsgFolderFlags.SentMail |
|
|
nsMsgFolderFlags.Drafts |
|
|
nsMsgFolderFlags.Queue |
|
|
nsMsgFolderFlags.Templates,
|
|
/**
|
|
* @return 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);
|
|
},
|
|
/**
|
|
* @return 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 & 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: function DBViewWrapper_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: function DBViewWrapper_endViewUpdate(aForceLevel) {
|
|
if (--this._viewUpdateDepth == 0)
|
|
this._applyViewChanges();
|
|
// Avoid pathological situations.
|
|
if (this._viewUpdateDepth < 0)
|
|
this._viewUpdateDepth = 0;
|
|
},
|
|
|
|
/**
|
|
* @return the primary sort type (as one of the numeric constants from
|
|
* nsMsgViewSortType).
|
|
*/
|
|
get primarySortType() {
|
|
return this._sort[0][0];
|
|
},
|
|
|
|
/**
|
|
* @return the primary sort order (as one of the numeric constants from
|
|
* nsMsgViewSortOrder.)
|
|
*/
|
|
get primarySortOrder() {
|
|
return this._sort[0][1];
|
|
},
|
|
|
|
/**
|
|
* @return true if the dominant sort is ascending.
|
|
*/
|
|
get isSortedAscending() {
|
|
return this._sort.length &&
|
|
this._sort[0][1] == nsMsgViewSortOrder.ascending;
|
|
},
|
|
/**
|
|
* @return true if the dominant sort is descending.
|
|
*/
|
|
get isSortedDescending() {
|
|
return this._sort.length &&
|
|
this._sort[0][1] == nsMsgViewSortOrder.descending;
|
|
},
|
|
/**
|
|
* Indicate if we are sorting by time or something correlated with time.
|
|
*
|
|
* @return true if the dominant sort is by time.
|
|
*/
|
|
get sortImpliesTemporalOrdering() {
|
|
if (!this._sort.length)
|
|
return false;
|
|
let sortType = this._sort[0][0];
|
|
return sortType == nsMsgViewSortType.byDate ||
|
|
sortType == nsMsgViewSortType.byReceived ||
|
|
sortType == nsMsgViewSortType.byId ||
|
|
sortType == nsMsgViewSortType.byThread;
|
|
},
|
|
|
|
sortAscending: function() {
|
|
if (!this.isSortedAscending)
|
|
this.magicSort(this._sort[0][0], nsMsgViewSortOrder.ascending);
|
|
},
|
|
sortDescending: function() {
|
|
if (!this.isSortedDescending)
|
|
this.magicSort(this._sort[0][0], 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: function DBViewWrapper_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.
|
|
*
|
|
* @return [sort type, sort order, sort custom column name]
|
|
*/
|
|
_getSortDetails: function(aIndex) {
|
|
let [sortType, sortOrder] = this._sort[aIndex];
|
|
let sortCustomColumn = null;
|
|
let sortTypeType = typeof(sortType);
|
|
if (sortTypeType != "number") {
|
|
sortCustomColumn = (sortTypeType == "string") ? sortType : sortType.id;
|
|
sortType = 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: function DBViewWrapper_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 != nsMsgViewSortType.byNone &&
|
|
(this.dbView.secondarySortType != sortType ||
|
|
(this.dbView.secondarySortType == 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: function DBViewWrapper_ensureValidSort(aViewFlags) {
|
|
if ((aViewFlags != null ? aViewFlags : this._viewFlags) &
|
|
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 == nsMsgViewSortType.byThread ||
|
|
sortType == nsMsgViewSortType.byId ||
|
|
sortType == nsMsgViewSortType.byNone ||
|
|
sortType == nsMsgViewSortType.bySize) {
|
|
this._sort = [[nsMsgViewSortType.byDate, this._sort[0][1]]];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @return 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 & 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 |
|
|
nsMsgViewFlagsType.kGroupBySort |
|
|
nsMsgViewFlagsType.kExpandAll |
|
|
nsMsgViewFlagsType.kThreadedDisplay;
|
|
this._ensureValidSort(viewFlags);
|
|
this._viewFlags = viewFlags;
|
|
}
|
|
// maybe we shouldn't do anything in this case?
|
|
else
|
|
this._viewFlags &= ~(nsMsgViewFlagsType.kGroupBySort |
|
|
nsMsgViewFlagsType.kThreadedDisplay);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Are we showing ignored/killed threads?
|
|
*/
|
|
get showIgnored() {
|
|
return Boolean(this._viewFlags & nsMsgViewFlagsType.kShowIgnored);
|
|
},
|
|
/**
|
|
* Set whether we are showing ignored/killed threads.
|
|
*/
|
|
set showIgnored(aShowIgnored) {
|
|
if (this.showIgnored == aShowIgnored)
|
|
return;
|
|
|
|
if (aShowIgnored)
|
|
this._viewFlags |= nsMsgViewFlagsType.kShowIgnored;
|
|
else
|
|
this._viewFlags &= ~nsMsgViewFlagsType.kShowIgnored;
|
|
},
|
|
|
|
/**
|
|
* @return true if we are in threaded display (as opposed to grouped or
|
|
* unthreaded.)
|
|
*/
|
|
get showThreaded() {
|
|
return (this._viewFlags & nsMsgViewFlagsType.kThreadedDisplay) &&
|
|
!(this._viewFlags & 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 |= nsMsgViewFlagsType.kThreadedDisplay;
|
|
// maybe we shouldn't do anything in this case?
|
|
else
|
|
viewFlags &= ~nsMsgViewFlagsType.kThreadedDisplay;
|
|
// lose the group bit...
|
|
viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
|
|
this._viewFlags = viewFlags;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @return true if we are in unthreaded mode (which means not threaded and
|
|
* not grouped by sort).
|
|
*/
|
|
get showUnthreaded() {
|
|
return Boolean(!(this._viewFlags & (nsMsgViewFlagsType.kGroupBySort |
|
|
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 &= ~(nsMsgViewFlagsType.kGroupBySort |
|
|
nsMsgViewFlagsType.kThreadedDisplay);
|
|
// maybe we shouldn't do anything in this case?
|
|
else
|
|
this._viewFlags = (this._viewFlags & ~nsMsgViewFlagsType.kGroupBySort) |
|
|
nsMsgViewFlagsType.kThreadedDisplay;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @return true if we are showing only unread messages.
|
|
*/
|
|
get showUnreadOnly() {
|
|
return Boolean(this._viewFlags & 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 |= nsMsgViewFlagsType.kUnreadOnly;
|
|
else
|
|
this._viewFlags &= ~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: function DBViewWrapper__setSpecialView(aViewEnum) {
|
|
// special views simply cannot work for virtual folders. explode.
|
|
if (this.isVirtual)
|
|
throw new Exception("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();
|
|
},
|
|
/**
|
|
* @return true if the special view that shows threads with unread messages
|
|
* in them is active.
|
|
*/
|
|
get specialViewThreadsWithUnread() {
|
|
return this._specialView == 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(nsMsgViewType.eShowThreadsWithUnread);
|
|
},
|
|
/**
|
|
* @return true if the special view that shows watched threads with unread
|
|
* messages in them is active.
|
|
*/
|
|
get specialViewWatchedThreadsWithUnread() {
|
|
return this._specialView == 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(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: function DBViewWrapper_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) : "");
|
|
}
|
|
}
|
|
|
|
// we don't need to notify the view picker to update because the makeActive
|
|
// that cascades out of the view update will do it for us.
|
|
},
|
|
|
|
/**
|
|
* @return true if the row at the given index contains a collapsed thread,
|
|
* false if the row is a collapsed group or anything else.
|
|
*/
|
|
isCollapsedThreadAtIndex:
|
|
function DBViewWrapper_isCollapsedThreadAtIndex(aViewIndex) {
|
|
let flags = this.dbView.getFlagsAt(aViewIndex);
|
|
return (flags & nsMsgMessageFlags.Elided) &&
|
|
!(flags & MSG_VIEW_FLAG_DUMMY) &&
|
|
this.dbView.isContainer(aViewIndex);
|
|
},
|
|
|
|
/**
|
|
* @return true if the row at the given index is a grouped view dummy header
|
|
* row, false if anything else.
|
|
*/
|
|
isGroupedByHeaderAtIndex: function(aViewIndex) {
|
|
if (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: function DBViewWrapper_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){/* ignore */}
|
|
},
|
|
|
|
/**
|
|
* 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: function DBViewWrapper_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.
|
|
* @return 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: function DBViewWrapper_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;
|
|
},
|
|
};
|