/* 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/. */

/* import-globals-from SearchDialog.js */

/* globals ViewPickerBinding */ // From msgViewPickerOverlay.js

/* TODO: Now used exclusively in SearchDialog.xhtml. Needs dead code removal. */

var { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

ChromeUtils.defineESModuleGetters(this, {
  TreeSelection: "chrome://messenger/content/tree-selection.mjs",
});

XPCOMUtils.defineLazyModuleGetters(this, {
  DBViewWrapper: "resource:///modules/DBViewWrapper.jsm",
});

var gDBView;
var nsMsgKey_None = 0xffffffff;
var nsMsgViewIndex_None = 0xffffffff;

/**
 * Maintains a list of listeners for all FolderDisplayWidget instances in this
 *  window.  The assumption is that because of our multiplexed tab
 *  implementation all consumers are effectively going to care about all such
 *  tabs.
 *
 * We are not just a global list so that we can add brains about efficiently
 *  building lists, provide try-wrapper convenience, etc.
 */
var FolderDisplayListenerManager = {
  _listeners: [],

  /**
   * Register a listener that implements one or more of the methods defined on
   *  |IDBViewWrapperListener|.  Note that a change from those interface
   *  signatures is that the first argument is always a reference to the
   *  FolderDisplayWidget generating the notification.
   *
   * We additionally support the following notifications:
   * - onMakeActive.  Invoked when makeActive is called on the
   *   FolderDisplayWidget.  The second argument (after the folder display) is
   *   aWasInactive.
   *
   * - onActiveCreatedView.  onCreatedView deferred to when the tab is actually
   *   made active.
   *
   * - onActiveMessagesLoaded.  onMessagesLoaded deferred to when the
   *   tab is actually made active.  Use this if the actions you need to take
   *   are based on the folder display actually being visible, such as updating
   *   some UI widget, etc. Not all messages may have been loaded, but some.
   *
   */
  registerListener(aListener) {
    this._listeners.push(aListener);
  },

  /**
   * Unregister a previously registered event listener.
   */
  unregisterListener(aListener) {
    let idx = this._listeners.indexOf(aListener);
    if (idx >= 0) {
      this._listeners.splice(idx, 1);
    }
  },

  /**
   * For use by FolderDisplayWidget to trigger listener invocation.
   */
  _fireListeners(aEventName, aArgs) {
    for (let listener of this._listeners) {
      if (aEventName in listener) {
        try {
          listener[aEventName].apply(listener, aArgs);
        } catch (e) {
          console.error(
            aEventName + " event listener FAILED; " + e + " at: " + e.stack
          );
        }
      }
    }
  },
};

/**
 * Abstraction for a widget that (roughly speaking) displays the contents of
 *  folders.  The widget belongs to a tab and has a lifetime as long as the tab
 *  that contains it.  This class is strictly concerned with the UI aspects of
 *  this; the DBViewWrapper class handles the view details (and is exposed on
 *  the 'view' attribute.)
 *
 * The search window subclasses this into the SearchFolderDisplayWidget rather
 *  than us attempting to generalize everything excessively.  This is because
 *  we hate the search window and don't want to clutter up this code for it.
 * The standalone message display window also subclasses us; we do not hate it,
 *  but it's not invited to our birthday party either.
 * For reasons of simplicity and the original order of implementation, this
 *  class does alter its behavior slightly for the benefit of the standalone
 *  message window.  If no tab info is provided, we avoid touching tabmail
 *  (which is good, because it won't exist!)  And now we guard against treeBox
 *  manipulations...
 */
function FolderDisplayWidget() {
  // If the folder does not get handled by the DBViewWrapper, stash it here.
  //  ex: when isServer is true.
  this._nonViewFolder = null;

  this.view = new DBViewWrapper(this);

  /**
   * The XUL tree node, as retrieved by getDocumentElementById.  The caller is
   *  responsible for setting this.
   */
  this.tree = null;

  /**
   * The nsIMsgWindow corresponding to the window that holds us.  There is only
   *  one of these per tab.  The caller is responsible for setting this.
   */
  this.msgWindow = null;
  /**
   * The nsIMessenger instance that corresponds to our tab/window.  We do not
   *  use this ourselves, but are responsible for using it to update the
   *  global |messenger| object so that our tab maintains its own undo and
   *  navigation history.  At some point we might touch it for those reasons.
   */
  this.messenger = null;
  this.threadPaneCommandUpdater = this;

  /**
   * Flag to expose whether all messages are loaded or not.  Set by
   *  onMessagesLoaded() when aAll is true.
   */
  this._allMessagesLoaded = false;

  /**
   * Save the top row displayed when we go inactive, restore when we go active,
   *  nuke it when we destroy the view.
   */
  this._savedFirstVisibleRow = null;
  /** the next view index to select once the delete completes */
  this._nextViewIndexAfterDelete = null;
  /**
   * Track when a mass move is in effect (we get told by hintMassMoveStarting,
   *  and hintMassMoveCompleted) so that we can avoid deletion-triggered
   *  moving to _nextViewIndexAfterDelete until the mass move completes.
   */
  this._massMoveActive = false;
  /**
   * Track when a message is being deleted so we can respond appropriately.
   */
  this._deleteInProgress = false;

  /**
   * Used by pushNavigation to queue a navigation request for when we enter the
   *  next folder; onMessagesLoaded(true) is the one that processes it.
   */
  this._pendingNavigation = null;

  this._active = false;
  /**
   * A list of methods to call on 'this' object when we are next made active.
   *  This list is populated by calls to |_notifyWhenActive| when we are
   *  not active at the moment.
   */
  this._notificationsPendingActivation = [];

  this._mostRecentSelectionCounts = [];
  this._mostRecentCurrentIndices = [];
}
FolderDisplayWidget.prototype = {
  /**
   * @returns the currently displayed folder.  This is just proxied from the
   *     view wrapper.
   * @groupName Displayed
   */
  get displayedFolder() {
    return this._nonViewFolder || this.view.displayedFolder;
  },

  /**
   * @returns true if the selection should be summarized for this folder. This
   *     is based on the mail.operate_on_msgs_in_collapsed_threads pref and
   *     if we are in a newsgroup folder. XXX When bug 478167 is fixed, this
   *     should be limited to being disabled for newsgroups that are not stored
   *     offline.
   */
  get summarizeSelectionInFolder() {
    return (
      Services.prefs.getBoolPref("mail.operate_on_msgs_in_collapsed_threads") &&
      !(this.displayedFolder instanceof Ci.nsIMsgNewsFolder)
    );
  },

  /**
   * @returns the nsITreeSelection object for our tree view.  This exists for
   *     the benefit of message tabs that haven't been switched to yet.
   *     We provide a fake tree selection in those cases.
   * @protected
   */
  get treeSelection() {
    // If we haven't switched to this tab yet, dbView will exist but
    // dbView.selection won't, so use the fake tree selection instead.
    if (this.view.dbView) {
      return this.view.dbView.selection;
    }
    return null;
  },

  /**
   * Determine which pane currently has focus (one of the folder pane, thread
   * pane, or message pane). The message pane node is the common ancestor of
   * the single- and multi-message content windows. When changing focus to the
   * message pane, be sure to focus the appropriate content window in addition
   * to the messagepanebox (doing both is required in order to blur the
   * previously-focused chrome element).
   *
   * @returns the focused pane
   */
  get focusedPane() {
    let panes = ["threadTree", "folderTree", "messagepanebox"].map(id =>
      document.getElementById(id)
    );

    let currentNode = top.document.activeElement;

    while (currentNode) {
      if (panes.includes(currentNode)) {
        return currentNode;
      }

      currentNode = currentNode.parentNode;
    }
    return null;
  },

  /**
   * Number of headers to tell the message database to cache when we enter a
   *  folder.  This value is being propagated from legacy code which provided
   *  no explanation for its choice.
   *
   * We definitely want the header cache size to be larger than the number of
   *  rows that can be displayed on screen simultaneously.
   *
   * @private
   */
  PERF_HEADER_CACHE_SIZE: 100,

  /**
   * @name Selection Persistence
   * @private
   */
  // @{

  /**
   * An optional object, with the following properties:
   * - messages: This is a list where each item is an object with the following
   *       attributes sufficient to re-establish the selected items even in the
   *       face of folder renaming.
   *   - messageId: The value of the message's message-id header.
   *
   * That's right, we only save the message-id header value.  This is arguably
   *  overkill and ambiguous in the face of duplicate messages, but it's the
   *  most persistent/reliable thing we have without gloda.
   * Using the view index was ruled out because it is hardly stable.  Using the
   *  message key alone is insufficient for cross-folder searches.  Using a
   *  folder identifier and message key is insufficient for local folders in the
   *  face of compaction, let alone complexities where the folder name may
   *  change due to renaming/moving.  Which means we eventually need to fall
   *  back to message-id anyways.  Feel free to add in lots of complexity if
   *  you actually write unit tests for all the many possible cases.
   * Additional justification is that selection saving/restoration should not
   *  happen all that frequently.  A nice freebie is that message-id is
   *  definitely persistable.
   *
   * - forceSelect: Whether we are allowed to drop all filters in our quest to
   *       select messages.
   */
  _savedSelection: null,

  /**
   * Save the current view selection for when we the view is getting destroyed
   *  or otherwise re-ordered in such a way that the nsITreeSelection will lose
   *  track of things (because it just has a naive view-index 'view' of the
   *  world.)  We just save each message's message-id header.  This is overkill
   *  and ambiguous in the face of duplicate messages (and expensive to
   *  restore), but is also the most reliable option for this use case.
   */
  _saveSelection() {
    this._savedSelection = {
      messages: this.selectedMessages.map(msgHdr => ({
        messageId: msgHdr.messageId,
      })),
      forceSelect: false,
    };
  },

  /**
   * Clear the saved selection.
   */
  _clearSavedSelection() {
    this._savedSelection = null;
  },

  /**
   * Restore the view selection if we have a saved selection.  We must be
   *  active!
   *
   * @returns true if we were able to restore the selection and there was
   *     a selection, false if there was no selection (anymore).
   */
  _restoreSelection() {
    if (!this._savedSelection || !this._active) {
      return false;
    }

    // translate message IDs back to messages.  this is O(s(m+n)) where:
    // - s is the number of messages saved in the selection
    // - m is the number of messages in the view (from findIndexOfMsgHdr)
    // - n is the number of messages in the underlying folders (from
    //   DBViewWrapper.getMsgHdrForMessageID).
    // which ends up being O(sn)
    let messages = this._savedSelection.messages
      .map(savedInfo => this.view.getMsgHdrForMessageID(savedInfo.messageId))
      .filter(msgHdr => !!msgHdr);

    this.selectMessages(messages, this._savedSelection.forceSelect, true);
    this._savedSelection = null;

    return this.selectedCount != 0;
  },

  /**
   * Restore the last expandAll/collapseAll state, for both grouped and threaded
   * views. Not all views respect viewFlags, ie single folder non-virtual.
   */
  restoreThreadState() {
    if (!this._active || !this.tree || !this.view.dbView.viewFolder) {
      return;
    }

    if (
      this.view._threadExpandAll &&
      !(this.view.dbView.viewFlags & Ci.nsMsgViewFlagsType.kExpandAll)
    ) {
      this.view.dbView.doCommand(Ci.nsMsgViewCommandType.expandAll);
    }
    if (
      !this.view._threadExpandAll &&
      this.view.dbView.viewFlags & Ci.nsMsgViewFlagsType.kExpandAll
    ) {
      this.view.dbView.doCommand(Ci.nsMsgViewCommandType.collapseAll);
    }
  },
  // @}

  /**
   * @name Columns
   * @protected
   */
  // @{

  /**
   * The map of all stock sortable columns and their sortType. The key must
   * match the column's xul <treecol> id.
   */
  COLUMNS_MAP: new Map([
    ["accountCol", "byAccount"],
    ["attachmentCol", "byAttachments"],
    ["senderCol", "byAuthor"],
    ["correspondentCol", "byCorrespondent"],
    ["dateCol", "byDate"],
    ["flaggedCol", "byFlagged"],
    ["idCol", "byId"],
    ["junkStatusCol", "byJunkStatus"],
    ["locationCol", "byLocation"],
    ["priorityCol", "byPriority"],
    ["receivedCol", "byReceived"],
    ["recipientCol", "byRecipient"],
    ["sizeCol", "bySize"],
    ["statusCol", "byStatus"],
    ["subjectCol", "bySubject"],
    ["tagsCol", "byTags"],
    ["threadCol", "byThread"],
    ["unreadButtonColHeader", "byUnread"],
  ]),

  /**
   * The map of stock non-sortable columns. The key must match the column's
   *  xul <treecol> id.
   */
  COLUMNS_MAP_NOSORT: new Set([
    "selectCol",
    "totalCol",
    "unreadCol",
    "deleteCol",
  ]),

  /**
   * The set of potential default columns in their default display order.  Each
   *  column in this list is checked against |COLUMN_DEFAULT_TESTERS| to see if
   *  it is actually an appropriate default for the folder type.
   */
  DEFAULT_COLUMNS: [
    "threadCol",
    "attachmentCol",
    "flaggedCol",
    "subjectCol",
    "unreadButtonColHeader",
    "senderCol", // news folders or incoming folders when correspondents not in use
    "recipientCol", // outgoing folders when correspondents not in use
    "correspondentCol", // mail folders
    "junkStatusCol",
    "dateCol",
    "locationCol", // multiple-folder backed folders
  ],

  /**
   * Maps column ids to functions that test whether the column is a good default
   *  for display for the folder.  Each function should expect a DBViewWrapper
   *  instance as its argument.  The intent is that the various helper
   *  properties like isMailFolder/isIncomingFolder/isOutgoingFolder allow the
   *  constraint to be expressed concisely.  If a helper does not exist, add
   *  one! (If doing so is out of reach, than access viewWrapper.displayedFolder
   *  to get at the nsIMsgFolder.)
   * If a column does not have a function, it is assumed that it should be
   *  displayed by default.
   */
  COLUMN_DEFAULT_TESTERS: {
    correspondentCol(viewWrapper) {
      if (Services.prefs.getBoolPref("mail.threadpane.use_correspondents")) {
        // Don't show the correspondent for news or RSS where it doesn't make sense.
        return viewWrapper.isMailFolder && !viewWrapper.isFeedFolder;
      }
      return false;
    },
    senderCol(viewWrapper) {
      if (Services.prefs.getBoolPref("mail.threadpane.use_correspondents")) {
        // Show the sender even if correspondent is enabled for news and feeds.
        return viewWrapper.isNewsFolder || viewWrapper.isFeedFolder;
      }
      // senderCol = From. You only care in incoming folders.
      return viewWrapper.isIncomingFolder;
    },
    recipientCol(viewWrapper) {
      if (Services.prefs.getBoolPref("mail.threadpane.use_correspondents")) {
        // No recipient column if we use correspondent.
        return false;
      }
      // recipientCol = To. You only care in outgoing folders.
      return viewWrapper.isOutgoingFolder;
    },
    // Only show the location column for non-single-folder results
    locationCol(viewWrapper) {
      return !viewWrapper.isSingleFolder;
    },
    // core UI does not provide an ability to mark newsgroup messages as spam
    junkStatusCol(viewWrapper) {
      return !viewWrapper.isNewsFolder;
    },
  },

  /**
   * The property name we use to store the column states on the
   *  dbFolderInfo.
   */
  PERSISTED_COLUMN_PROPERTY_NAME: "columnStates",

  /**
   * Given a dbFolderInfo, extract the persisted state from it if there is any.
   *
   * @returns null if there was no persisted state, the persisted state in object
   *     form otherwise.  (Ideally the state conforms to the documentation on
   *     |_savedColumnStates| but we can't stop people from doing bad things.)
   */
  _depersistColumnStatesFromDbFolderInfo(aDbFolderInfo) {
    let columnJsonString = aDbFolderInfo.getCharProperty(
      this.PERSISTED_COLUMN_PROPERTY_NAME
    );
    if (!columnJsonString) {
      return null;
    }

    return JSON.parse(columnJsonString);
  },

  /**
   * Persist the column state for the currently displayed folder.  We are
   *  assuming that the message database is already open when we are called and
   *  therefore that we do not need to worry about cleaning up after the message
   *  database.
   * The caller should only call this when they have reason to suspect that the
   *  column state has been changed.  This could be because there was no
   *  persisted state so we figured out a default one and want to save it.
   *  Otherwise this should be because the user explicitly changed up the column
   *  configurations.  You should not call this willy-nilly.
   *
   * @param aState State to persist.
   */
  _persistColumnStates(aState) {
    if (this.view.isSynthetic) {
      let syntheticView = this.view._syntheticView;
      if ("setPersistedSetting" in syntheticView) {
        syntheticView.setPersistedSetting("columns", aState);
      }
      return;
    }

    if (!this.view.displayedFolder || !this.view.displayedFolder.msgDatabase) {
      return;
    }

    let msgDatabase = this.view.displayedFolder.msgDatabase;
    let dbFolderInfo = msgDatabase.dBFolderInfo;
    dbFolderInfo.setCharProperty(
      this.PERSISTED_COLUMN_PROPERTY_NAME,
      JSON.stringify(aState)
    );
    msgDatabase.commit(Ci.nsMsgDBCommitType.kLargeCommit);
  },

  /**
   * Let us know that the state of the columns has changed.  This is either due
   *  to a re-ordering or hidden-ness being toggled.
   *
   * This method should only be called on (the active) gFolderDisplay.
   */
  hintColumnsChanged() {
    // ignore this if we are the ones doing things
    if (this._touchingColumns) {
      return;
    }
    this._persistColumnStates(this.getColumnStates());
  },

  /**
   * Either inherit the column state of another folder or use heuristics to
   *  figure out the best column state for the current folder.
   */
  _getDefaultColumnsForCurrentFolder(aDoNotInherit) {
    // If the view is synthetic, try asking it for its default columns. If it
    // fails, just return nothing, since most synthetic views don't care about
    // columns anyway.
    if (this.view.isSynthetic) {
      if ("getDefaultSetting" in this.view._syntheticView) {
        return this.view._syntheticView.getDefaultSetting("columns");
      }
      return {};
    }

    // do not inherit from the inbox if:
    // - It's an outgoing folder; these have a different use-case and there
    //    should be a small number of these, so it's okay to have no defaults.
    // - It's a virtual folder (single or multi-folder backed).  Who knows what
    //    the intent of the user is in this case.  This should also be bounded
    //    in number and our default heuristics should be pretty good.
    // - It's a multiple folder; this is either a search view (which has no
    //   displayed folder) or a virtual folder (which we eliminated above).
    // - News folders.  There is no inbox so there's nothing to inherit from.
    //    (Although we could try and see if they have opened any other news
    //    folders in the same account.  But it's not all that important to us.)
    // - It's an inbox!
    let doNotInherit =
      aDoNotInherit ||
      this.view.isOutgoingFolder ||
      this.view.isVirtual ||
      this.view.isMultiFolder ||
      this.view.isNewsFolder ||
      this.displayedFolder.getFlag(Ci.nsMsgFolderFlags.Inbox);

    // Try and grab the inbox for this account's settings.  we may not be able
    //  to, in which case we just won't inherit.  (It ends up the same since the
    //  inbox is obviously not customized in this case.)
    if (!doNotInherit) {
      let inboxFolder = this.displayedFolder.rootFolder.getFolderWithFlags(
        Ci.nsMsgFolderFlags.Inbox
      );
      if (inboxFolder) {
        let state = this._depersistColumnStatesFromDbFolderInfo(
          inboxFolder.msgDatabase.dBFolderInfo
        );
        // inbox message databases don't get closed as a matter of policy.

        if (state) {
          return state;
        }
      }
    }

    // if we are still here, use the defaults and helper functions
    let state = {};
    for (let colId of this.DEFAULT_COLUMNS) {
      let shouldShowColumn = true;
      if (colId in this.COLUMN_DEFAULT_TESTERS) {
        // This is potentially going to be used by extensions; avoid them
        //  killing us.
        try {
          shouldShowColumn = this.COLUMN_DEFAULT_TESTERS[colId](this.view);
        } catch (ex) {
          shouldShowColumn = false;
          console.error(ex);
        }
      }
      state[colId] = { visible: shouldShowColumn };
    }
    return state;
  },

  /**
   * Is setColumnStates messing with the columns' DOM?  This is used by
   *  hintColumnsChanged to avoid wasteful state persistence.
   */
  _touchingColumns: false,

  /**
   * Set the column states of this FolderDisplay to the provided state.
   *
   * @param aColumnStates an object of the form described on
   *     |_savedColumnStates|.  If ordinal attributes are omitted then no
   *     re-ordering will be performed.  This is intentional, but potentially a
   *     bad idea.  (Right now only gloda search underspecifies ordinals.)
   * @param [aPersistChanges=false] Should we persist the changes to the view?
   *     This only has an effect if we are active.
   *
   * @public
   */
  setColumnStates(aColumnStates, aPersistChanges) {
    // If we are not active, just overwrite our current state with the provided
    //  state and bail.
    if (!this._active) {
      this._savedColumnStates = aColumnStates;
      return;
    }

    this._touchingColumns = true;

    try {
      let cols = document.getElementById("threadCols");
      let colChildren = cols.children;

      for (let iKid = 0; iKid < colChildren.length; iKid++) {
        let colChild = colChildren[iKid];
        if (colChild == null) {
          continue;
        }

        // We only care about treecols.  The splitters do not need to be marked
        //  hidden or un-hidden.
        if (colChild.tagName == "treecol") {
          // if it doesn't have preserved state it should be hidden
          let shouldBeHidden = true;
          // restore state
          if (colChild.id in aColumnStates) {
            let colState = aColumnStates[colChild.id];
            if ("visible" in colState) {
              shouldBeHidden = !colState.visible;
            }
            if ("ordinal" in colState && colChild.ordinal != colState.ordinal) {
              colChild.ordinal = colState.ordinal;
            }
          }
          let isHidden = colChild.hidden;
          if (isHidden != shouldBeHidden) {
            if (shouldBeHidden) {
              colChild.setAttribute("hidden", "true");
            } else {
              colChild.removeAttribute("hidden");
            }
          }
        }
      }
    } finally {
      this._touchingColumns = false;
    }

    if (aPersistChanges) {
      this.hintColumnsChanged();
    }
  },

  /**
   * A dictionary that maps column ids to dictionaries where each dictionary
   *  has the following fields:
   * - visible: Is the column visible.
   * - ordinal: The 1-based XUL 'ordinal' value assigned to the column.  This
   *    corresponds to the position but is not something you want to manipulate.
   *    See the documentation in _saveColumnStates for more information.
   */
  _savedColumnStates: null,

  /**
   * Return a dictionary in the form of |_savedColumnStates| representing the
   *  current column states.
   *
   * @public
   */
  getColumnStates() {
    if (!this._active) {
      return this._savedColumnStates;
    }

    let columnStates = {};

    let cols = document.getElementById("threadCols");
    let colChildren = cols.children;
    for (let iKid = 0; iKid < colChildren.length; iKid++) {
      let colChild = colChildren[iKid];
      if (colChild.tagName != "treecol") {
        continue;
      }
      columnStates[colChild.id] = {
        visible: !colChild.hidden,
        ordinal: colChild.ordinal,
      };
    }

    return columnStates;
  },

  /**
   * For now, just save the visible columns into a dictionary for use in a
   *  subsequent call to |setColumnStates|.
   */
  _saveColumnStates() {
    // In the actual TreeColumn, the index property indicates the column
    // number. This column number is a 0-based index with no gaps; it only
    // increments the number each time it sees a column.
    // However, this is subservient to the 'ordinal' property which
    // defines the _apparent content sequence_ provided by GetNextSibling.
    // The underlying content ordering is still the same, which is how
    // _ensureColumnOrder() can reset things to their XUL definition sequence.
    // The 'ordinal' stuff works because nsBoxFrame::RelayoutChildAtOrdinal
    // messes with the sibling relationship.
    // Ordinals are 1-based. _ensureColumnOrder() apparently is dumb and does
    // not know this, although the ordering is relative so it doesn't actually
    // matter. The annoying splitters do have ordinals, and live between
    // tree columns. The splitters adjacent to a tree column do not need to
    // have any 'ordinal' relationship, although it would appear user activity
    // tends to move them around in a predictable fashion with oddness involved
    // at the edges.
    // Changes to the ordinal attribute should take immediate effect in terms of
    // sibling relationship, but will merely invalidate the columns rather than
    // cause a re-computation of column relationships every time.
    // _ensureColumnOrder() invalidates the tree when it is done re-ordering;
    // I'm not sure that's entirely necessary...
    this._savedColumnStates = this.getColumnStates();
  },

  /**
   * Restores the visible columns saved by |_saveColumnStates|.
   */
  _restoreColumnStates() {
    if (this._savedColumnStates) {
      this.setColumnStates(this._savedColumnStates);
      this._savedColumnStates = null;
    }
  },
  // @}

  /**
   * @name What To Display
   * @protected
   */
  // @{
  showFolderUri(aFolderURI) {
    return this.show(MailUtils.getExistingFolder(aFolderURI));
  },

  /**
   * Invoked by showFolder when it turns out the folder is in fact a server.
   *
   * @private
   */
  _showServer() {
    // currently nothing to do.  makeActive handles everything for us (because
    //  what is displayed needs to be re-asserted each time we are activated
    //  too.)
  },

  /**
   * Select a folder for display.
   *
   * @param aFolder The nsIMsgDBFolder to display.
   */
  show(aFolder) {
    if (aFolder == null) {
      this._nonViewFolder = null;
      this.view.close();
    } else if (aFolder instanceof Ci.nsIMsgFolder) {
      if (aFolder.isServer) {
        this._nonViewFolder = aFolder;
        this._showServer();
        this.view.close();
        // A server is fully loaded immediately, for now.  (When we have the
        //  account summary, we might want to change this to wait for the page
        //  load to complete.)
        this._allMessagesLoaded = true;
      } else {
        this._nonViewFolder = null;
        this.view.open(aFolder);
      }
    } else {
      // it must be a synthetic view
      this.view.openSynthetic(aFolder);
    }
    if (this._active) {
      this.makeActive();
    }
  },

  /**
   * Clone an existing view wrapper as the basis for our display.
   */
  cloneView(aViewWrapper) {
    this.view = aViewWrapper.clone(this);
    // generate a view created notification; this will cause us to do the right
    //  thing in terms of associating the view with the tree and such.
    this.onCreatedView();
    if (this._active) {
      this.makeActive();
    }
  },

  /**
   * Close resources associated with the currently displayed folder because you
   *  no longer care about this FolderDisplayWidget.
   */
  close() {
    // Mark ourselves as inactive without doing any of the hard work of becoming
    //  inactive.  This saves us from trying to update things as they go away.
    this._active = false;

    this.view.close();
    this.messenger.setWindow(null, null);
    this.messenger = null;
  },
  // @}

  /*   ===============================   */
  /* ===== IDBViewWrapper Listener ===== */
  /*   ===============================   */

  /**
   * @name IDBViewWrapperListener Interface
   * @private
   */
  // @{

  /**
   * @returns true if the mail view picker is visible.  This affects whether the
   *     DBViewWrapper will actually use the persisted mail view or not.
   */
  get shouldUseMailViews() {
    return ViewPickerBinding.isVisible;
  },

  /**
   * Let the viewWrapper know if we should defer message display because we
   *  want the user to connect to the server first so password authentication
   *  can occur.
   *
   * @returns true if the folder should be shown immediately, false if we should
   *     wait for updateFolder to complete.
   */
  get shouldDeferMessageDisplayUntilAfterServerConnect() {
    let passwordPromptRequired = false;

    if (Services.prefs.getBoolPref("mail.password_protect_local_cache")) {
      passwordPromptRequired =
        this.view.displayedFolder.server.passwordPromptRequired;
    }

    return passwordPromptRequired;
  },

  /**
   * Let the viewWrapper know if it should mark the messages read when leaving
   *  the provided folder.
   *
   * @returns true if the preference is set for the folder's server type.
   */
  shouldMarkMessagesReadOnLeavingFolder(aMsgFolder) {
    return Services.prefs.getBoolPref(
      "mailnews.mark_message_read." + aMsgFolder.server.type
    );
  },

  /**
   * The view wrapper tells us when it starts loading a folder, and we set the
   *  cursor busy.  Setting the cursor busy on a per-tab basis is us being
   *  nice to the future. Loading a folder is a blocking operation that is going
   *  to make us unresponsive and accordingly make it very hard for the user to
   *  change tabs.
   */
  onFolderLoading(aFolderLoading) {
    FolderDisplayListenerManager._fireListeners("onFolderLoading", [
      this,
      aFolderLoading,
    ]);
  },

  /**
   * The view wrapper tells us when a search is active, and we mark the tab as
   *  thinking so the user knows something is happening.  'Searching' in this
   *  case is more than just a user-initiated search.  Virtual folders / saved
   *  searches, mail views, plus the more obvious quick search are all based off
   *  of searches and we will receive a notification for them.
   */
  onSearching(aIsSearching) {
    FolderDisplayListenerManager._fireListeners("onSearching", [
      this,
      aIsSearching,
    ]);
  },

  /**
   * Things we do on creating a view:
   * - notify the observer service so that custom column handler providers can
   *   add their custom columns to our view.
   */
  onCreatedView() {
    // All of our messages are not displayed if the view was just created.  We
    //  will get an onMessagesLoaded(true) nearly immediately if this is a local
    //  folder where view creation is synonymous with having all messages.
    this._allMessagesLoaded = false;

    FolderDisplayListenerManager._fireListeners("onCreatedView", [this]);

    this._notifyWhenActive(this._activeCreatedView);
  },
  _activeCreatedView() {
    gDBView = this.view.dbView; // eslint-disable-line no-global-assign

    // A change in view may result in changes to sorts, the view menu, etc.
    // Do this before we 'reroot' the dbview.
    this._updateThreadDisplay();

    // this creates a new selection object for the view.
    if (this.tree) {
      this.tree.view = this.view.dbView;
    }

    FolderDisplayListenerManager._fireListeners("onActiveCreatedView", [this]);

    // The data payload used to be viewType + ":" + viewFlags.  We no longer
    //  do this because we already have the implied contract that gDBView is
    //  valid at the time we generate the notification.  In such a case, you
    //  can easily get that information from the gDBView.  (The documentation
    //  on creating a custom column assumes gDBView.)
    Services.obs.notifyObservers(this.displayedFolder, "MsgCreateDBView");
  },

  /**
   * If our view is being destroyed and it is coming back, we want to save the
   *  current selection so we can restore it when the view comes back.
   */
  onDestroyingView(aFolderIsComingBack) {
    // try and persist the selection's content if we can
    if (this._active) {
      // If saving the selection throws an exception, we still want continue
      // destroying the view. Saving the selection can fail if an underlying
      // local folder has been compacted, invalidating the message keys.
      // See bug 536676 for more info.
      try {
        // If a new selection is coming up, there's no point in trying to
        // persist any selections.
        if (aFolderIsComingBack && !this._aboutToSelectMessage) {
          this._saveSelection();
        } else {
          this._clearSavedSelection();
        }
      } catch (ex) {
        console.error(ex);
      }
      gDBView = null; // eslint-disable-line no-global-assign
    }

    FolderDisplayListenerManager._fireListeners("onDestroyingView", [
      this,
      aFolderIsComingBack,
    ]);

    // if we have no view, no messages could be loaded.
    this._allMessagesLoaded = false;

    // but the actual tree view selection (based on view indices) is a goner no
    //  matter what, make everyone forget.
    this.view.dbView.selection = null;
    this._savedFirstVisibleRow = null;
    this._nextViewIndexAfterDelete = null;
    // although the move may still be active, its relation to the view is moot.
    this._massMoveActive = false;

    // Anything pending needs to get cleared out; the new view and its related
    //  events will re-schedule anything required or simply run it when it
    //  has its initial call to makeActive compelled.
    this._notificationsPendingActivation = [];
  },

  /**
   * Restore persisted information about what columns to display for the folder.
   *  If we have no persisted information, we leave/set _savedColumnStates null.
   *  The column states will be set to default values in onDisplayingFolder in
   *  that case.
   */
  onLoadingFolder(aDbFolderInfo) {
    this._savedColumnStates =
      this._depersistColumnStatesFromDbFolderInfo(aDbFolderInfo);

    FolderDisplayListenerManager._fireListeners("onLoadingFolder", [
      this,
      aDbFolderInfo,
    ]);
  },

  /**
   * We are entering the folder for display:
   * - set the header cache size.
   * - Setup the columns if we did not already depersist in |onLoadingFolder|.
   */
  onDisplayingFolder() {
    let displayedFolder = this.view.displayedFolder;
    let msgDatabase = displayedFolder && displayedFolder.msgDatabase;
    if (msgDatabase) {
      msgDatabase.resetHdrCacheSize(this.PERF_HEADER_CACHE_SIZE);
    }

    // makeActive will restore the folder state
    if (!this._savedColumnStates) {
      if (
        this.view.isSynthetic &&
        "getPersistedSetting" in this.view._syntheticView
      ) {
        let columns = this.view._syntheticView.getPersistedSetting("columns");
        this._savedColumnStates = columns;
      } else {
        // get the default for this folder
        this._savedColumnStates = this._getDefaultColumnsForCurrentFolder();
        // and save it so it doesn't wiggle if the inbox/prototype changes
        this._persistColumnStates(this._savedColumnStates);
      }
    }

    FolderDisplayListenerManager._fireListeners("onDisplayingFolder", [this]);

    if (this.active) {
      this.makeActive();
    }
  },

  /**
   * Notification from DBViewWrapper that it is closing the folder.  This can
   *  happen for reasons other than our own 'close' method closing the view.
   *  For example, user deletion of the folder or underlying folder closes it.
   */
  onLeavingFolder() {
    FolderDisplayListenerManager._fireListeners("onLeavingFolder", [this]);

    // Keep the msgWindow's openFolder up-to-date; it powers nsMessenger's
    //  concept of history so that it can bring you back to the actual folder
    //  you were looking at, rather than just the underlying folder.
    if (this._active) {
      msgWindow.openFolder = null;
    }
  },

  /**
   * Indicates whether we are done loading the messages that should be in this
   *  folder.  This is being surfaced for testing purposes, but could be useful
   *  to other code as well.  But don't poll this property; ask for an event
   *  that you can hook.
   */
  get allMessagesLoaded() {
    return this._allMessagesLoaded;
  },

  /**
   * Things to do once some or 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 virtual folder, this happens when the search completes.
   *
   * What we do:
   * - Any scrolling required!
   */
  onMessagesLoaded(aAll) {
    this._allMessagesLoaded = aAll;

    FolderDisplayListenerManager._fireListeners("onMessagesLoaded", [
      this,
      aAll,
    ]);

    this._notifyWhenActive(this._activeMessagesLoaded);
  },
  _activeMessagesLoaded() {
    FolderDisplayListenerManager._fireListeners("onActiveMessagesLoaded", [
      this,
    ]);

    // - if a selectMessage's coming up, get out of here
    if (this._aboutToSelectMessage) {
      return;
    }

    // - restore user's last expand/collapse choice.
    this.restoreThreadState();

    // - restore selection
    // Attempt to restore the selection (if we saved it because the view was
    //  being destroyed or otherwise manipulated in a fashion that the normal
    //  nsTreeSelection would be unable to handle.)
    if (this._restoreSelection()) {
      this.ensureRowIsVisible(this.view.dbView.viewIndexForFirstSelectedMsg);
      return;
    }

    // - pending navigation from pushNavigation (probably spacebar triggered)
    // Need to have all messages loaded first.
    if (this._pendingNavigation) {
      // Move it to a local and clear the state in case something bad happens.
      //  (We don't want to swallow the exception.)
      let pendingNavigation = this._pendingNavigation;
      this._pendingNavigation = null;
      this.navigate.apply(this, pendingNavigation);
      return;
    }

    // - if something's already selected (e.g. in a message tab), scroll to the
    //   first selected message and get out
    if (this.view.dbView.numSelected > 0) {
      this.ensureRowIsVisible(this.view.dbView.viewIndexForFirstSelectedMsg);
      return;
    }

    // - new messages
    // if configured to scroll to new messages, try that
    if (
      Services.prefs.getBoolPref("mailnews.scroll_to_new_message") &&
      this.navigate(Ci.nsMsgNavigationType.firstNew, /* select */ false)
    ) {
      return;
    }

    // - last selected message
    // if configured to load the last selected message (this is currently more
    //  persistent than our saveSelection/restoreSelection stuff), and the view
    //  is backed by a single underlying folder (the only way having just a
    //  message key works out), try that
    if (
      Services.prefs.getBoolPref("mailnews.remember_selected_message") &&
      this.view.isSingleFolder &&
      this.view.displayedFolder
    ) {
      // use the displayed folder; nsMsgDBView goes to the effort to save the
      //  state to the viewFolder, so this is the correct course of action.
      let lastLoadedMessageKey = this.view.displayedFolder.lastMessageLoaded;
      if (lastLoadedMessageKey != nsMsgKey_None) {
        this.view.dbView.selectMsgByKey(lastLoadedMessageKey);
        // The message key may not be present in the view for a variety of
        //  reasons.  Beyond message deletion, it simply may not match the
        //  active mail view or quick search, for example.
        if (this.view.dbView.numSelected > 0) {
          this.ensureRowIsVisible(
            this.view.dbView.viewIndexForFirstSelectedMsg
          );
          return;
        }
      }
    }

    // - towards the newest messages, but don't select
    if (
      this.view.isSortedAscending &&
      this.view.sortImpliesTemporalOrdering &&
      this.navigate(Ci.nsMsgNavigationType.lastMessage, /* select */ false)
    ) {
      return;
    }

    // - to the top, the coliseum
    this.ensureRowIsVisible(0);
  },

  /**
   * The DBViewWrapper tells us when someone (possibly the wrapper itself)
   *  changes the active mail view so that we can kick the UI to update.
   */
  onMailViewChanged() {
    // only do this if we're currently active.  no need to queue it because we
    //  always update the mail view whenever we are made active.
    if (this.active) {
      // you cannot cancel a view change!
      window.dispatchEvent(
        new Event("MailViewChanged", { bubbles: false, cancelable: false })
      );
    }
  },

  /**
   * Just the sort or threading was changed, without changing other things.  We
   *  will not get this notification if the view was re-created, for example.
   */
  onSortChanged() {
    if (this.active) {
      UpdateSortIndicators(
        this.view.primarySortType,
        this.view.primarySortOrder
      );
    }

    FolderDisplayListenerManager._fireListeners("onSortChanged", [this]);
  },

  /**
   * Messages (that may have been displayed) have been removed; this may impact
   * our message selection. We might know it's coming; if we do then
   * this._nextViewIndexAfterDelete should know what view index to select next.
   * For the imap mark-as-deleted we won't know beforehand.
   */
  onMessagesRemoved() {
    FolderDisplayListenerManager._fireListeners("onMessagesRemoved", [this]);

    this._deleteInProgress = false;

    // - we saw this coming
    let rowCount = this.view.dbView.rowCount;
    if (!this._massMoveActive && this._nextViewIndexAfterDelete != null) {
      // adjust the index if it is after the last row...
      // (this can happen if the "mail.delete_matches_sort_order" pref is not
      //  set and the message is the last message in the view.)
      if (this._nextViewIndexAfterDelete >= rowCount) {
        this._nextViewIndexAfterDelete = rowCount - 1;
      }
      // just select the index and get on with our lives
      this.selectViewIndex(this._nextViewIndexAfterDelete);
      this._nextViewIndexAfterDelete = null;
      return;
    }

    // - we didn't see it coming

    // A deletion happened to our folder.
    let treeSelection = this.treeSelection;
    // we can't fix the selection if we have no selection
    if (!treeSelection) {
      return;
    }

    // For reasons unknown (but theoretically knowable), sometimes the selection
    //  object will be invalid.  At least, I've reliably seen a selection of
    //  [0, 0] with 0 rows.  If that happens, we need to fix up the selection
    //  here.
    if (rowCount == 0 && treeSelection.count) {
      // nsTreeSelection doesn't generate an event if we use clearRange, so use
      //  that to avoid spurious events, given that we are going to definitely
      //  trigger a change notification below.
      treeSelection.clearRange(0, 0);
    }

    // Check if we now no longer have a selection, but we had exactly one
    //  message selected previously.  If we did, then try and do some
    //  'persistence of having a thing selected'.
    if (
      treeSelection.count == 0 &&
      this._mostRecentSelectionCounts.length > 1 &&
      this._mostRecentSelectionCounts[1] == 1 &&
      this._mostRecentCurrentIndices[1] != -1
    ) {
      let targetIndex = this._mostRecentCurrentIndices[1];
      if (targetIndex >= rowCount) {
        targetIndex = rowCount - 1;
      }
      this.selectViewIndex(targetIndex);
      return;
    }

    // Otherwise, just tell the view that things have changed so it can update
    //  itself to the new state of things.
    // tell the view that things have changed so it can update itself suitably.
    if (this.view.dbView) {
      this.view.dbView.selectionChanged();
    }
  },

  /**
   * Messages were not actually removed, but we were expecting that they would
   *  be.  Clean-up what onMessagesRemoved would have cleaned up, namely the
   *  next view index to select.
   */
  onMessageRemovalFailed() {
    this._nextViewIndexAfterDelete = null;
    FolderDisplayListenerManager._fireListeners("onMessagesRemovalFailed", [
      this,
    ]);
  },

  /**
   * Update the status bar to reflect our exciting message counts.
   */
  onMessageCountsChanged() {},
  // @}
  /* ===== End IDBViewWrapperListener ===== */

  /*   ==================================   */
  /* ===== nsIMsgDBViewCommandUpdater ===== */
  /*   ==================================   */

  /**
   * @name nsIMsgDBViewCommandUpdater Interface
   * @private
   */
  // @{

  /**
   * This gets called when the selection changes AND !suppressCommandUpdating
   *  AND (we're not removing a row OR we are now out of rows).
   * In response, we update the toolbar.
   */
  updateCommandStatus() {},

  /**
   * This gets called by nsMsgDBView::UpdateDisplayMessage following a call
   *  to nsIMessenger.OpenURL to kick off message display OR (UDM gets called)
   *  by nsMsgDBView::SelectionChanged in lieu of loading the message because
   *  mSupressMsgDisplay.
   * In other words, we get notified immediately after the process of displaying
   *  a message triggered by the nsMsgDBView happens.  We get some arguments
   *  that are display optimizations for historical reasons (as usual).
   *
   * Things this makes us want to do:
   * - Set the tab title, perhaps.  (If we are a message display.)
   * - Update message counts, because things might have changed, why not.
   * - Update some toolbar buttons, why not.
   *
   * @param aFolder The display/view folder, as opposed to the backing folder.
   * @param aSubject The subject with "Re: " if it's got one, which makes it
   *     notably different from just directly accessing the message header's
   *     subject.
   * @param aKeywords The keywords, which roughly translates to message tags.
   */
  displayMessageChanged(aFolder, aSubject, aKeywords) {},

  /**
   * This gets called as a hint that the currently selected message is junk and
   *  said junked message is going to be moved out of the current folder, or
   *  right before a header is removed from the db view.  The legacy behaviour
   *  is to retrieve the msgToSelectAfterDelete attribute off the db view,
   *  stashing it for benefit of the code that gets called when a message
   *  move/deletion is completed so that we can trigger its display.
   */
  updateNextMessageAfterDelete() {
    this.hintAboutToDeleteMessages();
  },

  /**
   * The most recent currentIndexes on the selection (from the last time
   *  summarizeSelection got called).  We use this in onMessagesRemoved if
   *  we get an unexpected notification.
   * We keep a maximum of 2 entries in this list.
   */
  _mostRecentCurrentIndices: undefined, // initialized in constructor
  /**
   * The most recent counts on the selection (from the last time
   *  summarizeSelection got called).  We use this in onMessagesRemoved if
   *  we get an unexpected notification.
   * We keep a maximum of 2 entries in this list.
   */
  _mostRecentSelectionCounts: undefined, // initialized in constructor

  /**
   * Always called by the db view when the selection changes in
   *  SelectionChanged.  This event will come after the notification to
   *  displayMessageChanged (if one happens), and before the notification to
   *  updateCommandStatus (if one happens).
   */
  summarizeSelection() {
    // save the current index off in case the selection gets deleted out from
    //  under us and we want to have persistence of actually-having-something
    //  selected.
    let treeSelection = this.treeSelection;
    if (treeSelection) {
      this._mostRecentCurrentIndices.unshift(treeSelection.currentIndex);
      this._mostRecentCurrentIndices.splice(2);
      this._mostRecentSelectionCounts.unshift(treeSelection.count);
      this._mostRecentSelectionCounts.splice(2);
    }
  },
  // @}
  /* ===== End nsIMsgDBViewCommandUpdater ===== */

  /* ===== Hints from the command infrastructure ===== */
  /**
   * @name Command Infrastructure Hints
   * @protected
   */
  // @{

  /**
   * doCommand helps us out by telling us when it is telling the view to delete
   *  some messages.  Ideally it should go through us / the DB View Wrapper to
   *  kick off the delete in the first place, but that's a thread I don't want
   *  to pull on right now.
   * We use this hint to figure out the next message to display once the
   *  deletion completes.  We do this before the deletion happens because the
   *  selection is probably going away (except in the IMAP delete model), and it
   *  might be too late to figure this out after the deletion happens.
   * Our automated complement (that calls us) is updateNextMessageAfterDelete.
   */
  hintAboutToDeleteMessages() {
    this._deleteInProgress = true;
    // save the value, even if it is nsMsgViewIndex_None.
    this._nextViewIndexAfterDelete = this.view.dbView.msgToSelectAfterDelete;
  },

  /**
   * The archive code tells us when it is starting to archive messages.  This
   *  is different from hinting about deletion because it will also tell us
   *  when it has completed its mass move.
   * The UI goal is that we do not immediately jump beyond the selected messages
   *  to the next message until all of the selected messages have been
   *  processed (moved).  Ideally we would also do this when deleting messages
   *  from a multiple-folder backed message view, but we don't know when the
   *  last job completes in that case (whereas in this case we do because of the
   *  call to hintMassMoveCompleted.)
   */
  hintMassMoveStarting() {
    this.hintAboutToDeleteMessages();
    this._massMoveActive = true;
  },

  /**
   * The archival has completed, we can finally let onMessagseRemoved run to
   *  completion.
   */
  hintMassMoveCompleted() {
    this._massMoveActive = false;
    this.onMessagesRemoved();
  },

  /**
   * When a right-click on the thread pane is going to alter our selection, we
   *  get this notification (currently from |ChangeSelectionWithoutContentLoad|
   *  in threadPane.js), which lets us save our state.
   * This ends one of two ways: we get made inactive because a new tab popped up
   *  or we get a call to |hintRightClickSelectionPerturbationDone|.
   *
   * Ideally, we could just save off our current nsITreeSelection and restore it
   *  when this is all over.  This assumption would rely on the underlying view
   *  not having any changes to its rows before we restore the selection.  I am
   *  not confident we can rule out background processes making changes, plus
   *  the right-click itself may mutate the view (although we could try and get
   *  it to restore the selection before it gets to the mutation part).  Our
   *  only way to resolve this would be to create a 'tee' like fake selection
   *  that would proxy view change notifications to both sets of selections.
   *  That is hard.
   * So we just use the existing _saveSelection/_restoreSelection mechanism
   *  which is potentially very costly.
   */
  hintRightClickPerturbingSelection() {
    this._saveSelection();
  },

  /**
   * When a right-click on the thread pane altered our selection (which we
   *  should have received a call to |hintRightClickPerturbingSelection| for),
   *  we should receive this notification from
   *  |RestoreSelectionWithoutContentLoad| when it wants to put things back.
   */
  hintRightClickSelectionPerturbationDone() {
    this._restoreSelection();
  },
  // @}
  /* ===== End hints from the command infrastructure ==== */

  _updateThreadDisplay() {
    if (this.active) {
      if (this.view.dbView) {
        UpdateSortIndicators(
          this.view.dbView.sortType,
          this.view.dbView.sortOrder
        );
        SetNewsFolderColumns();
        UpdateSelectCol();
      }
    }
  },

  /**
   * Update the UI display apart from the thread tree because the folder being
   *  displayed has changed.  This can be the result of changing the folder in
   *  this FolderDisplayWidget, or because this FolderDisplayWidget is being
   *  made active.  _updateThreadDisplay handles the parts of the thread tree
   *  that need updating.
   */
  _updateContextDisplay() {
    if (this.active) {
      UpdateStatusQuota(this.displayedFolder);

      // - mail view combo-box.
      this.onMailViewChanged();
    }
  },

  /**
   * @name Activation Control
   * @protected
   */
  // @{

  /**
   * Run the provided notification function right now if we are 'active' (the
   *  currently displayed tab), otherwise queue it to be run when we become
   *  active.  We do this because our tabbing model uses multiplexed (reused)
   *  widgets, and extensions likewise depend on these global/singleton things.
   * If the requested notification function is already queued, it will not be
   *  added a second time, and the original call ordering will be maintained.
   *  If a new call ordering is required, the list of notifications should
   *  probably be reset by the 'big bang' event (new view creation?).
   */
  _notifyWhenActive(aNotificationFunc) {
    if (this._active) {
      aNotificationFunc.call(this);
    } else if (
      !this._notificationsPendingActivation.includes(aNotificationFunc)
    ) {
      this._notificationsPendingActivation.push(aNotificationFunc);
    }
  },

  /**
   * Some notifications cannot run while the FolderDisplayWidget is inactive
   *  (presumbly because it is in a background tab).  We accumulate those in
   *  _notificationsPendingActivation and then this method runs them when we
   *  become active again.
   */
  _runNotificationsPendingActivation() {
    if (!this._notificationsPendingActivation.length) {
      return;
    }

    let pendingNotifications = this._notificationsPendingActivation;
    this._notificationsPendingActivation = [];
    for (let notif of pendingNotifications) {
      notif.call(this);
    }
  },

  // This is not guaranteed to be up to date if the folder display is active
  _folderPaneVisible: null,

  /**
   * Whether the folder pane is visible. When we're inactive, we stash the value
   * in |this._folderPaneVisible|.
   */
  get folderPaneVisible() {
    // Early return if the user wants to use Thunderbird without an email
    // account and no account is configured.
    if (
      Services.prefs.getBoolPref("app.use_without_mail_account", false) &&
      !MailServices.accounts.accounts.length
    ) {
      return false;
    }

    if (this._active) {
      let folderPaneBox = document.getElementById("folderPaneBox");
      if (folderPaneBox) {
        return !folderPaneBox.collapsed;
      }
    } else {
      return this._folderPaneVisible;
    }

    return null;
  },

  /**
   * Sets the visibility of the folder pane. This should reflect reality and
   * not define it (for active tabs at least).
   */
  set folderPaneVisible(aVisible) {
    this._folderPaneVisible = aVisible;
  },

  get active() {
    return this._active;
  },

  /**
   * Make this FolderDisplayWidget the 'active' widget by updating globals and
   *  linking us up to the UI widgets.  This is intended for use by the tabbing
   *  logic.
   */
  makeActive(aWasInactive) {
    let wasInactive = !this._active;

    // -- globals
    // update per-tab globals that we own
    gFolderDisplay = this; // eslint-disable-line no-global-assign
    gDBView = this.view.dbView; // eslint-disable-line no-global-assign
    messenger = this.messenger; // eslint-disable-line no-global-assign

    // update singleton globals' state
    msgWindow.openFolder = this.view.displayedFolder;

    this._active = true;
    this._runNotificationsPendingActivation();

    FolderDisplayListenerManager._fireListeners("onMakeActive", [
      this,
      aWasInactive,
    ]);

    // -- UI

    // thread pane if we have a db view
    if (this.view.dbView) {
      // Make sure said thread pane is visible.  If we do this after we re-root
      //  the tree, the thread pane may not actually replace the account central
      //  pane.  Concerning...
      this._showThreadPane();

      // some things only need to happen if we are transitioning from inactive
      //  to active
      if (wasInactive) {
        if (this.tree) {
          // Setting the 'view' attribute on treeBox results in the following
          //  effective calls, noting that in makeInactive we made sure to null
          //  out its view so that it won't try and clean up any views or their
          //  selections.  (The actual actions happen in
          //  nsTreeBodyFrame::SetView)
          // - this.view.dbView.selection.tree = this.tree
          // - this.view.dbView.setTree(this.tree)
          // - this.tree.view = this.view.dbView (in
          //   nsTreeBodyObject::SetView)
          this.tree.view = this.view.dbView;

          if (this._savedFirstVisibleRow != null) {
            this.tree.scrollToRow(this._savedFirstVisibleRow);
          }
        }
      }

      // Always restore the column state if we have persisted state.  We restore
      //  state on folder entry, in which case we were probably not inactive.
      this._restoreColumnStates();

      // update the columns and such that live inside the thread pane
      this._updateThreadDisplay();
    }

    this._updateContextDisplay();
  },

  /**
   * Cause the displayBox to display the thread pane.
   */
  _showThreadPane() {
    document.getElementById("accountCentralBox").collapsed = true;
    document.getElementById("threadPaneBox").collapsed = false;
  },

  /**
   * Cause the displayBox to display the (preference configurable) account
   *  central page.
   */
  _showAccountCentral() {
    if (!this.displayedFolder && MailServices.accounts.accounts.length > 0) {
      // If we have any accounts set up, but no folder is selected yet,
      // we expect another selection event to come when session restore finishes.
      // Until then, do nothing.
      return;
    }
    document.getElementById("accountCentralBox").collapsed = false;
    document.getElementById("threadPaneBox").collapsed = true;

    // Prevent a second load if necessary.
    let loadURL =
      "chrome://messenger/content/msgAccountCentral.xhtml" +
      (this.displayedFolder
        ? "?folderURI=" + encodeURIComponent(this.displayedFolder.URI)
        : "");
    if (window.frames.accountCentralPane.location.href != loadURL) {
      window.frames.accountCentralPane.location.href = loadURL;
    }
  },

  /**
   * Call this when the tab using us is being hidden.
   */
  makeInactive() {
    // - things to do before we mark ourselves inactive (because they depend on
    //   us being active)

    // getColumnStates returns _savedColumnStates when we are inactive (and is
    //  used by _saveColumnStates) so we must do this before marking inactive.
    this._saveColumnStates();

    // - mark us inactive
    this._active = false;

    // - (everything after this point doesn't care that we are marked inactive)
    // save the folder pane's state always
    this._folderPaneVisible =
      !document.getElementById("folderPaneBox").collapsed;

    if (this.view.dbView) {
      if (this.tree) {
        this._savedFirstVisibleRow = this.tree.getFirstVisibleRow();
      }

      // save the message pane's state only when it is potentially visible
      this.messagePaneCollapsed = document.getElementById(
        "messagepaneboxwrapper"
      ).collapsed;
    }
  },
  // @}

  /**
   * @name Command Support
   */
  // @{

  /**
   * @returns true if there is a db view and the command is enabled on the view.
   *  This function hides some of the XPCOM-odditities of the getCommandStatus
   *  call.
   */
  getCommandStatus(aCommandType, aEnabledObj, aCheckStatusObj) {
    // no view means not enabled
    if (!this.view.dbView) {
      return false;
    }
    let enabledObj = {},
      checkStatusObj = {};
    this.view.dbView.getCommandStatus(aCommandType, enabledObj, checkStatusObj);
    return enabledObj.value;
  },

  /**
   * Make code cleaner by allowing peoples to call doCommand on us rather than
   *  having to do folderDisplayWidget.view.dbView.doCommand.
   *
   * @param aCommandName The command name to invoke.
   */
  doCommand(aCommandName) {
    return this.view.dbView && this.view.dbView.doCommand(aCommandName);
  },

  /**
   * Make code cleaner by allowing peoples to call doCommandWithFolder on us
   *  rather than having to do:
   *  folderDisplayWidget.view.dbView.doCommandWithFolder.
   *
   * @param aCommandName The command name to invoke.
   * @param aFolder The folder context for the command.
   */
  doCommandWithFolder(aCommandName, aFolder) {
    return (
      this.view.dbView &&
      this.view.dbView.doCommandWithFolder(aCommandName, aFolder)
    );
  },
  // @}

  /**
   * @returns true when account central is being displayed.
   * @groupName Displayed
   */
  get isAccountCentralDisplayed() {
    return this.view.dbView == null;
  },

  /**
   * @name Navigation
   * @protected
   */
  // @{

  /**
   * Navigate using nsMsgNavigationType rules and ensuring the resulting row is
   *  visible.  This is trickier than it used to be because we now support
   *  treating collapsed threads as the set of all the messages in the collapsed
   *  thread rather than just the root message in that thread.
   *
   * @param {nsMsgNavigationType} aNavType navigation command.
   * @param {boolean} [aSelect=true] should we select the message if we find
   *     one?
   *
   * @returns true if the navigation constraint matched anything, false if not.
   *     We will have navigated if true, we will have done nothing if false.
   */
  navigate(aNavType, aSelect) {
    if (aSelect === undefined) {
      aSelect = true;
    }
    let resultKeyObj = {},
      resultIndexObj = {},
      threadIndexObj = {};

    let summarizeSelection = this.summarizeSelectionInFolder;

    let treeSelection = this.treeSelection; // potentially magic getter
    let currentIndex = treeSelection ? treeSelection.currentIndex : 0;

    let viewIndex;
    // if we're doing next unread, and a collapsed thread is selected, and
    // the top level message is unread, just set the result manually to
    // the top level message, without using viewNavigate.
    if (
      summarizeSelection &&
      aNavType == Ci.nsMsgNavigationType.nextUnreadMessage &&
      currentIndex != -1 &&
      this.view.isCollapsedThreadAtIndex(currentIndex) &&
      !(this.view.dbView.getFlagsAt(currentIndex) & Ci.nsMsgMessageFlags.Read)
    ) {
      viewIndex = currentIndex;
    } else {
      // always 'wrap' because the start index is relative to the selection.
      // (keep in mind that many forms of navigation do not care about the
      //  starting position or 'wrap' at all; for example, firstNew just finds
      //  the first new message.)
      // allegedly this does tree-expansion for us.
      this.view.dbView.viewNavigate(
        aNavType,
        resultKeyObj,
        resultIndexObj,
        threadIndexObj,
        true
      );
      viewIndex = resultIndexObj.value;
    }

    if (viewIndex == nsMsgViewIndex_None) {
      return false;
    }

    // - Expand if required.
    // (The nsMsgDBView isn't really aware of the varying semantics of
    //  collapsed threads, so viewNavigate might tell us about the root message
    //  and leave it collapsed, not realizing that it needs to be expanded.)
    if (summarizeSelection && this.view.isCollapsedThreadAtIndex(viewIndex)) {
      this.view.dbView.toggleOpenState(viewIndex);
    }

    if (aSelect) {
      this.selectViewIndex(viewIndex);
    } else {
      this.ensureRowIsVisible(viewIndex);
    }
    return true;
  },

  /**
   * Push a call to |navigate| to be what we do once we successfully open the
   *  next folder.  This is intended to be used by cross-folder navigation
   *  code.  It should call this method before triggering the folder change.
   */
  pushNavigation(aNavType, aSelect) {
    this._pendingNavigation = [aNavType, aSelect];
  },
  // @}

  /**
   * @name Selection
   */
  // @{

  /**
   * @returns the message header for the first selected message, or null if
   *  there is no selected message.
   *
   * If the user has right-clicked on a message, this method will return that
   *  message and not the 'current index' (the dude with the dotted selection
   *  rectangle around him.)  If you instead always want the currently
   *  displayed message (which is not impacted by right-clicking), then you
   *  would want to access the displayedMessage property on the
   *  MessageDisplayWidget.  You can get to that via the messageDisplay
   *  attribute on this object or (potentially) via the gMessageDisplay object.
   */
  get selectedMessage() {
    // there are inconsistencies in hdrForFirstSelectedMessage between
    //  nsMsgDBView and nsMsgSearchDBView in whether they use currentIndex,
    //  do it ourselves.  (nsMsgDBView does not use currentIndex, search does.)
    let treeSelection = this.treeSelection;
    if (!treeSelection || !treeSelection.count) {
      return null;
    }
    let minObj = {},
      maxObj = {};
    treeSelection.getRangeAt(0, minObj, maxObj);
    return this.view.dbView.getMsgHdrAt(minObj.value);
  },

  /**
   * @returns true if there is a selected message and it's an RSS feed message;
   *  a feed message does not have to be in an rss account folder if stored in
   *  Tb15 and later.
   */
  get selectedMessageIsFeed() {
    return FeedUtils.isFeedMessage(this.selectedMessage);
  },

  /**
   * @returns true if there is a selected message and it's an IMAP message.
   */
  get selectedMessageIsImap() {
    let message = this.selectedMessage;
    return Boolean(
      message &&
        message.folder &&
        message.folder.flags & Ci.nsMsgFolderFlags.ImapBox
    );
  },

  /**
   * @returns true if there is a selected message and it's a news message.  It
   *  would be great if messages knew this about themselves, but they don't.
   */
  get selectedMessageIsNews() {
    let message = this.selectedMessage;
    return Boolean(
      message &&
        message.folder &&
        message.folder.flags & Ci.nsMsgFolderFlags.Newsgroup
    );
  },

  /**
   * @returns true if there is a selected message and it's an external message,
   *  meaning it is loaded from an .eml file on disk or is an rfc822 attachment
   *  on a message.
   */
  get selectedMessageIsExternal() {
    let message = this.selectedMessage;
    // Dummy messages currently lack a folder.  This is not a great heuristic.
    // I have annotated msgHdrView.js which provides the dummy header to
    //  express this implementation dependency.
    // (Currently, since external mails can only be opened in standalone windows
    //  which subclass us, we could always return false, and have the subclass
    //  return true using its own heuristics.  But since we are moving to a tab
    //  model more heavily, at some point the 3-pane will need this.)
    return Boolean(message && !message.folder);
  },

  /**
   * @returns true if there is a selected message and the message belongs to an
   *              ignored thread.
   */
  get selectedMessageThreadIgnored() {
    let message = this.selectedMessage;
    return Boolean(
      message &&
        message.folder &&
        message.folder.msgDatabase.isIgnored(message.messageKey)
    );
  },

  /**
   * @returns true if there is a selected message and the message is the base
   *              message for an ignored subthread.
   */
  get selectedMessageSubthreadIgnored() {
    let message = this.selectedMessage;
    return Boolean(
      message && message.folder && message.flags & Ci.nsMsgMessageFlags.Ignored
    );
  },

  /**
   * @returns true if there is a selected message and the message belongs to a
   *              watched thread.
   */
  get selectedMessageThreadWatched() {
    let message = this.selectedMessage;
    return Boolean(
      message &&
        message.folder &&
        message.folder.msgDatabase.isWatched(message.messageKey)
    );
  },

  /**
   * @returns the number of selected messages.  If summarizeSelectionInFolder is
   *  true, then any collapsed thread roots that are selected will also
   *  conceptually have all of the messages in that thread selected.
   */
  get selectedCount() {
    return this.selectedMessages.length;
  },

  /**
   * Provides a list of the view indices that are selected which is *not* the
   *  same as the rows of the selected messages.  When
   *  summarizeSelectionInFolder is true, messages may be selected but not
   *  visible (because the thread root is selected.)
   * You probably want to use the |selectedMessages| attribute instead of this
   *  one.  (Or selectedMessageUris in some rare cases.)
   *
   * If the user has right-clicked on a message, this will return that message
   *  and not the selection prior to the right-click.
   *
   * @returns a list of the view indices that are currently selected
   */
  get selectedIndices() {
    if (!this.view.dbView) {
      return [];
    }

    return this.view.dbView.getIndicesForSelection();
  },

  /**
   * Provides a list of the message headers for the currently selected messages.
   *  If summarizeSelectionInFolder is true, then any collapsed thread roots
   *  that are selected will also (conceptually) have all of the messages in
   *  that thread selected and they will be included in the returned list.
   *
   * If the user has right-clicked on a message, this will return that message
   *  (and any collapsed children if so enabled) and not the selection prior to
   *  the right-click.
   *
   * @returns a list of the message headers for the currently selected messages.
   *     If there are no selected messages, the result is an empty list.
   */
  get selectedMessages() {
    if (!this._active && this._savedSelection?.messages) {
      return this._savedSelection.messages
        .map(savedInfo => this.view.getMsgHdrForMessageID(savedInfo.messageId))
        .filter(msgHdr => !!msgHdr);
    }
    if (!this.view.dbView) {
      return [];
    }
    return this.view.dbView.getSelectedMsgHdrs();
  },

  /**
   * @returns a list of the URIs for the currently selected messages or null
   *     (instead of a list) if there are no selected messages.  Do not
   *     pass around URIs unless you have a good reason.  Legacy code is an
   *     ok reason.
   *
   * If the user has right-clicked on a message, this will return that message's
   *  URI and not the selection prior to the right-click.
   */
  get selectedMessageUris() {
    if (!this.view.dbView) {
      return null;
    }

    let messageArray = this.view.dbView.getURIsForSelection();
    return messageArray.length ? messageArray : null;
  },

  /**
   * @returns true if all the selected messages can be archived, false otherwise.
   */
  get canArchiveSelectedMessages() {
    return false;
  },

  /**
   * The maximum number of messages canMarkThreadAsRead will look through.
   * If the number exceeds this limit, as a performance measure, we return
   * true rather than looking through the messages and possible
   * submessages.
   */
  MAX_COUNT_FOR_MARK_THREAD: 1000,

  /**
   * Check if the thread for the currently selected message can be marked as
   * read. A thread can be marked as read if and only if it has at least one
   * unread message.
   */
  get canMarkThreadAsRead() {
    if (
      (this.displayedFolder && this.displayedFolder.getNumUnread(false) > 0) ||
      this.view._underlyingData === this.view.kUnderlyingSynthetic
    ) {
      // If the messages limit is exceeded we bail out early and return true.
      if (this.selectedIndices.length > this.MAX_COUNT_FOR_MARK_THREAD) {
        return true;
      }

      for (let i of this.selectedIndices) {
        if (
          this.view.dbView.getThreadContainingIndex(i).numUnreadChildren > 0
        ) {
          return true;
        }
      }
    }
    return false;
  },

  /**
   * @returns true if all the selected messages can be deleted from their
   * folders, false otherwise.
   */
  get canDeleteSelectedMessages() {
    if (!this.view.dbView) {
      return false;
    }

    let selectedMessages = this.selectedMessages;
    for (let i = 0; i < selectedMessages.length; ++i) {
      if (
        selectedMessages[i].folder &&
        (!selectedMessages[i].folder.canDeleteMessages ||
          selectedMessages[i].folder.flags & Ci.nsMsgFolderFlags.Newsgroup)
      ) {
        return false;
      }
    }
    return true;
  },

  /**
   * Clear the tree selection, making sure the message pane is cleared and
   *  the context display (toolbars, etc.) are updated.
   */
  clearSelection() {
    let treeSelection = this.treeSelection; // potentially magic getter
    if (!treeSelection) {
      return;
    }
    treeSelection.clearSelection();
    this._updateContextDisplay();
  },

  // Whether we're about to select a message
  _aboutToSelectMessage: false,

  /**
   * This needs to be called to let us know that a selectMessage or equivalent
   * is coming  up right after a show() call, so that we know that a double
   * message load won't be happening.
   *
   * This can be assumed to be idempotent.
   */
  selectMessageComingUp() {
    this._aboutToSelectMessage = true;
  },

  /**
   * Select a message for display by header.  Attempt to select the message
   *  right now.  If we were unable to find it, update our saved selection
   *  to want to display the message.  Threads are expanded to find the header.
   *
   * @param aMsgHdr The message header to select for display.
   * @param [aForceSelect] 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.
   */
  selectMessage(aMsgHdr, aForceSelect) {
    let viewIndex = this.view.getViewIndexForMsgHdr(aMsgHdr, aForceSelect);
    if (viewIndex != nsMsgViewIndex_None) {
      this._savedSelection = null;
      this.selectViewIndex(viewIndex);
    } else {
      this._savedSelection = {
        messages: [{ messageId: aMsgHdr.messageId }],
        forceSelect: aForceSelect,
      };
      // queue the selection to be restored once we become active if we are not
      //  active.
      if (!this.active) {
        this._notifyWhenActive(this._restoreSelection);
      }
    }

    // Do this here instead of at the beginning to prevent reentrancy issues
    this._aboutToSelectMessage = false;
  },

  /**
   * Select all of the provided nsIMsgDBHdrs in the aMessages array, expanding
   *  threads as required.  If we were not able to find all of the messages,
   *  update our saved selection to want to display the messages.  The messages
   *  will then be selected when we are made active or all messages in the
   *  folder complete loading.  This is to accommodate the use-case where we
   *  are backed by an in-progress search and no
   *
   * @param aMessages An array of nsIMsgDBHdr instances.
   * @param [aForceSelect] If a 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.
   * @param aDoNotNeedToFindAll If true (can be omitted and left undefined), we
   *     do not attempt to save the selection for future use.  This is intended
   *     for use by the _restoreSelection call which is the end-of-the-line for
   *     restoring the selection.  (Once it gets called all of our messages
   *     should have already been loaded.)
   */
  selectMessages(aMessages, aForceSelect, aDoNotNeedToFindAll) {
    let treeSelection = this.treeSelection; // potentially magic getter
    let foundAll = true;
    if (treeSelection) {
      let minRow = null,
        maxRow = null;

      treeSelection.selectEventsSuppressed = true;
      treeSelection.clearSelection();

      for (let msgHdr of aMessages) {
        let viewIndex = this.view.getViewIndexForMsgHdr(msgHdr, aForceSelect);

        if (viewIndex != nsMsgViewIndex_None) {
          if (minRow == null || viewIndex < minRow) {
            minRow = viewIndex;
          }
          if (maxRow == null || viewIndex > maxRow) {
            maxRow = viewIndex;
          }
          // nsTreeSelection is actually very clever about doing this
          //  efficiently.
          treeSelection.rangedSelect(viewIndex, viewIndex, true);
        } else {
          foundAll = false;
        }

        // make sure the selection is as visible as possible
        if (minRow != null) {
          this.ensureRowRangeIsVisible(minRow, maxRow);
        }
      }

      treeSelection.selectEventsSuppressed = false;

      // If we haven't selected every message, we'll set |this._savedSelection|
      // below, so it's fine to null it out at this point.
      this._savedSelection = null;
    }

    // Do this here instead of at the beginning to prevent reentrancy issues
    this._aboutToSelectMessage = false;

    // Two cases.
    // 1. The tree selection isn't there at all.
    // 2. The tree selection is there, and we needed to find all messages, but
    //    we didn't.
    if (!treeSelection || (!aDoNotNeedToFindAll && !foundAll)) {
      this._savedSelection = {
        messages: aMessages.map(msgHdr => ({ messageId: msgHdr.messageId })),
        forceSelect: aForceSelect,
      };
      if (!this.active) {
        this._notifyWhenActive(this._restoreSelection);
      }
    }
  },

  /**
   * Select the message at view index.
   *
   * @param aViewIndex The view index to select.  This will be bounds-checked
   *     and if it is outside the bounds, we will clear the selection and
   *     bail.
   */
  selectViewIndex(aViewIndex) {
    let treeSelection = this.treeSelection;
    // if we have no selection, we can't select something
    if (!treeSelection) {
      return;
    }
    let rowCount = this.view.dbView.rowCount;
    if (
      aViewIndex == nsMsgViewIndex_None ||
      aViewIndex < 0 ||
      aViewIndex >= rowCount
    ) {
      this.clearSelection();
      return;
    }

    // Check whether the index is already selected/current.  This can be the
    //  case when we are here as the result of a deletion.  Assuming
    //  nsMsgDBView::NoteChange ran and was not suppressing change
    //  notifications, then it's very possible the selection is already where
    //  we want it to go.  However, in that case, nsMsgDBView::SelectionChanged
    //  bailed without doing anything because m_deletingRows...
    // So we want to generate a change notification if that is the case. (And
    //  we still want to call ensureRowIsVisible, as there may be padding
    //  required.)
    if (
      treeSelection.count == 1 &&
      (treeSelection.currentIndex == aViewIndex ||
        treeSelection.isSelected(aViewIndex))
    ) {
      // Make sure the index we just selected is also the current index.
      //  This can happen when the tree selection adjusts itself as a result of
      //  changes to the tree as a result of deletion.  This will not trigger
      //  a notification.
      treeSelection.select(aViewIndex);
      this.view.dbView.selectionChanged();
    } else {
      // Previous code was concerned about avoiding updating commands on the
      //  assumption that only the selection count mattered.  We no longer
      //  make this assumption.
      // Things that may surprise you about the call to treeSelection.select:
      // 1) This ends up calling the onselect method defined on the XUL 'tree'
      //    tag.  For the 3pane this is the ThreadPaneSelectionChanged method in
      //    threadPane.js.  That code checks a global to see if it is dealing
      //    with a right-click, and ignores it if so.
      treeSelection.select(aViewIndex);
    }

    if (this._active) {
      this.ensureRowIsVisible(aViewIndex);
    }

    // The saved selection is invalidated, since we've got something newer
    this._savedSelection = null;

    // Do this here instead of at the beginning to prevent reentrancy issues
    this._aboutToSelectMessage = false;
  },

  /**
   * For every selected message in the display that is part of a (displayed)
   *  thread and is not the root message, de-select it and ensure that the
   *  root message of the thread is selected.
   * This is primarily intended to be used when collapsing visible threads.
   *
   * We do nothing if we are not in a threaded display mode.
   */
  selectSelectedThreadRoots() {
    if (!this.view.showThreaded) {
      return;
    }

    // There are basically two implementation strategies available to us:
    // 1) For each selected view index with a level > 0, keep walking 'up'
    //    (numerically smaller) until we find a message with level 0.
    //    The inefficiency here is the potentially large number of JS calls
    //    into XPCOM space that will be required.
    // 2) Ask for the thread that each view index belongs to, use that to
    //    efficiently retrieve the thread root, then find the root using
    //    the message header.  The inefficiency here is that the view
    //    currently does a linear scan, albeit a relatively efficient one.
    // And the winner is... option 2, because the code is simpler because we
    //  can reuse selectMessages to do most of the work.
    let selectedIndices = this.selectedIndices;
    let newSelectedMessages = [];
    let dbView = this.view.dbView;
    for (let index of selectedIndices) {
      let thread = dbView.getThreadContainingIndex(index);
      newSelectedMessages.push(thread.getRootHdr());
    }
    this.selectMessages(newSelectedMessages);
  },

  // @}

  /**
   * @name Ensure Visibility
   */
  // @{

  /**
   * Minimum number of lines to display between the 'focused' message and the
   *  top / bottom of the thread pane.
   */
  get visibleRowPadding() {
    let topPadding, bottomPadding;

    // If we can get the height of the folder pane, treat the values as
    //  percentages of that.
    if (this.tree) {
      let topPercentPadding = Services.prefs.getIntPref(
        "mail.threadpane.padding.top_percent"
      );
      let bottomPercentPadding = Services.prefs.getIntPref(
        "mail.threadpane.padding.bottom_percent"
      );

      // Assume the bottom row is half-visible and should generally be ignored.
      // (We could actually do the legwork to see if there is a partial one...)
      let paneHeight = this.tree.getPageLength() - 1;

      // Convert from percentages to absolute row counts.
      topPadding = Math.ceil((topPercentPadding / 100) * paneHeight);
      bottomPadding = Math.ceil((bottomPercentPadding / 100) * paneHeight);

      // We need one visible row not counted in either padding, for the actual
      //  target message. Also helps correct for rounding errors.
      if (topPadding + bottomPadding > paneHeight) {
        if (topPadding > bottomPadding) {
          topPadding--;
        } else {
          bottomPadding--;
        }
      }
    } else {
      // Something's gone wrong elsewhere, and we likely have bigger problems.
      topPadding = 0;
      bottomPadding = 0;
      console.error("Unable to get height of folder pane (treeBox is null)");
    }

    return [topPadding, bottomPadding];
  },

  /**
   * Ensure the given view index is visible, optionally with some padding.
   * By padding, we mean that the index will not be the first or last message
   *  displayed, but rather have messages on either side.
   * We have the concept of a 'lip' when we are at the end of the message
   *  display.  If we are near the end of the display, we want to show an
   *  empty row (at the bottom) so the user knows they are at the end.  Also,
   *  if a message shows up that is new and things are sorted ascending, this
   *  turns out to be useful.
   */
  ensureRowIsVisible(aViewIndex, aBounced) {
    // Dealing with the tree view layout is a nightmare, let's just always make
    //  sure we re-schedule ourselves.  The most particular rationale here is
    //  that the message pane may be toggling its state and it's much simpler
    //  and reliable if we ensure that all of FolderDisplayWidget's state
    //  change logic gets to run to completion before we run ourselves.
    if (!aBounced) {
      let dis = this;
      window.setTimeout(function () {
        dis.ensureRowIsVisible(aViewIndex, true);
      }, 0);
    }

    let tree = this.tree;
    if (!tree || !tree.view) {
      return;
    }

    // try and trigger a reflow...
    tree.getBoundingClientRect();

    let maxIndex = tree.view.rowCount - 1;

    let first = tree.getFirstVisibleRow();
    // Assume the bottom row is half-visible and should generally be ignored.
    // (We could actually do the legwork to see if there is a partial one...)
    const halfVisible = 1;
    let last = tree.getLastVisibleRow() - halfVisible;
    let span = tree.getPageLength() - halfVisible;
    let [topPadding, bottomPadding] = this.visibleRowPadding;

    let target;
    if (aViewIndex >= last - bottomPadding) {
      // The index is after the last visible guy (with padding),
      // move down so that the target index is padded in 1 from the bottom.
      target = Math.min(maxIndex, aViewIndex + bottomPadding) - span;
    } else if (aViewIndex <= first + topPadding) {
      // The index is before the first visible guy (with padding), move up.
      target = Math.max(0, aViewIndex - topPadding);
    } else {
      // It is already visible.
      return;
    }

    // this sets the first visible row
    tree.scrollToRow(target);
  },

  /**
   * Ensure that the given range of rows is maximally visible in the thread
   *  pane.  If the range is larger than the number of rows that can be
   *  displayed in the thread pane, we bias towards showing the min row (with
   *  padding).
   *
   * @param aMinRow The numerically smallest row index defining the start of
   *     the inclusive range.
   * @param aMaxRow The numberically largest row index defining the end of the
   *     inclusive range.
   */
  ensureRowRangeIsVisible(aMinRow, aMaxRow, aBounced) {
    // Dealing with the tree view layout is a nightmare, let's just always make
    //  sure we re-schedule ourselves.  The most particular rationale here is
    //  that the message pane may be toggling its state and it's much simpler
    //  and reliable if we ensure that all of FolderDisplayWidget's state
    //  change logic gets to run to completion before we run ourselves.
    if (!aBounced) {
      let dis = this;
      window.setTimeout(function () {
        dis.ensureRowRangeIsVisible(aMinRow, aMaxRow, true);
      }, 0);
    }

    let tree = this.tree;
    if (!tree) {
      return;
    }
    let first = tree.getFirstVisibleRow();
    const halfVisible = 1;
    let last = tree.getLastVisibleRow() - halfVisible;
    let span = tree.getPageLength() - halfVisible;
    let [topPadding, bottomPadding] = this.visibleRowPadding;

    // bail if the range is already visible with padding constraints handled
    if (first + topPadding <= aMinRow && last - bottomPadding >= aMaxRow) {
      return;
    }

    let target;
    // if the range is bigger than we can fit, optimize position for the min row
    //  with padding to make it obvious the range doesn't extend above the row.
    if (aMaxRow - aMinRow > span) {
      target = Math.max(0, aMinRow - topPadding);
    } else {
      // So the range must fit, and it's a question of how we want to position
      //  it.  For now, the answer is we try and center it, why not.
      let rowSpan = aMaxRow - aMinRow + 1;
      let halfSpare = Math.floor(
        (span - rowSpan - topPadding - bottomPadding) / 2
      );
      target = aMinRow - halfSpare - topPadding;
    }
    tree.scrollToRow(target);
  },

  /**
   * Ensure that the selection is visible to the extent possible.
   */
  ensureSelectionIsVisible() {
    let treeSelection = this.treeSelection; // potentially magic getter
    if (!treeSelection || !treeSelection.count) {
      return;
    }

    let minRow = null,
      maxRow = null;

    let rangeCount = treeSelection.getRangeCount();
    for (let iRange = 0; iRange < rangeCount; iRange++) {
      let rangeMinObj = {},
        rangeMaxObj = {};
      treeSelection.getRangeAt(iRange, rangeMinObj, rangeMaxObj);
      let rangeMin = rangeMinObj.value,
        rangeMax = rangeMaxObj.value;
      if (minRow == null || rangeMin < minRow) {
        minRow = rangeMin;
      }
      if (maxRow == null || rangeMax > maxRow) {
        maxRow = rangeMax;
      }
    }

    this.ensureRowRangeIsVisible(minRow, maxRow);
  },
  // @}
};

function SetNewsFolderColumns() {
  var sizeColumn = document.getElementById("sizeCol");
  var bundle = document.getElementById("bundle_messenger");

  if (gDBView.usingLines) {
    sizeColumn.setAttribute("label", bundle.getString("linesColumnHeader"));
    sizeColumn.setAttribute(
      "tooltiptext",
      bundle.getString("linesColumnTooltip2")
    );
  } else {
    sizeColumn.setAttribute("label", bundle.getString("sizeColumnHeader"));
    sizeColumn.setAttribute(
      "tooltiptext",
      bundle.getString("sizeColumnTooltip2")
    );
  }
}

function UpdateStatusQuota(folder) {
  if (!document.getElementById("quotaPanel")) {
    // No quotaPanel in here, like for the search window.
    return;
  }

  if (!(folder && folder instanceof Ci.nsIMsgImapMailFolder)) {
    document.getElementById("quotaPanel").hidden = true;
    return;
  }

  let quotaUsagePercentage = q =>
    Number((100n * BigInt(q.usage)) / BigInt(q.limit));

  // For display on main window panel only include quota names containing
  // "STORAGE" or "MESSAGE". This will exclude unusual quota names containing
  // items like "MAILBOX" and "LEVEL" from the panel bargraph. All quota names
  // will still appear on the folder properties quota window.
  // Note: Quota name is typically something like "User Quota / STORAGE".
  let folderQuota = folder
    .getQuota()
    .filter(
      quota =>
        quota.name.toUpperCase().includes("STORAGE") ||
        quota.name.toUpperCase().includes("MESSAGE")
    );
  // If folderQuota not empty, find the index of the element with highest
  //  percent usage and determine if it is above the panel display threshold.
  if (folderQuota.length > 0) {
    let highest = folderQuota.reduce((acc, current) =>
      quotaUsagePercentage(acc) > quotaUsagePercentage(current) ? acc : current
    );
    let percent = quotaUsagePercentage(highest);
    if (
      percent <
      Services.prefs.getIntPref("mail.quota.mainwindow_threshold.show")
    ) {
      document.getElementById("quotaPanel").hidden = true;
    } else {
      document.getElementById("quotaPanel").hidden = false;
      document.getElementById("quotaMeter").setAttribute("value", percent);
      var bundle = document.getElementById("bundle_messenger");
      document.getElementById("quotaLabel").value = bundle.getFormattedString(
        "percent",
        [percent]
      );
      document.getElementById("quotaLabel").tooltipText =
        bundle.getFormattedString("quotaTooltip2", [
          highest.usage,
          highest.limit,
        ]);
      let quotaPanel = document.getElementById("quotaPanel");
      if (
        percent <
        Services.prefs.getIntPref("mail.quota.mainwindow_threshold.warning")
      ) {
        quotaPanel.classList.remove("alert-warning", "alert-critical");
      } else if (
        percent <
        Services.prefs.getIntPref("mail.quota.mainwindow_threshold.critical")
      ) {
        quotaPanel.classList.remove("alert-critical");
        quotaPanel.classList.add("alert-warning");
      } else {
        quotaPanel.classList.remove("alert-warning");
        quotaPanel.classList.add("alert-critical");
      }
    }
  } else {
    document.getElementById("quotaPanel").hidden = true;
  }
}
