Bug 1460579 - Replace the 'bookmarkPropertiesDialog/folderLastUsed' annotation with a key/value pair in moz_meta. r?mak draft
authorMark Banner <standard8@mozilla.com>
Fri, 11 May 2018 07:35:59 +0100
changeset 804158 3fe2f732649866ea03d9843c4d269e11d0975ac6
parent 804157 752465b44c793318cef36df46ca5ff00c3d8854a
push id112312
push userbmo:standard8@mozilla.com
push dateTue, 05 Jun 2018 16:07:56 +0000
reviewersmak
bugs1460579
milestone62.0a1
Bug 1460579 - Replace the 'bookmarkPropertiesDialog/folderLastUsed' annotation with a key/value pair in moz_meta. r?mak MozReview-Commit-ID: GpEPxOMDret
browser/base/content/browser-places.js
browser/components/places/PlacesUIUtils.jsm
browser/components/places/content/editBookmark.js
browser/components/places/tests/browser/browser.ini
browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/head_migration.js
toolkit/components/places/tests/migration/test_current_from_v43.js
toolkit/components/places/tests/migration/test_current_from_v50.js
toolkit/components/places/tests/migration/xpcshell.ini
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -14,16 +14,19 @@ var StarUI = {
   _batching: false,
   _isNewBookmark: false,
   _isComposing: false,
   _autoCloseTimer: 0,
   // The autoclose timer is diasbled if the user interacts with the
   // popup, such as making a change through typing or clicking on
   // the popup.
   _autoCloseTimerEnabled: true,
+  // The autoclose timeout length. 3500ms matches the timeout that Pocket uses
+  // in browser/extensions/pocket/content/panels/js/saved.js.
+  _autoCloseTimeout: 3500,
   _removeBookmarksOnPopupHidden: false,
 
   _element(aID) {
     return document.getElementById(aID);
   },
 
   // Edit-bookmark panel
   get panel() {
@@ -79,18 +82,23 @@ var StarUI = {
       case "mousemove":
         clearTimeout(this._autoCloseTimer);
         // The autoclose timer is not disabled on generic mouseout
         // because the user may not have actually interacted with the popup.
         break;
       case "popuphidden": {
         clearTimeout(this._autoCloseTimer);
         if (aEvent.originalTarget == this.panel) {
-          if (!this._element("editBookmarkPanelContent").hidden)
+          let selectedFolderGuid;
+
+          if (!this._element("editBookmarkPanelContent").hidden) {
+            // Get the folder first, before we uninit the overlay.
+            selectedFolderGuid = gEditItemOverlay.selectedFolderGuid;
             this.quitEditMode();
+          }
 
           if (this._anchorToolbarButton) {
             this._anchorToolbarButton.removeAttribute("open");
             this._anchorToolbarButton = null;
           }
           this._restoreCommandsState();
           let removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
           this._removeBookmarksOnPopupHidden = false;
@@ -108,16 +116,20 @@ var StarUI = {
             }
             // Remove all bookmarks for the bookmark's url, this also removes
             // the tags for the url.
             PlacesTransactions.Remove(guidsForRemoval)
                               .transact().catch(Cu.reportError);
           } else if (this._isNewBookmark) {
             LibraryUI.triggerLibraryAnimation("bookmark");
           }
+
+          if (!removeBookmarksOnPopupHidden) {
+            this._storeRecentlyUsedFolder(selectedFolderGuid).catch(console.error);
+          }
         }
         break;
       }
       case "keypress":
         clearTimeout(this._autoCloseTimer);
         this._autoCloseTimerEnabled = false;
 
         if (aEvent.defaultPrevented) {
@@ -181,19 +193,17 @@ var StarUI = {
         // Explicit fall-through
       case "popupshown":
         // Don't handle events for descendent elements.
         if (aEvent.target != aEvent.currentTarget) {
           break;
         }
         // auto-close if new and not interacted with
         if (this._isNewBookmark && !this._isComposing) {
-          // 3500ms matches the timeout that Pocket uses in
-          // browser/extensions/pocket/content/panels/js/saved.js
-          let delay = 3500;
+          let delay = this._autoCloseTimeout;
           if (this._closePanelQuickForTesting) {
             delay /= 10;
           }
           clearTimeout(this._autoCloseTimer);
           this._autoCloseTimer = setTimeout(() => {
             if (!this.panel.mozMatchesSelector(":hover")) {
               this.panel.hidePopup(true);
             }
@@ -335,16 +345,42 @@ var StarUI = {
 
   endBatch() {
     if (!this._batching)
       return;
 
     this._batchBlockingDeferred.resolve();
     this._batchBlockingDeferred = null;
     this._batching = false;
+  },
+
+  async _storeRecentlyUsedFolder(selectedFolderGuid) {
+    // These are displayed by default, so don't save the folder for them.
+    if (PlacesUtils.bookmarks.userContentRoots.includes(selectedFolderGuid)) {
+      return;
+    }
+
+    // List of recently used folders:
+    let lastUsedFolderGuids =
+      await PlacesUtils.metadata.get(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY, []);
+
+    let index = lastUsedFolderGuids.indexOf(selectedFolderGuid);
+    if (index > 1) {
+      // The guid is in the array but not the most recent.
+      lastUsedFolderGuids.splice(index, 1);
+      lastUsedFolderGuids.unshift(selectedFolderGuid);
+    } else if (index == -1) {
+      lastUsedFolderGuids.unshift(selectedFolderGuid);
+    }
+    if (lastUsedFolderGuids.length > 5) {
+      lastUsedFolderGuids.pop();
+    }
+
+    await PlacesUtils.metadata.set(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
+      lastUsedFolderGuids);
   }
 };
 
 var PlacesCommandHook = {
   /**
    * Adds a bookmark to the page loaded in the given browser.
    *
    * @param aBrowser
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -206,16 +206,17 @@ let InternalFaviconLoader = {
     let loadDataForWindow = gFaviconLoadDataMap.get(win);
     loadDataForWindow.push(loadData);
   },
 };
 
 var PlacesUIUtils = {
   LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
   DESCRIPTION_ANNO: "bookmarkProperties/description",
+  LAST_USED_FOLDERS_META_KEY: "bookmarks/lastusedfolders",
 
   /**
    * Makes a URI from a spec, and do fixup
    * @param   aSpec
    *          The string spec of the URI
    * @return A URI object for the spec.
    */
   createFixedURI: function PUIU_createFixedURI(aSpec) {
@@ -1027,17 +1028,17 @@ var PlacesUIUtils = {
 
   setMouseoverURL(url, win) {
     // When the browser window is closed with an open sidebar, the sidebar
     // unload event happens after the browser's one.  In this case
     // top.XULBrowserWindow has been nullified already.
     if (win.top.XULBrowserWindow) {
       win.top.XULBrowserWindow.setOverLink(url, null);
     }
-  }
+  },
 };
 
 // These are lazy getters to avoid importing PlacesUtils immediately.
 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => {
   return [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
           PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
           PlacesUtils.TYPE_X_MOZ_PLACE];
 });
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -1,15 +1,14 @@
 /* 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/. */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
 const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
 
 var gEditItemOverlay = {
   _observersAdded: false,
   _staticFoldersListBuilt: false,
 
   _paneInfo: null,
   _setPaneInfo(aInitInfo) {
@@ -41,33 +40,31 @@ var gEditItemOverlay = {
     let uri = isURI || isTag ? Services.io.newURI(node.uri) : null;
     let title = node ? node.title : null;
     let isBookmark = isItem && isURI;
     let bulkTagging = !node;
     let uris = bulkTagging ? aInitInfo.uris : null;
     let visibleRows = new Set();
     let isParentReadOnly = false;
     let postData = aInitInfo.postData;
-    let parentId = -1;
     let parentGuid = null;
 
     if (node && isItem) {
       if (!node.parent || (node.parent.itemId > 0 && !node.parent.bookmarkGuid)) {
         throw new Error("Cannot use an incomplete node to initialize the edit bookmark panel");
       }
       let parent = node.parent;
       isParentReadOnly = !PlacesUtils.nodeIsFolder(parent);
-      parentId = parent.itemId;
       parentGuid = parent.bookmarkGuid;
     }
 
     let focusedElement = aInitInfo.focusedElement;
     let onPanelReady = aInitInfo.onPanelReady;
 
-    return this._paneInfo = { itemId, itemGuid, parentId, parentGuid, isItem,
+    return this._paneInfo = { itemId, itemGuid, parentGuid, isItem,
                               isURI, uri, title,
                               isBookmark, isFolderShortcut, isParentReadOnly,
                               bulkTagging, uris,
                               visibleRows, postData, isTag, focusedElement,
                               onPanelReady, tag };
   },
 
   get initialized() {
@@ -204,17 +201,17 @@ var gEditItemOverlay = {
       }
     }
 
     // For sanity ensure that the implementer has uninited the panel before
     // trying to init it again, or we could end up leaking due to observers.
     if (this.initialized)
       this.uninitPanel(false);
 
-    let { parentId, isItem, isURI,
+    let { parentGuid, isItem, isURI,
           isBookmark, bulkTagging, uris,
           visibleRows, focusedElement,
           onPanelReady } = this._setPaneInfo(aInfo);
 
     let showOrCollapse =
       (rowId, isAppropriateForInput, nameInHiddenRows = null) => {
         let visible = isAppropriateForInput;
         if (visible && "hiddenRows" in aInfo && nameInHiddenRows)
@@ -253,17 +250,17 @@ var gEditItemOverlay = {
       this._initLoadInSidebar();
     }
 
     // Folder picker.
     // Technically we should check that the item is not moveable, but that's
     // not cheap (we don't always have the parent), and there's no use case for
     // this (it's only the Star UI that shows the folderPicker)
     if (showOrCollapse("folderRow", isItem, "folderPicker")) {
-      this._initFolderMenuList(parentId).catch(Cu.reportError);
+      this._initFolderMenuList(parentGuid).catch(Cu.reportError);
     }
 
     // Selection count.
     if (showOrCollapse("selectionCount", bulkTagging)) {
       this._element("itemsCountText").value =
         PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
                                       uris.length,
                                       [uris.length]);
@@ -352,97 +349,84 @@ var gEditItemOverlay = {
       }
     }
   },
 
   /**
    * Appends a menu-item representing a bookmarks folder to a menu-popup.
    * @param aMenupopup
    *        The popup to which the menu-item should be added.
-   * @param aFolderId
+   * @param aFolderGuid
    *        The identifier of the bookmarks folder.
    * @param aTitle
    *        The title to use as a label.
    * @return the new menu item.
    */
-  _appendFolderItemToMenupopup(aMenupopup, aFolderId, aTitle) {
+  _appendFolderItemToMenupopup(aMenupopup, aFolderGuid, aTitle) {
     // First make sure the folders-separator is visible
     this._element("foldersSeparator").hidden = false;
 
     var folderMenuItem = document.createElement("menuitem");
-    var folderTitle = aTitle;
-    folderMenuItem.folderId = aFolderId;
-    folderMenuItem.setAttribute("label", folderTitle);
+    folderMenuItem.folderGuid = aFolderGuid;
+    folderMenuItem.setAttribute("label", aTitle);
     folderMenuItem.className = "menuitem-iconic folder-icon";
     aMenupopup.appendChild(folderMenuItem);
     return folderMenuItem;
   },
 
-  async _initFolderMenuList(aSelectedFolder) {
+  async _initFolderMenuList(aSelectedFolderGuid) {
     // clean up first
     var menupopup = this._folderMenuList.menupopup;
     while (menupopup.childNodes.length > 6)
       menupopup.removeChild(menupopup.lastChild);
 
     // Build the static list
     if (!this._staticFoldersListBuilt) {
       let unfiledItem = this._element("unfiledRootItem");
       unfiledItem.label = PlacesUtils.getString("OtherBookmarksFolderTitle");
-      unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
+      unfiledItem.folderGuid = PlacesUtils.bookmarks.unfiledGuid;
       let bmMenuItem = this._element("bmRootItem");
       bmMenuItem.label = PlacesUtils.getString("BookmarksMenuFolderTitle");
-      bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
+      bmMenuItem.folderGuid = PlacesUtils.bookmarks.menuGuid;
       let toolbarItem = this._element("toolbarFolderItem");
       toolbarItem.label = PlacesUtils.getString("BookmarksToolbarFolderTitle");
-      toolbarItem.folderId = PlacesUtils.toolbarFolderId;
+      toolbarItem.folderGuid = PlacesUtils.bookmarks.toolbarGuid;
       this._staticFoldersListBuilt = true;
     }
 
     // List of recently used folders:
-    var folderIds =
-      PlacesUtils.annotations.getItemsWithAnnotation(LAST_USED_ANNO);
+    let lastUsedFolderGuids =
+      await PlacesUtils.metadata.get(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY, []);
 
     /**
-     * The value of the LAST_USED_ANNO annotation is the time (in the form of
-     * Date.getTime) at which the folder has been last used.
+     * The list of last used folders is sorted in most-recent first order.
      *
      * First we build the annotated folders array, each item has both the
      * folder identifier and the time at which it was last-used by this dialog
      * set. Then we sort it descendingly based on the time field.
      */
     this._recentFolders = [];
-    for (let folderId of folderIds) {
-      var lastUsed =
-        PlacesUtils.annotations.getItemAnnotation(folderId, LAST_USED_ANNO);
-      let guid = await PlacesUtils.promiseItemGuid(folderId);
+    for (let guid of lastUsedFolderGuids) {
       let bm = await PlacesUtils.bookmarks.fetch(guid);
-      // Since this could be a root mobile folder, we should get the proper
-      // title.
-      let title = PlacesUtils.bookmarks.getLocalizedTitle(bm);
-      this._recentFolders.push({ folderId, guid, title, lastUsed });
+      if (bm) {
+        let title = PlacesUtils.bookmarks.getLocalizedTitle(bm);
+        this._recentFolders.push({ guid, title });
+      }
     }
-    this._recentFolders.sort(function(a, b) {
-      if (b.lastUsed < a.lastUsed)
-        return -1;
-      if (b.lastUsed > a.lastUsed)
-        return 1;
-      return 0;
-    });
 
     var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST,
                                  this._recentFolders.length);
     for (let i = 0; i < numberOfItems; i++) {
       await this._appendFolderItemToMenupopup(menupopup,
-                                              this._recentFolders[i].folderId,
+                                              this._recentFolders[i].guid,
                                               this._recentFolders[i].title);
     }
 
-    let selectedFolderGuid = await PlacesUtils.promiseItemGuid(aSelectedFolder);
-    let title = (await PlacesUtils.bookmarks.fetch(selectedFolderGuid)).title;
-    var defaultItem = this._getFolderMenuItem(aSelectedFolder, title);
+    let title = (await PlacesUtils.bookmarks.fetch(aSelectedFolderGuid)).title;
+    var defaultItem = this._getFolderMenuItem(aSelectedFolderGuid, title);
     this._folderMenuList.selectedItem = defaultItem;
 
     // Set a selectedIndex attribute to show special icons
     this._folderMenuList.setAttribute("selectedIndex",
                                       this._folderMenuList.selectedIndex);
 
     // Hide the folders-separator if no folder is annotated as recently-used
     this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
@@ -473,16 +457,20 @@ var gEditItemOverlay = {
       PlacesUtils.bookmarks.removeObserver(this);
       this._observersAdded = false;
     }
 
     this._setPaneInfo(null);
     this._firstEditedField = "";
   },
 
+  get selectedFolderGuid() {
+    return this._folderMenuList.selectedItem && this._folderMenuList.selectedItem.folderGuid;
+  },
+
   onTagsFieldChange() {
     // Check for _paneInfo existing as the dialog may be closing but receiving
     // async updates from unresolved promises.
     if (this._paneInfo &&
         (this._paneInfo.isURI || this._paneInfo.bulkTagging)) {
       this._updateTags().then(
         anyChanges => {
           // Check _paneInfo here as we might be closing the dialog.
@@ -690,145 +678,102 @@ var gEditItemOverlay = {
     }
   },
 
   /**
    * Get the corresponding menu-item in the folder-menu-list for a bookmarks
    * folder if such an item exists. Otherwise, this creates a menu-item for the
    * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
    * the new item replaces the last menu-item.
-   * @param aFolderId
+   * @param aFolderGuid
    *        The identifier of the bookmarks folder.
    * @param aTitle
    *        The title to use in case of menuitem creation.
    * @return handle to the menuitem.
    */
-  _getFolderMenuItem(aFolderId, aTitle) {
+  _getFolderMenuItem(aFolderGuid, aTitle) {
     let menupopup = this._folderMenuList.menupopup;
     let menuItem = Array.prototype.find.call(
-      menupopup.childNodes, item => item.folderId === aFolderId);
+      menupopup.childNodes, item => item.folderGuid === aFolderGuid);
     if (menuItem !== undefined)
       return menuItem;
 
     // 3 special folders + separator + folder-items-count limit
     if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
       menupopup.removeChild(menupopup.lastChild);
 
-    return this._appendFolderItemToMenupopup(menupopup, aFolderId, aTitle);
+    return this._appendFolderItemToMenupopup(menupopup, aFolderGuid, aTitle);
   },
 
   async onFolderMenuListCommand(aEvent) {
     // Check for _paneInfo existing as the dialog may be closing but receiving
     // async updates from unresolved promises.
     if (!this._paneInfo) {
       return;
     }
     // Set a selectedIndex attribute to show special icons
     this._folderMenuList.setAttribute("selectedIndex",
                                       this._folderMenuList.selectedIndex);
 
     if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
       // reset the selection back to where it was and expand the tree
       // (this menu-item is hidden when the tree is already visible
-      let item = this._getFolderMenuItem(this._paneInfo.parentId,
+      let item = this._getFolderMenuItem(this._paneInfo.parentGuid,
                                          this._paneInfo.title);
       this._folderMenuList.selectedItem = item;
       // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
       // menulist right away
       setTimeout(() => this.toggleFolderTreeVisibility(), 100);
       return;
     }
 
     // Move the item
-    let containerId = this._folderMenuList.selectedItem.folderId;
-    if (this._paneInfo.parentId != containerId &&
-        this._paneInfo.itemId != containerId) {
-      let newParentGuid = await PlacesUtils.promiseItemGuid(containerId);
-      let guid = this._paneInfo.itemGuid;
-      await PlacesTransactions.Move({ guid, newParentGuid }).transact();
-
-      // Mark the containing folder as recently-used if it isn't in the
-      // static list
-      if (containerId != PlacesUtils.unfiledBookmarksFolderId &&
-          containerId != PlacesUtils.toolbarFolderId &&
-          containerId != PlacesUtils.bookmarksMenuFolderId) {
-        this._markFolderAsRecentlyUsed(containerId)
-            .catch(Cu.reportError);
-      }
+    let containerGuid = this._folderMenuList.selectedItem.folderGuid;
+    if (this._paneInfo.parentGuid != containerGuid &&
+        this._paneInfo.itemGuid != containerGuid) {
+      await PlacesTransactions.Move({
+        guid: this._paneInfo.itemGuid,
+        newParentGuid: containerGuid
+      }).transact();
 
       // Auto-show the bookmarks toolbar when adding / moving an item there.
-      if (containerId == PlacesUtils.toolbarFolderId) {
+      if (containerGuid == PlacesUtils.bookmarks.toolbarGuid) {
         Services.obs.notifyObservers(null, "autoshow-bookmarks-toolbar");
       }
     }
 
     // Update folder-tree selection
     var folderTreeRow = this._element("folderTreeRow");
     if (!folderTreeRow.collapsed) {
       var selectedNode = this._folderTree.selectedNode;
       if (!selectedNode ||
-          PlacesUtils.getConcreteItemId(selectedNode) != containerId)
-        this._folderTree.selectItems([containerId]);
+          PlacesUtils.getConcreteItemGuid(selectedNode) != containerGuid)
+        this._folderTree.selectItems([containerGuid]);
     }
   },
 
   onFolderTreeSelect() {
     var selectedNode = this._folderTree.selectedNode;
 
     // Disable the "New Folder" button if we cannot create a new folder
     this._element("newFolderButton")
         .disabled = !this._folderTree.insertionPoint || !selectedNode;
 
     if (!selectedNode)
       return;
 
-    var folderId = PlacesUtils.getConcreteItemId(selectedNode);
-    if (this._folderMenuList.selectedItem.folderId == folderId)
+    var folderGuid = PlacesUtils.getConcreteItemGuid(selectedNode);
+    if (this._folderMenuList.selectedItem.folderGuid == folderGuid)
       return;
 
-    var folderItem = this._getFolderMenuItem(folderId, selectedNode.title);
+    var folderItem = this._getFolderMenuItem(folderGuid, selectedNode.title);
     this._folderMenuList.selectedItem = folderItem;
     folderItem.doCommand();
   },
 
-  async _markFolderAsRecentlyUsed(aFolderId) {
-    // Expire old unused recent folders.
-    let guids = [];
-    while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
-      let folderId = this._recentFolders.pop().folderId;
-      let guid = await PlacesUtils.promiseItemGuid(folderId);
-      guids.push(guid);
-    }
-    if (guids.length > 0) {
-      let annotation = this._getLastUsedAnnotationObject(false);
-      PlacesTransactions.Annotate({ guids, annotation  })
-                        .transact().catch(Cu.reportError);
-    }
-
-    // Mark folder as recently used
-    let annotation = this._getLastUsedAnnotationObject(true);
-    let guid = await PlacesUtils.promiseItemGuid(aFolderId);
-    PlacesTransactions.Annotate({ guid, annotation })
-                      .transact().catch(Cu.reportError);
-  },
-
-  /**
-   * Returns an object which could then be used to set/unset the
-   * LAST_USED_ANNO annotation for a folder.
-   *
-   * @param aLastUsed
-   *        Whether to set or unset the LAST_USED_ANNO annotation.
-   * @returns an object representing the annotation which could then be used
-   *          with the transaction manager.
-   */
-  _getLastUsedAnnotationObject(aLastUsed) {
-    return { name: LAST_USED_ANNO,
-             value: aLastUsed ? new Date().getTime() : null };
-  },
-
   _rebuildTagsSelectorList() {
     let tagsSelector = this._element("tagsSelector");
     let tagsSelectorRow = this._element("tagsSelectorRow");
     if (tagsSelectorRow.collapsed)
       return;
 
     // Save the current scroll position and restore it after the rebuild.
     let firstIndex = tagsSelector.getIndexOfFirstVisibleRow();
@@ -1069,28 +1014,27 @@ var gEditItemOverlay = {
   },
 
   onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, type, guid,
               oldParentGuid, newParentGuid) {
     if (!this._paneInfo.isItem || this._paneInfo.itemId != id) {
       return;
     }
 
-    this._paneInfo.parentId = newParentId;
     this._paneInfo.parentGuid = newParentGuid;
 
     if (!this._paneInfo.visibleRows.has("folderRow") ||
-        newParentId == this._folderMenuList.selectedItem.folderId) {
+        newParentGuid == this._folderMenuList.selectedItem.folderGuid) {
       return;
     }
 
     // Just setting selectItem _does not_ trigger oncommand, so we don't
     // recurse.
     PlacesUtils.bookmarks.fetch(newParentGuid).then(bm => {
-      this._folderMenuList.selectedItem = this._getFolderMenuItem(newParentId,
+      this._folderMenuList.selectedItem = this._getFolderMenuItem(newParentGuid,
                                                                   bm.title);
     });
   },
 
   onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI) {
     this._lastNewItem = aItemId;
   },
 
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -30,16 +30,17 @@ support-files =
 [browser_bookmarkProperties_addKeywordForThisSearch.js]
 [browser_bookmarkProperties_addLivemark.js]
 [browser_bookmarkProperties_bookmarkAllTabs.js]
 [browser_bookmarkProperties_cancel.js]
 [browser_bookmarkProperties_editFolder.js]
 [browser_bookmarkProperties_editTagContainer.js]
 [browser_bookmarkProperties_no_user_actions.js]
 [browser_bookmarkProperties_readOnlyRoot.js]
+[browser_bookmarkProperties_remember_folders.js]
 [browser_bookmarksProperties.js]
 [browser_check_correct_controllers.js]
 [browser_click_bookmarks_on_toolbar.js]
 [browser_controller_onDrop_sidebar.js]
 [browser_controller_onDrop_tagFolder.js]
 [browser_controller_onDrop.js]
 [browser_copy_query_without_tree.js]
 subsuite = clipboard
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Tests that multiple tags can be added to a bookmark using the star-shaped button, the library and the sidebar.
+ */
+
+const bookmarkPanel = document.getElementById("editBookmarkPanel");
+let folders;
+
+async function clickBookmarkStar() {
+  let shownPromise = promisePopupShown(bookmarkPanel);
+  BookmarkingUI.star.click();
+  await shownPromise;
+}
+
+async function hideBookmarksPanel() {
+  let hiddenPromise = promisePopupHidden(bookmarkPanel);
+  // Confirm and close the dialog.
+  document.getElementById("editBookmarkPanelDoneButton").click();
+  await hiddenPromise;
+}
+
+async function openPopupAndSelectFolder(guid) {
+  await clickBookmarkStar();
+
+  // Expand the folder tree.
+  document.getElementById("editBMPanel_foldersExpander").click();
+  document.getElementById("editBMPanel_folderTree").selectItems([guid]);
+
+  await hideBookmarksPanel();
+  await PlacesTestUtils.promiseAsyncUpdates();
+}
+
+async function assertRecentFolders(expectedGuids, msg) {
+  await clickBookmarkStar();
+
+  let actualGuids = [];
+  function getGuids() {
+    const folderMenuPopup = document.getElementById("editBMPanel_folderMenuList").children[0];
+
+    let separatorFound = false;
+    // The list of folders goes from editBMPanel_foldersSeparator to the end.
+    for (let child of folderMenuPopup.children) {
+      if (separatorFound) {
+        actualGuids.push(child.folderGuid);
+      } else if (child.id == "editBMPanel_foldersSeparator") {
+        separatorFound = true;
+      }
+    }
+  }
+
+  await TestUtils.waitForCondition(() => {
+    getGuids();
+    return actualGuids.length == expectedGuids.length;
+  }, msg);
+
+  Assert.deepEqual(actualGuids, expectedGuids, msg);
+
+  await hideBookmarksPanel();
+}
+
+add_task(async function setup() {
+  await PlacesUtils.bookmarks.eraseEverything();
+  await PlacesUtils.metadata.delete(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY);
+
+  bookmarkPanel.setAttribute("animate", false);
+
+  let oldTimeout = StarUI._autoCloseTimeout;
+  // Make the timeout something big, so it doesn't iteract badly with tests.
+  StarUI._autoCloseTimeout = 6000000;
+
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: "about:robots",
+    waitForStateStop: true
+  });
+
+  folders = await PlacesUtils.bookmarks.insertTree({
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    children: [{
+      title: "Bob",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    }, {
+      title: "Place",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    }, {
+      title: "Delight",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    }, {
+      title: "Surprise",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    }, {
+      title: "Treble Bob",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    }, {
+      title: "Principal",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    }]
+  });
+
+  registerCleanupFunction(async () => {
+    StarUI._autoCloseTimeout = oldTimeout;
+    BrowserTestUtils.removeTab(tab);
+    bookmarkPanel.removeAttribute("animate");
+    await PlacesUtils.bookmarks.eraseEverything();
+    await PlacesUtils.metadata.delete(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY);
+  });
+});
+
+add_task(async function test_remember_last_folder() {
+  await assertRecentFolders([], "Should have no recent folders to start with.");
+
+  await openPopupAndSelectFolder(folders[0].guid);
+
+  await assertRecentFolders([folders[0].guid], "Should have one folder in the list.");
+});
+
+add_task(async function test_forget_oldest_folder() {
+  // Add some more folders.
+  let expectedFolders = [folders[0].guid];
+  for (let i = 1; i < folders.length; i++) {
+    await assertRecentFolders(expectedFolders,
+      "Should have only the expected folders in the list");
+
+    await openPopupAndSelectFolder(folders[i].guid);
+
+    expectedFolders.unshift(folders[i].guid);
+    if (expectedFolders.length > 5) {
+      expectedFolders.pop();
+    }
+  }
+
+  await assertRecentFolders(expectedFolders,
+    "Should have expired the original folder");
+});
+
+add_task(async function test_reorder_folders() {
+  let expectedFolders = [
+    folders[2].guid,
+    folders[5].guid,
+    folders[4].guid,
+    folders[3].guid,
+    folders[1].guid,
+  ];
+
+  // Take an old one and put it at the front.
+  await openPopupAndSelectFolder(folders[2].guid);
+
+  await assertRecentFolders(expectedFolders,
+    "Should have correctly re-ordered the list");
+});
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/ScopeExit.h"
+#include "mozilla/JSONWriter.h"
 
 #include "Database.h"
 
 #include "nsIAnnotationService.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIFile.h"
 #include "nsIWritablePropertyBag2.h"
 
@@ -106,16 +107,21 @@
 // Livemarks annotations.
 #define LMANNO_FEEDURI "livemark/feedURI"
 #define LMANNO_SITEURI "livemark/siteURI"
 
 // This is no longer used & obsolete except for during migration.
 // Note: it may still be found in older places databases.
 #define MOBILE_ROOT_ANNO "mobile/bookmarksRoot"
 
+// This annotation is no longer used & is obsolete, but here for migration.
+#define LAST_USED_ANNO NS_LITERAL_CSTRING("bookmarkPropertiesDialog/folderLastUsed")
+// This is key in the meta table that the LAST_USED_ANNO is migrated to.
+#define LAST_USED_FOLDERS_META_KEY NS_LITERAL_CSTRING("places/bookmarks/edit/lastusedfolder")
+
 // We use a fixed title for the mobile root to avoid marking the database as
 // corrupt if we can't look up the localized title in the string bundle. Sync
 // sets the title to the localized version when it creates the left pane query.
 #define MOBILE_ROOT_TITLE "mobile"
 
 using namespace mozilla;
 
 namespace mozilla {
@@ -1295,17 +1301,22 @@ Database::InitSchema(bool* aDatabaseMigr
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       if (currentSchemaVersion < 50) {
         rv = MigrateV50Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
-      // Firefox 62 uses schema version 50.
+      if (currentSchemaVersion < 51) {
+        rv = MigrateV51Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 62 uses schema version 51.
 
       // Schema Upgrades must add migration code here.
       // >>> IMPORTANT! <<<
       // NEVER MIX UP SYNC AND ASYNC EXECUTION IN MIGRATORS, YOU MAY LOCK THE
       // CONNECTION AND CAUSE FURTHER STEPS TO FAIL.
       // In case, set a bool and do the async work in the ScopeExit guard just
       // before the migration steps.
     }
@@ -2554,16 +2565,101 @@ Database::MigrateV50Up() {
     rv = syncStmt->Execute();
     if (NS_FAILED(rv)) return rv;
   }
 
   return NS_OK;
 }
 
 
+struct StringWriteFunc : public JSONWriteFunc
+{
+  nsCString& mCString;
+  explicit StringWriteFunc(nsCString& aCString) : mCString(aCString)
+  {
+  }
+  void Write(const char* aStr) override { mCString.Append(aStr); }
+};
+
+nsresult
+Database::MigrateV51Up()
+{
+  nsCOMPtr<mozIStorageStatement> stmt;
+  nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT b.guid FROM moz_anno_attributes n "
+    "JOIN moz_items_annos a ON n.id = a.anno_attribute_id "
+    "JOIN moz_bookmarks b ON a.item_id = b.id "
+    "WHERE n.name = :anno_name ORDER BY a.content DESC"
+  ), getter_AddRefs(stmt));
+  if (NS_FAILED(rv)) {
+    MOZ_ASSERT(false, "Should succeed unless item annotations table has been removed");
+    return NS_OK;
+  };
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"),
+                                  LAST_USED_ANNO);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsAutoCString json;
+  JSONWriter jw{ MakeUnique<StringWriteFunc>(json) };
+  jw.StartArrayProperty(nullptr, JSONWriter::SingleLineStyle);
+
+  bool hasMore = false;
+  uint32_t length;
+  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+    jw.StringElement(stmt->AsSharedUTF8String(0, &length));
+  }
+  jw.EndArray();
+
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "INSERT OR REPLACE INTO moz_meta "
+    "VALUES (:key, :value) "
+  ), getter_AddRefs(stmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("key"),
+                                  LAST_USED_FOLDERS_META_KEY);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("value"),
+                                  json);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = stmt->Execute();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Clean up the now redundant annotations.
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_items_annos WHERE anno_attribute_id = "
+      "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) "
+  ), getter_AddRefs(stmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), LAST_USED_ANNO);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = stmt->Execute();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_anno_attributes WHERE name = :anno_name "
+  ), getter_AddRefs(stmt));
+  if (NS_FAILED(rv)) {
+    MOZ_ASSERT(false, "Should succeed unless item annotations table has been removed");
+    return NS_OK;
+  };
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"),  LAST_USED_ANNO);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = stmt->Execute();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+
 nsresult
 Database::ConvertOldStyleQuery(nsCString& aURL)
 {
   AutoTArray<QueryKeyValuePair, 8> tokens;
   nsresult rv = TokenizeQueryString(aURL, &tokens);
   NS_ENSURE_SUCCESS(rv, rv);
 
   AutoTArray<QueryKeyValuePair, 8> newTokens;
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -14,17 +14,17 @@
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 #include "Shutdown.h"
 #include "nsCategoryCache.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 50
+#define DATABASE_SCHEMA_VERSION 51
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
 #define TOPIC_PROFILE_CHANGE_TEARDOWN "profile-change-teardown"
 // Fired when Places is shutting down.  Any code should stop accessing Places
@@ -332,16 +332,17 @@ protected:
   nsresult MigrateV43Up();
   nsresult MigrateV44Up();
   nsresult MigrateV45Up();
   nsresult MigrateV46Up();
   nsresult MigrateV47Up();
   nsresult MigrateV48Up();
   nsresult MigrateV49Up();
   nsresult MigrateV50Up();
+  nsresult MigrateV51Up();
 
   void MigrateV48Frecencies();
 
   nsresult UpdateBookmarkRootTitles();
 
   friend class ConnectionShutdownBlocker;
 
   int64_t CreateMobileRoot();
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,16 +1,13 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const CURRENT_SCHEMA_VERSION = 50;
-const FIRST_UPGRADABLE_SCHEMA_VERSION = 30;
-
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
 const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
 const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED;
 const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK;
--- a/toolkit/components/places/tests/migration/head_migration.js
+++ b/toolkit/components/places/tests/migration/head_migration.js
@@ -9,8 +9,40 @@ ChromeUtils.import("resource://gre/modul
 {
   /* import-globals-from ../head_common.js */
   let commonFile = do_get_file("../head_common.js", false);
   let uri = Services.io.newFileURI(commonFile);
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
 // Put any other stuff relative to this test folder below.
+
+const CURRENT_SCHEMA_VERSION = 51;
+const FIRST_UPGRADABLE_SCHEMA_VERSION = 30;
+
+async function assertAnnotationsRemoved(db, expectedAnnos) {
+  for (let anno of expectedAnnos) {
+    let rows = await db.execute(`
+      SELECT id FROM moz_anno_attributes
+      WHERE name = :anno
+    `, {anno});
+
+    Assert.equal(rows.length, 0, `${anno} should not exist in the database`);
+  }
+}
+
+async function assertNoOrphanAnnotations(db) {
+  let rows = await db.execute(`
+    SELECT item_id FROM moz_items_annos
+    WHERE item_id NOT IN (SELECT id from moz_bookmarks)
+  `);
+
+  Assert.equal(rows.length, 0,
+    `Should have no orphan annotations.`);
+
+  rows = await db.execute(`
+    SELECT id FROM moz_anno_attributes
+    WHERE id NOT IN (SELECT id from moz_items_annos)
+  `);
+
+  Assert.equal(rows.length, 0,
+    `Should have no orphan annotation attributes.`);
+}
--- a/toolkit/components/places/tests/migration/test_current_from_v43.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v43.js
@@ -111,24 +111,17 @@ add_task(async function test_tombstones_
 
   Assert.equal(rows.length, EXPECTED_REMOVED_BOOKMARK_GUIDS.length,
     "Should have removed all the expected bookmarks.");
 });
 
 add_task(async function test_annotations_removed() {
   let db = await PlacesUtils.promiseDBConnection();
 
-  for (let anno of EXPECTED_REMOVED_ANNOTATIONS) {
-    let rows = await db.execute(`
-      SELECT id FROM moz_anno_attributes
-      WHERE name = :anno
-    `, {anno});
-
-    Assert.equal(rows.length, 0, `${anno} should not exist in the database`);
-  }
+  await assertAnnotationsRemoved(db, EXPECTED_REMOVED_ANNOTATIONS);
 });
 
 add_task(async function test_check_history_entries() {
   let db = await PlacesUtils.promiseDBConnection();
 
   for (let entry of EXPECTED_REMOVED_PLACES_ENTRIES) {
     let rows = await db.execute(`
       SELECT id FROM moz_places
@@ -163,31 +156,17 @@ add_task(async function test_check_keywo
     Assert.equal(rows.length, 0,
       `Should have removed the expected keyword: ${keyword}.`);
   }
 });
 
 add_task(async function test_no_orphan_annotations() {
   let db = await PlacesUtils.promiseDBConnection();
 
-  let rows = await db.execute(`
-    SELECT item_id FROM moz_items_annos
-    WHERE item_id NOT IN (SELECT id from moz_bookmarks)
-  `);
-
-  Assert.equal(rows.length, 0,
-    `Should have no orphan annotations.`);
-
-  rows = await db.execute(`
-    SELECT id FROM moz_anno_attributes
-    WHERE id NOT IN (SELECT id from moz_items_annos)
-  `);
-
-  Assert.equal(rows.length, 0,
-    `Should have no orphan annotation attributes.`);
+  await assertNoOrphanAnnotations(db);
 });
 
 add_task(async function test_no_orphan_keywords() {
   let db = await PlacesUtils.promiseDBConnection();
 
   let rows = await db.execute(`
     SELECT place_id FROM moz_keywords
     WHERE place_id NOT IN (SELECT id from moz_places)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v50.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BASE_GUID = "null".padEnd(11, "_");
+const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
+const LAST_USED_META_DATA = "places/bookmarks/edit/lastusedfolder";
+
+let expectedGuids = [];
+
+add_task(async function setup() {
+  await setupPlacesDatabase("places_v43.sqlite");
+
+  // Setup database contents to be migrated.
+  let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+  let db = await Sqlite.openConnection({ path });
+  // We can reuse the same guid, it doesn't matter for this test.
+  await db.execute(`INSERT INTO moz_anno_attributes (name)
+                    VALUES (:last_used_anno)`, { last_used_anno: LAST_USED_ANNO });
+
+  for (let i = 0; i < 3; i++) {
+    let guid = `${BASE_GUID}${i}`;
+    await db.execute(`INSERT INTO moz_bookmarks (guid, type)
+                      VALUES (:guid, :type)
+                      `, { guid, type: PlacesUtils.bookmarks.TYPE_FOLDER });
+    await db.execute(`INSERT INTO moz_items_annos (item_id, anno_attribute_id, content)
+                      VALUES ((SELECT id FROM moz_bookmarks WHERE guid = :guid),
+                              (SELECT id FROM moz_anno_attributes WHERE name = :last_used_anno),
+                              :content)`, {
+      guid,
+      content: new Date(1517318477569) - (3 - i) * 60 * 60 * 1000,
+      last_used_anno: LAST_USED_ANNO,
+    });
+    expectedGuids.unshift(guid);
+  }
+  await db.close();
+});
+
+add_task(async function database_is_valid() {
+  // Accessing the database for the first time triggers migration.
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+  let db = await PlacesUtils.promiseDBConnection();
+  Assert.equal((await db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(async function test_folders_migrated() {
+  let metaData = await PlacesUtils.metadata.get(LAST_USED_META_DATA);
+
+  Assert.deepEqual(JSON.parse(metaData), expectedGuids);
+});
+
+add_task(async function test_annotations_removed() {
+  let db = await PlacesUtils.promiseDBConnection();
+
+  await assertAnnotationsRemoved(db, [LAST_USED_ANNO]);
+});
+
+add_task(async function test_no_orphan_annotations() {
+  let db = await PlacesUtils.promiseDBConnection();
+
+  await assertNoOrphanAnnotations(db);
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -22,8 +22,9 @@ support-files =
 [test_current_from_v38.js]
 [test_current_from_v41.js]
 [test_current_from_v42.js]
 [test_current_from_v43.js]
 [test_current_from_v45.js]
 [test_current_from_v46.js]
 [test_current_from_v47.js]
 [test_current_from_v48.js]
+[test_current_from_v50.js]