Bug 1678607: Apply bookmark-tags-changed event. r=mak
authorDaisuke Akatsuka <daisuke@birchill.co.jp>
Mon, 18 Oct 2021 04:43:44 +0000
changeset 596155 13868a361ad3c4004be8450d7119faaa820e05bc
parent 596154 7705a6b69701b3c852548c04ced4b4ca7f88f181
child 596156 63d10a00d25619d4a11c08a96749dad932dc0bec
push id151653
push userdakatsuka.birchill@mozilla.com
push dateMon, 18 Oct 2021 04:46:21 +0000
treeherderautoland@63d10a00d256 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1678607
milestone95.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1678607: Apply bookmark-tags-changed event. r=mak Depends on D128326 Differential Revision: https://phabricator.services.mozilla.com/D128327
browser/components/places/content/editBookmark.js
browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
browser/components/places/tests/browser/browser_bookmark_add_tags.js
browser/components/places/tests/browser/browser_bookmark_remove_tags.js
browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js
browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js
services/sync/modules/engines/bookmarks.js
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavHistoryResult.cpp
toolkit/components/places/nsNavHistoryResult.h
toolkit/components/places/tests/bookmarks/head_bookmarks.js
toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js
toolkit/components/places/tests/unit/test_onItemChanged_tags.js
toolkit/components/places/tests/unit/xpcshell.ini
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -338,17 +338,22 @@ var gEditItemOverlay = {
       );
     }
 
     // Observe changes.
     if (!this._observersAdded) {
       PlacesUtils.bookmarks.addObserver(this);
       this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
       PlacesUtils.observers.addListener(
-        ["bookmark-moved", "bookmark-title-changed", "bookmark-url-changed"],
+        [
+          "bookmark-moved",
+          "bookmark-tags-changed",
+          "bookmark-title-changed",
+          "bookmark-url-changed",
+        ],
         this.handlePlacesEvents
       );
       window.addEventListener("unload", this);
       this._observersAdded = true;
     }
 
     let focusElement = () => {
       // The focusedElement possible values are:
@@ -556,17 +561,22 @@ var gEditItemOverlay = {
       if (!tagsSelectorRow.collapsed) {
         this.toggleTagsSelector().catch(Cu.reportError);
       }
     }
 
     if (this._observersAdded) {
       PlacesUtils.bookmarks.removeObserver(this);
       PlacesUtils.observers.removeListener(
-        ["bookmark-moved", "bookmark-title-changed", "bookmark-url-changed"],
+        [
+          "bookmark-moved",
+          "bookmark-tags-changed",
+          "bookmark-title-changed",
+          "bookmark-url-changed",
+        ],
         this.handlePlacesEvents
       );
       window.removeEventListener("unload", this);
       this._observersAdded = false;
     }
 
     if (this._folderMenuListListenerAdded) {
       this._folderMenuList.removeEventListener("select", this);
@@ -1149,16 +1159,21 @@ var gEditItemOverlay = {
           // Just setting selectItem _does not_ trigger oncommand, so we don't
           // recurse.
           const bm = await PlacesUtils.bookmarks.fetch(event.parentGuid);
           this._folderMenuList.selectedItem = this._getFolderMenuItem(
             event.parentGuid,
             bm.title
           );
           break;
+        case "bookmark-tags-changed":
+          if (this._paneInfo.visibleRows.has("tagsRow")) {
+            this._onTagsChange(event.guid).catch(Cu.reportError);
+          }
+          break;
         case "bookmark-title-changed":
           if (this._paneInfo.isItem || this._paneInfo.isTag) {
             // This also updates titles of folders in the folder menu list.
             this._onItemTitleChange(event.id, event.title, event.guid);
           }
           break;
         case "bookmark-url-changed":
           if (!this._paneInfo.isItem || this._paneInfo.itemId != event.id) {
@@ -1286,21 +1301,16 @@ var gEditItemOverlay = {
     aProperty,
     aIsAnnotationProperty,
     aValue,
     aLastModified,
     aItemType,
     aParentId,
     aGuid
   ) {
-    if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow")) {
-      this._onTagsChange(aGuid).catch(Cu.reportError);
-      return;
-    }
-
     if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId) {
       return;
     }
 
     switch (aProperty) {
       case "keyword":
         if (this._paneInfo.visibleRows.has("keywordRow")) {
           this._initKeywordField(aValue).catch(Cu.reportError);
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
@@ -29,21 +29,25 @@ add_task(async function() {
   Assert.equal(tagNode.title, "tag1", "tagNode title is correct");
 
   Assert.ok(
     tree.controller.isCommandEnabled("placesCmd_show:info"),
     "'placesCmd_show:info' on current selected node is enabled"
   );
 
   let promiseTagResetNotification = PlacesTestUtils.waitForNotification(
-    "onItemChanged",
-    (itemId, prop) => {
-      let tags = PlacesUtils.tagging.getTagsForURI(uri);
-      return prop == "tags" && tags.length == 1 && tags[0] == "tag1";
-    }
+    "bookmark-tags-changed",
+    events =>
+      events.some(event => {
+        const tags = PlacesUtils.tagging.getTagsForURI(
+          Services.io.newURI(event.url)
+        );
+        return tags.length === 1 && tags[0] === "tag1";
+      }),
+    "places"
   );
 
   await withBookmarksDialog(
     true,
     function openDialog() {
       tree.controller.doCommand("placesCmd_show:info");
     },
     async function test(dialogWin) {
@@ -55,21 +59,25 @@ add_task(async function() {
 
       // Check that name picker is not read only
       let namepicker = dialogWin.document.getElementById(
         "editBMPanel_namePicker"
       );
       Assert.ok(!namepicker.readOnly, "Name field should not be read-only");
       Assert.equal(namepicker.value, "tag1", "Node title is correct");
       let promiseTagChangeNotification = PlacesTestUtils.waitForNotification(
-        "onItemChanged",
-        (itemId, prop) => {
-          let tags = PlacesUtils.tagging.getTagsForURI(uri);
-          return prop == "tags" && tags.length == 1 && tags[0] == "tag2";
-        }
+        "bookmark-tags-changed",
+        events =>
+          events.some(event => {
+            const tags = PlacesUtils.tagging.getTagsForURI(
+              Services.io.newURI(event.url)
+            );
+            return tags.length === 1 && tags[0] === "tag2";
+          }),
+        "places"
       );
 
       let promiseTagRemoveNotification = PlacesTestUtils.waitForNotification(
         "bookmark-removed",
         events =>
           events.some(
             event => event.parentGuid == PlacesUtils.bookmarks.tagsGuid
           ),
--- a/browser/components/places/tests/browser/browser_bookmark_add_tags.js
+++ b/browser/components/places/tests/browser/browser_bookmark_add_tags.js
@@ -98,18 +98,19 @@ add_task(async function test_add_bookmar
     ["tag1"]
   );
   let doneButton = document.getElementById("editBookmarkPanelDoneButton");
   await hideBookmarksPanel(() => doneButton.click());
 
   // Click the bookmark star again, add more tags.
   await clickBookmarkStar();
   promiseNotification = PlacesTestUtils.waitForNotification(
-    "onItemChanged",
-    (id, property) => property == "tags"
+    "bookmark-tags-changed",
+    () => true,
+    "places"
   );
   await fillBookmarkTextField(
     "editBMPanel_tagsField",
     "tag1, tag2, tag3",
     window
   );
   await promiseNotification;
   await hideBookmarksPanel(() => doneButton.click());
--- a/browser/components/places/tests/browser/browser_bookmark_remove_tags.js
+++ b/browser/components/places/tests/browser/browser_bookmark_remove_tags.js
@@ -51,18 +51,19 @@ add_task(async function test_remove_tags
   let bookmarkPanelTitle = document.getElementById("editBookmarkPanelTitle");
   Assert.equal(
     document.l10n.getAttributes(bookmarkPanelTitle).id,
     "bookmarks-edit-bookmark",
     "Bookmark panel title is correct."
   );
 
   let promiseTagsChange = PlacesTestUtils.waitForNotification(
-    "onItemChanged",
-    (id, property) => property === "tags"
+    "bookmark-tags-changed",
+    () => true,
+    "places"
   );
 
   // Update the "tags" field.
   fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2, tag3", window);
   let tagspicker = document.getElementById("editBMPanel_tagsField");
   await TestUtils.waitForCondition(
     () => tagspicker.value === "tag1, tag2, tag3",
     "Tags are correct after update."
@@ -114,18 +115,19 @@ add_task(async function test_remove_tags
       );
       Assert.equal(
         tagspicker.value,
         "tag1, tag2, tag3",
         "Tags are correct before update."
       );
 
       let promiseTagsChange = PlacesTestUtils.waitForNotification(
-        "onItemChanged",
-        (id, property) => property === "tags"
+        "bookmark-tags-changed",
+        () => true,
+        "places"
       );
 
       // Update the "tags" field.
       fillBookmarkTextField(
         "editBMPanel_tagsField",
         "tag1, tag2",
         dialogWin,
         false
@@ -169,18 +171,19 @@ add_task(async function test_remove_tags
         );
         Assert.equal(
           tagspicker.value,
           "tag1, tag2",
           "Tags are correct before update."
         );
 
         let promiseTagsChange = PlacesTestUtils.waitForNotification(
-          "onItemChanged",
-          (id, property) => property === "tags"
+          "bookmark-tags-changed",
+          () => true,
+          "places"
         );
 
         // Update the "tags" field.
         fillBookmarkTextField(
           "editBMPanel_tagsField",
           "tag1",
           dialogWin,
           false
--- a/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js
+++ b/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js
@@ -18,18 +18,19 @@ add_task(async function() {
   StarUI._createPanelIfNeeded();
   ok(gEditItemOverlay, "gEditItemOverlay is in context");
   let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
   gEditItemOverlay.initPanel({ node });
 
   // add a tag
   document.getElementById("editBMPanel_tagsField").value = testTag;
   let promiseNotification = PlacesTestUtils.waitForNotification(
-    "onItemChanged",
-    (id, property) => property == "tags"
+    "bookmark-tags-changed",
+    () => true,
+    "places"
   );
   gEditItemOverlay.onTagsFieldChange();
   await promiseNotification;
 
   // test that the tag has been added in the backend
   is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTag, "tags match");
 
   // change the tag
--- a/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js
+++ b/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js
@@ -68,18 +68,19 @@ add_task(async function test_tags() {
 
     Assert.equal(
       ContentTree.view.selectedNode.title,
       `bm${i}`,
       `Should have selected bm${i}`
     );
 
     let promiseNotification = PlacesTestUtils.waitForNotification(
-      "onItemChanged",
-      (id, property) => property == "tags"
+      "bookmark-tags-changed",
+      () => true,
+      "places"
     );
 
     ContentTree.view.controller.doCommand("cmd_delete");
 
     await promiseNotification;
 
     for (let j = 0; j < uris.length; j++) {
       let tags = PlacesUtils.tagging.getTagsForURI(uris[j]);
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -804,16 +804,17 @@ BookmarksTracker.prototype = {
     this._placesListener = new PlacesWeakCallbackWrapper(
       this.handlePlacesEvents.bind(this)
     );
     PlacesUtils.observers.addListener(
       [
         "bookmark-added",
         "bookmark-removed",
         "bookmark-moved",
+        "bookmark-tags-changed",
         "bookmark-time-changed",
         "bookmark-title-changed",
         "bookmark-url-changed",
       ],
       this._placesListener
     );
     Svc.Obs.add("bookmarks-restore-begin", this);
     Svc.Obs.add("bookmarks-restore-success", this);
@@ -822,16 +823,17 @@ BookmarksTracker.prototype = {
 
   onStop() {
     PlacesUtils.bookmarks.removeObserver(this);
     PlacesUtils.observers.removeListener(
       [
         "bookmark-added",
         "bookmark-removed",
         "bookmark-moved",
+        "bookmark-tags-changed",
         "bookmark-time-changed",
         "bookmark-title-changed",
         "bookmark-url-changed",
       ],
       this._placesListener
     );
     Svc.Obs.remove("bookmarks-restore-begin", this);
     Svc.Obs.remove("bookmarks-restore-success", this);
@@ -881,16 +883,17 @@ BookmarksTracker.prototype = {
 
   handlePlacesEvents(events) {
     for (let event of events) {
       switch (event.type) {
         case "bookmark-added":
         case "bookmark-removed":
         case "bookmark-moved":
         case "bookmark-guid-changed":
+        case "bookmark-tags-changed":
         case "bookmark-time-changed":
         case "bookmark-title-changed":
         case "bookmark-url-changed":
           if (IGNORED_SOURCES.includes(event.source)) {
             continue;
           }
 
           this._log.trace(`'${event.type}': ${event.id}`);
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -331,36 +331,21 @@ var Bookmarks = Object.freeze({
           dateAdded: item.dateAdded,
           guid: item.guid,
           parentGuid: item.parentGuid,
           source: item.source,
           isTagging: isTagging || isTagsFolder,
         }),
       ];
 
-      // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
+      // If it's a tag, notify bookmark-tags-changed event to all bookmarks for this URL.
       if (isTagging) {
-        let observers = PlacesUtils.bookmarks.getObservers();
         for (let entry of await fetchBookmarksByURL(item, {
           concurrent: true,
         })) {
-          notify(observers, "onItemChanged", [
-            entry._id,
-            "tags",
-            false,
-            "",
-            PlacesUtils.toPRTime(entry.lastModified),
-            entry.type,
-            entry._parentId,
-            entry.guid,
-            entry.parentGuid,
-            "",
-            item.source,
-          ]);
-
           notifications.push(
             new PlacesBookmarkTags({
               id: entry._id,
               itemType: entry.type,
               url,
               guid: entry.guid,
               parentGuid: entry.parentGuid,
               lastModified: entry.lastModified,
@@ -863,19 +848,16 @@ var Bookmarks = Object.freeze({
             // ...though we don't wait for the calculation.
             updateFrecency(db, [item.url, updatedItem.url]).catch(
               Cu.reportError
             );
           }
 
           const notifications = [];
 
-          // Notify onItemChanged to listeners.
-          let observers = PlacesUtils.bookmarks.getObservers();
-
           // For lastModified, we only care about the original input, since we
           // should not notify implicit lastModified changes.
           if (
             (info.hasOwnProperty("lastModified") &&
               updateInfo.hasOwnProperty("lastModified") &&
               item.lastModified != updatedItem.lastModified) ||
             (info.hasOwnProperty("dateAdded") &&
               updateInfo.hasOwnProperty("dateAdded") &&
@@ -929,30 +911,16 @@ var Bookmarks = Object.freeze({
 
             // If we're updating a tag, we must notify all the tagged bookmarks
             // about the change.
             if (isTagging) {
               for (let entry of await fetchBookmarksByTags(
                 { tags: [updatedItem.title] },
                 { concurrent: true }
               )) {
-                notify(observers, "onItemChanged", [
-                  entry._id,
-                  "tags",
-                  false,
-                  "",
-                  PlacesUtils.toPRTime(entry.lastModified),
-                  entry.type,
-                  entry._parentId,
-                  entry.guid,
-                  entry.parentGuid,
-                  "",
-                  updatedItem.source,
-                ]);
-
                 notifications.push(
                   new PlacesBookmarkTags({
                     id: entry._id,
                     itemType: entry.type,
                     url: entry.url,
                     guid: entry.guid,
                     parentGuid: entry.parentGuid,
                     lastModified: entry.lastModified,
@@ -1337,17 +1305,16 @@ var Bookmarks = Object.freeze({
       }
 
       await removeBookmarks(removeItems, options);
 
       // Notify bookmark-removed to listeners.
       let notifications = [];
 
       for (let item of removeItems) {
-        let observers = PlacesUtils.bookmarks.getObservers();
         let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
         let url = "";
         if (item.type == Bookmarks.TYPE_BOOKMARK) {
           url = item.hasOwnProperty("url") ? item.url.href : null;
         }
 
         notifications.push(
           new PlacesBookmarkRemoved({
@@ -1363,30 +1330,16 @@ var Bookmarks = Object.freeze({
             isDescendantRemoval: false,
           })
         );
 
         if (isUntagging) {
           for (let entry of await fetchBookmarksByURL(item, {
             concurrent: true,
           })) {
-            notify(observers, "onItemChanged", [
-              entry._id,
-              "tags",
-              false,
-              "",
-              PlacesUtils.toPRTime(entry.lastModified),
-              entry.type,
-              entry._parentId,
-              entry.guid,
-              entry.parentGuid,
-              "",
-              options.source,
-            ]);
-
             notifications.push(
               new PlacesBookmarkTags({
                 id: entry._id,
                 itemType: entry.type,
                 url,
                 guid: entry.guid,
                 parentGuid: entry.parentGuid,
                 lastModified: entry.lastModified,
@@ -1892,48 +1845,16 @@ var Bookmarks = Object.freeze({
     }
 
     return queryBookmarks(query);
   },
 });
 
 // Globals.
 
-/**
- * Sends a bookmarks notification through the given observers.
- *
- * @param {Array} observers
- *        array of nsINavBookmarkObserver objects.
- * @param {String} notification
- *        the notification name.
- * @param {Array} [args]
- *        array of arguments to pass to the notification.
- * @param {Object} [information]
- *        Information about the notification, so we can filter based
- *        based on the observer's preferences.
- */
-function notify(observers, notification, args = [], information = {}) {
-  for (let observer of observers) {
-    if (information.isTagging && observer.skipTags) {
-      continue;
-    }
-
-    if (
-      information.isDescendantRemoval &&
-      !PlacesUtils.bookmarks.userContentRoots.includes(information.parentGuid)
-    ) {
-      continue;
-    }
-
-    try {
-      observer[notification](...args);
-    } catch (ex) {}
-  }
-}
-
 // Update implementation.
 
 /**
  * Updates a single bookmark in the database. This should be called from within
  * a transaction.
  *
  * @param {Object} db The pre-existing database connection.
  * @param {Object} info A bookmark-item structure with new properties.
@@ -3325,17 +3246,16 @@ var removeFoldersContents = async functi
 
   // Send onItemRemoved notifications to listeners.
   // TODO (Bug 1087580): for the case of eraseEverything, this should send a
   // single clear bookmarks notification rather than notifying for each
   // bookmark.
 
   // Notify listeners in reverse order to serve children before parents.
   let { source = Bookmarks.SOURCES.DEFAULT } = options;
-  let observers = PlacesUtils.bookmarks.getObservers();
   let notifications = [];
   for (let item of itemsRemoved.reverse()) {
     let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
     let url = "";
     if (item.type == Bookmarks.TYPE_BOOKMARK) {
       url = item.hasOwnProperty("url") ? item.url.href : null;
     }
     notifications.push(
@@ -3352,30 +3272,16 @@ var removeFoldersContents = async functi
         isDescendantRemoval: !PlacesUtils.bookmarks.userContentRoots.includes(
           item.parentGuid
         ),
       })
     );
 
     if (isUntagging) {
       for (let entry of await fetchBookmarksByURL(item, true)) {
-        notify(observers, "onItemChanged", [
-          entry._id,
-          "tags",
-          false,
-          "",
-          PlacesUtils.toPRTime(entry.lastModified),
-          entry.type,
-          entry._parentId,
-          entry.guid,
-          entry.parentGuid,
-          "",
-          source,
-        ]);
-
         notifications.push(
           new PlacesBookmarkTags({
             id: entry._id,
             itemType: entry.type,
             url,
             guid: entry.guid,
             parentGuid: entry.parentGuid,
             lastModified: entry.lastModified,
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -452,24 +452,16 @@ nsNavBookmarks::InsertBookmark(int64_t a
     // Notify a tags change to all bookmarks for this URI.
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(aURI, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
 
     for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
       // Check that bookmarks doesn't include the current tag itemId.
       MOZ_ASSERT(bookmarks[i].id != *aNewBookmarkId);
-
-      NOTIFY_BOOKMARKS_OBSERVERS(
-          mCanNotify, mObservers,
-          OnItemChanged(bookmarks[i].id, "tags"_ns, false, ""_ns,
-                        bookmarks[i].lastModified, TYPE_BOOKMARK,
-                        bookmarks[i].parentId, bookmarks[i].guid,
-                        bookmarks[i].parentGuid, ""_ns, aSource));
-
       RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
       tagsChanged->mId = bookmarks[i].id;
       tagsChanged->mItemType = TYPE_BOOKMARK;
       tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
       tagsChanged->mGuid = bookmarks[i].guid;
       tagsChanged->mParentGuid = bookmarks[i].parentGuid;
       tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
       tagsChanged->mSource = aSource;
@@ -594,23 +586,16 @@ nsNavBookmarks::RemoveItem(int64_t aItem
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(uri, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsAutoCString utf8spec;
     uri->GetSpec(utf8spec);
 
     for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
-      NOTIFY_BOOKMARKS_OBSERVERS(
-          mCanNotify, mObservers,
-          OnItemChanged(bookmarks[i].id, "tags"_ns, false, ""_ns,
-                        bookmarks[i].lastModified, TYPE_BOOKMARK,
-                        bookmarks[i].parentId, bookmarks[i].guid,
-                        bookmarks[i].parentGuid, ""_ns, aSource));
-
       RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
       tagsChanged->mId = bookmarks[i].id;
       tagsChanged->mItemType = TYPE_BOOKMARK;
       tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
       tagsChanged->mGuid = bookmarks[i].guid;
       tagsChanged->mParentGuid = bookmarks[i].parentGuid;
       tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
       tagsChanged->mSource = aSource;
@@ -915,23 +900,16 @@ nsresult nsNavBookmarks::RemoveFolderChi
       nsTArray<BookmarkData> bookmarks;
       rv = GetBookmarksForURI(uri, bookmarks);
       NS_ENSURE_SUCCESS(rv, rv);
 
       nsAutoCString utf8spec;
       uri->GetSpec(utf8spec);
 
       for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
-        NOTIFY_BOOKMARKS_OBSERVERS(
-            mCanNotify, mObservers,
-            OnItemChanged(bookmarks[i].id, "tags"_ns, false, ""_ns,
-                          bookmarks[i].lastModified, TYPE_BOOKMARK,
-                          bookmarks[i].parentId, bookmarks[i].guid,
-                          bookmarks[i].parentGuid, ""_ns, aSource));
-
         RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
         tagsChanged->mId = bookmarks[i].id;
         tagsChanged->mItemType = TYPE_BOOKMARK;
         tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
         tagsChanged->mGuid = bookmarks[i].guid;
         tagsChanged->mParentGuid = bookmarks[i].parentGuid;
         tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
         tagsChanged->mSource = aSource;
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -17,16 +17,17 @@
 #include "nsUnicharUtils.h"
 #include "prtime.h"
 #include "nsQueryObject.h"
 #include "mozilla/dom/PlacesObservers.h"
 #include "mozilla/dom/PlacesVisit.h"
 #include "mozilla/dom/PlacesVisitRemoved.h"
 #include "mozilla/dom/PlacesVisitTitle.h"
 #include "mozilla/dom/PlacesBookmarkMoved.h"
+#include "mozilla/dom/PlacesBookmarkTags.h"
 #include "mozilla/dom/PlacesBookmarkTime.h"
 #include "mozilla/dom/PlacesBookmarkTitle.h"
 #include "mozilla/dom/PlacesBookmarkUrl.h"
 
 #include "nsCycleCollectionParticipant.h"
 
 // Thanks, Windows.h :(
 #undef CompareString
@@ -2478,35 +2479,16 @@ nsNavHistoryQueryResultNode::OnItemChang
   // History observers should not get OnItemChanged but should get the
   // corresponding history notifications instead.
   // For bookmark queries, "all bookmark" observers should get OnItemChanged.
   // For example, when a title of a bookmark changes, we want that to refresh.
   if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) {
     return Refresh();
   }
 
-  // Some node could observe both bookmarks and history.  But a node observing
-  // only history should never get a bookmark notification.
-  NS_WARNING_ASSERTION(
-      mResult && mResult->mIsBookmarksObserver,
-      "history observers should not get OnItemChanged, but should get the "
-      "corresponding history notifications instead");
-
-  // Tags in history queries are a special case since tags are per uri and
-  // we filter tags based on searchterms.
-  if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
-      aProperty.EqualsLiteral("tags")) {
-    nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
-    NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
-    nsCOMPtr<nsIURI> uri;
-    nsresult rv = bookmarks->GetBookmarkURI(aItemId, getter_AddRefs(uri));
-    NS_ENSURE_SUCCESS(rv, rv);
-    rv = NotifyIfTagsChanged(uri);
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
   return NS_OK;
 }
 
 nsresult nsNavHistoryQueryResultNode::OnItemMoved(
     int64_t aFolder, int32_t aOldIndex, int32_t aNewIndex, uint16_t aItemType,
     const nsACString& aGUID, const nsACString& aOldParentGUID,
     const nsACString& aNewParentGUID, uint16_t aSource,
     const nsACString& aURI) {
@@ -2517,16 +2499,29 @@ nsresult nsNavHistoryQueryResultNode::On
   if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS &&
       aItemType != nsINavBookmarksService::TYPE_SEPARATOR &&
       !aNewParentGUID.Equals(aOldParentGUID)) {
     return Refresh();
   }
   return NS_OK;
 }
 
+nsresult nsNavHistoryQueryResultNode::OnItemTagsChanged(int64_t aItemId,
+                                                        const nsAString& aURL) {
+  nsresult rv = nsNavHistoryResultNode::OnItemTagsChanged(aItemId, aURL);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIURI> uri;
+  rv = NS_NewURI(getter_AddRefs(uri), aURL);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = NotifyIfTagsChanged(uri);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return NS_OK;
+}
+
 nsresult nsNavHistoryQueryResultNode::OnItemUrlChanged(int64_t aItemId,
                                                        const nsACString& aGUID,
                                                        const nsACString& aURL,
                                                        PRTime aLastModified) {
   if (aItemId != mItemId) {
     return NS_OK;
   }
 
@@ -3188,16 +3183,34 @@ nsresult nsNavHistoryFolderResultNode::O
   if (!StartIncrementalUpdate()) return NS_OK;  // we are completely refreshed
 
   // shift all following indices down
   ReindexRange(aIndex + 1, INT32_MAX, -1);
 
   return RemoveChildAt(index);
 }
 
+nsresult nsNavHistoryResultNode::OnItemTagsChanged(int64_t aItemId,
+                                                   const nsAString& aURL) {
+  if (aItemId != mItemId) {
+    return NS_OK;
+  }
+
+  mTags.SetIsVoid(true);
+
+  bool shouldNotify = !mParent || mParent->AreChildrenVisible();
+  if (shouldNotify) {
+    nsNavHistoryResult* result = GetResult();
+    NS_ENSURE_STATE(result);
+    NOTIFY_RESULT_OBSERVERS(result, NodeTagsChanged(this));
+  }
+
+  return NS_OK;
+}
+
 nsresult nsNavHistoryResultNode::OnItemTimeChanged(int64_t aItemId,
                                                    const nsACString& aGUID,
                                                    PRTime aDateAdded,
                                                    PRTime aLastModified) {
   if (aItemId != mItemId) {
     return NS_OK;
   }
 
@@ -3300,19 +3313,16 @@ nsNavHistoryResultNode::OnItemChanged(
 
   if (aProperty.EqualsLiteral("cleartime")) {
     PRTime oldTime = mTime;
     mTime = 0;
     if (shouldNotify && !result->CanSkipHistoryDetailsNotifications()) {
       NOTIFY_RESULT_OBSERVERS(
           result, NodeHistoryDetailsChanged(this, oldTime, mAccessCount));
     }
-  } else if (aProperty.EqualsLiteral("tags")) {
-    mTags.SetIsVoid(true);
-    if (shouldNotify) NOTIFY_RESULT_OBSERVERS(result, NodeTagsChanged(this));
   } else if (aProperty.EqualsLiteral("keyword")) {
     if (shouldNotify)
       NOTIFY_RESULT_OBSERVERS(result, NodeKeywordChanged(this, aNewValue));
   } else
     MOZ_ASSERT_UNREACHABLE("Unknown bookmark property changing.");
 
   if (!mParent) return NS_OK;
 
@@ -3578,27 +3588,28 @@ nsNavHistoryResult::~nsNavHistoryResult(
   // Delete all heap-allocated bookmark folder observer arrays.
   for (auto it = mBookmarkFolderObservers.Iter(); !it.Done(); it.Next()) {
     delete it.Data();
     it.Remove();
   }
 }
 
 void nsNavHistoryResult::StopObserving() {
-  AutoTArray<PlacesEventType, 10> events;
+  AutoTArray<PlacesEventType, 11> events;
   events.AppendElement(PlacesEventType::Favicon_changed);
   if (mIsBookmarksObserver) {
     nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
     if (bookmarks) {
       bookmarks->RemoveObserver(this);
       mIsBookmarksObserver = false;
     }
     events.AppendElement(PlacesEventType::Bookmark_added);
     events.AppendElement(PlacesEventType::Bookmark_removed);
     events.AppendElement(PlacesEventType::Bookmark_moved);
+    events.AppendElement(PlacesEventType::Bookmark_tags_changed);
     events.AppendElement(PlacesEventType::Bookmark_time_changed);
     events.AppendElement(PlacesEventType::Bookmark_title_changed);
     events.AppendElement(PlacesEventType::Bookmark_url_changed);
   }
   if (mIsMobilePrefObserver) {
     Preferences::UnregisterCallback(OnMobilePrefChangedCallback,
                                     MOBILE_BOOKMARKS_PREF, this);
     mIsMobilePrefObserver = false;
@@ -3712,20 +3723,21 @@ void nsNavHistoryResult::EnsureIsObservi
     return;
   }
   nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
   if (!bookmarks) {
     MOZ_ASSERT_UNREACHABLE("Can't create bookmark service");
     return;
   }
   bookmarks->AddObserver(this, true);
-  AutoTArray<PlacesEventType, 7> events;
+  AutoTArray<PlacesEventType, 8> events;
   events.AppendElement(PlacesEventType::Bookmark_added);
   events.AppendElement(PlacesEventType::Bookmark_removed);
   events.AppendElement(PlacesEventType::Bookmark_moved);
+  events.AppendElement(PlacesEventType::Bookmark_tags_changed);
   events.AppendElement(PlacesEventType::Bookmark_time_changed);
   events.AppendElement(PlacesEventType::Bookmark_title_changed);
   events.AppendElement(PlacesEventType::Bookmark_url_changed);
   // If we're not observing visits yet, also add a page-visited observer to
   // serve onItemVisited.
   if (!mIsHistoryObserver && !mIsHistoryDetailsObserver) {
     events.AppendElement(PlacesEventType::Page_visited);
     mIsHistoryDetailsObserver = true;
@@ -4286,16 +4298,28 @@ void nsNavHistoryResult::HandlePlacesEve
                         item->mItemType, item->mGuid, item->mOldParentGuid,
                         item->mParentGuid, item->mSource, url));
         ENUMERATE_HISTORY_OBSERVERS(
             OnItemMoved(item->mId, item->mOldIndex, item->mIndex,
                         item->mItemType, item->mGuid, item->mOldParentGuid,
                         item->mParentGuid, item->mSource, url));
         break;
       }
+      case PlacesEventType::Bookmark_tags_changed: {
+        const dom::PlacesBookmarkTags* tagsEvent =
+            event->AsPlacesBookmarkTags();
+        if (NS_WARN_IF(!tagsEvent)) {
+          continue;
+        }
+
+        ENUMERATE_BOOKMARK_CHANGED_OBSERVERS(
+            tagsEvent->mParentGuid, tagsEvent->mId,
+            OnItemTagsChanged(tagsEvent->mId, tagsEvent->mUrl));
+        break;
+      }
       case PlacesEventType::Bookmark_time_changed: {
         const dom::PlacesBookmarkTime* timeEvent =
             event->AsPlacesBookmarkTime();
         if (NS_WARN_IF(!timeEvent)) {
           continue;
         }
 
         ENUMERATE_BOOKMARK_CHANGED_OBSERVERS(
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -309,16 +309,17 @@ class nsNavHistoryResultNode : public ns
   NS_IMETHOD GetPageGuid(nsACString& aPageGuid) override;
   NS_IMETHOD GetBookmarkGuid(nsACString& aBookmarkGuid) override;
   NS_IMETHOD GetVisitId(int64_t* aVisitId) override;
   NS_IMETHOD GetFromVisitId(int64_t* aFromVisitId) override;
   NS_IMETHOD GetVisitType(uint32_t* aVisitType) override;
 
   virtual void OnRemoving();
 
+  nsresult OnItemTagsChanged(int64_t aItemId, const nsAString& aURL);
   nsresult OnItemTimeChanged(int64_t aItemId, const nsACString& aGUID,
                              PRTime aDateAdded, PRTime aLastModified);
   nsresult OnItemTitleChanged(int64_t aItemId, const nsACString& aGUID,
                               const nsACString& aTitle, PRTime aLastModified);
   nsresult OnItemUrlChanged(int64_t aItemId, const nsACString& aGUID,
                             const nsACString& aURL, PRTime aLastModified);
 
   // Called from result's onItemChanged, see also bookmark observer declaration
@@ -695,16 +696,17 @@ class nsNavHistoryQueryResultNode final
                          uint16_t aItemType, nsIURI* aURI,
                          const nsACString& aGUID, const nsACString& aParentGUID,
                          uint16_t aSource);
   nsresult OnItemMoved(int64_t aFolder, int32_t aOldIndex, int32_t aNewIndex,
                        uint16_t aItemType, const nsACString& aGUID,
                        const nsACString& aOldParentGUID,
                        const nsACString& aNewParentGUID, uint16_t aSource,
                        const nsACString& aURI);
+  nsresult OnItemTagsChanged(int64_t aItemId, const nsAString& aURL);
   nsresult OnItemUrlChanged(int64_t aItemId, const nsACString& aGUID,
                             const nsACString& aURL, PRTime aLastModified);
 
   // The internal version has an output aAdded parameter, it is incremented by
   // query nodes when the visited uri belongs to them. If no such query exists,
   // the history result creates a new query node dynamically.
   nsresult OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
                    uint32_t aTransitionType, bool aHidden, uint32_t* aAdded);
--- a/toolkit/components/places/tests/bookmarks/head_bookmarks.js
+++ b/toolkit/components/places/tests/bookmarks/head_bookmarks.js
@@ -118,16 +118,29 @@ function expectPlacesObserverNotificatio
             parentGuid: event.parentGuid,
             source: event.source,
             index: event.index,
             oldParentGuid: event.oldParentGuid,
             oldIndex: event.oldIndex,
             isTagging: event.isTagging,
           });
           break;
+        case "bookmark-tags-changed":
+          notifications.push({
+            type: event.type,
+            id: event.id,
+            itemType: event.itemType,
+            url: event.url,
+            guid: event.guid,
+            parentGuid: event.parentGuid,
+            lastModified: new Date(event.lastModified),
+            source: event.source,
+            isTagging: event.isTagging,
+          });
+          break;
         case "bookmark-time-changed":
           notifications.push({
             type: event.type,
             id: event.id,
             itemType: event.itemType,
             url: event.url,
             guid: event.guid,
             parentGuid: event.parentGuid,
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -145,66 +145,59 @@ add_task(async function insert_bookmark_
 
 add_task(async function insert_bookmark_tag_notification() {
   let bm = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     url: new URL("http://tag.example.com/"),
   });
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
-  let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
 
   let tagFolder = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     parentGuid: PlacesUtils.bookmarks.tagsGuid,
     title: "tag",
   });
-  let placesObserver = expectPlacesObserverNotifications(["bookmark-added"]);
-  let bookmarksObserver = expectNotifications();
+  const observer = expectPlacesObserverNotifications([
+    "bookmark-added",
+    "bookmark-tags-changed",
+  ]);
   let tag = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
     parentGuid: tagFolder.guid,
     url: new URL("http://tag.example.com/"),
   });
   let tagId = await PlacesUtils.promiseItemId(tag.guid);
   let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid);
 
-  placesObserver.check([
+  observer.check([
     {
       type: "bookmark-added",
       id: tagId,
       parentId: tagParentId,
       index: tag.index,
       itemType: tag.type,
       url: tag.url,
       title: "",
       dateAdded: tag.dateAdded,
       guid: tag.guid,
       parentGuid: tag.parentGuid,
       source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
       isTagging: true,
     },
-  ]);
-
-  bookmarksObserver.check([
     {
-      name: "onItemChanged",
-      arguments: [
-        itemId,
-        "tags",
-        false,
-        "",
-        PlacesUtils.toPRTime(bm.lastModified),
-        bm.type,
-        parentId,
-        bm.guid,
-        bm.parentGuid,
-        "",
-        Ci.nsINavBookmarksService.SOURCE_DEFAULT,
-      ],
+      type: "bookmark-tags-changed",
+      id: itemId,
+      itemType: bm.type,
+      url: bm.url,
+      guid: bm.guid,
+      parentGuid: bm.parentGuid,
+      lastModified: bm.lastModified,
+      source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+      isTagging: false,
     },
   ]);
 });
 
 add_task(async function update_bookmark_lastModified() {
   let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
   Preferences.set("privacy.reduceTimerPrecision", false);
 
@@ -540,65 +533,59 @@ add_task(async function remove_folder() 
 
 add_task(async function remove_bookmark_tag_notification() {
   let bm = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     url: new URL("http://untag.example.com/"),
   });
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
-  let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
 
   let tagFolder = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     parentGuid: PlacesUtils.bookmarks.tagsGuid,
     title: "tag",
   });
   let tag = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
     parentGuid: tagFolder.guid,
     url: new URL("http://untag.example.com/"),
   });
   let tagId = await PlacesUtils.promiseItemId(tag.guid);
   let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid);
 
-  let placesObserver = expectPlacesObserverNotifications(["bookmark-removed"]);
-  let observer = expectNotifications();
+  const observer = expectPlacesObserverNotifications([
+    "bookmark-removed",
+    "bookmark-tags-changed",
+  ]);
   await PlacesUtils.bookmarks.remove(tag.guid);
 
-  placesObserver.check([
+  observer.check([
     {
       type: "bookmark-removed",
       id: tagId,
       parentId: tagParentId,
       index: tag.index,
       url: tag.url,
       guid: tag.guid,
       parentGuid: tag.parentGuid,
       source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
       itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       isTagging: true,
     },
-  ]);
-  observer.check([
     {
-      name: "onItemChanged",
-      arguments: [
-        itemId,
-        "tags",
-        false,
-        "",
-        PlacesUtils.toPRTime(bm.lastModified),
-        bm.type,
-        parentId,
-        bm.guid,
-        bm.parentGuid,
-        "",
-        Ci.nsINavBookmarksService.SOURCE_DEFAULT,
-      ],
+      type: "bookmark-tags-changed",
+      id: itemId,
+      itemType: bm.type,
+      url: bm.url,
+      guid: bm.guid,
+      parentGuid: bm.parentGuid,
+      lastModified: bm.lastModified,
+      source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+      isTagging: false,
     },
   ]);
 });
 
 add_task(async function remove_folder_notification() {
   let folder1 = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
@@ -988,40 +975,8 @@ add_task(async function update_notitle_n
       guid: menuFolder.guid,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       lastModified: updatedMenuBm.lastModified,
       source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
       isTagging: false,
     },
   ]);
 });
-
-function expectNotifications() {
-  let notifications = [];
-  let observer = new Proxy(NavBookmarkObserver, {
-    get(target, name) {
-      if (name == "check") {
-        PlacesUtils.bookmarks.removeObserver(observer);
-        return expectedNotifications =>
-          Assert.deepEqual(notifications, expectedNotifications);
-      }
-
-      if (name.startsWith("onItem")) {
-        return (...origArgs) => {
-          let args = Array.from(origArgs, arg => {
-            if (arg && arg instanceof Ci.nsIURI) {
-              return new URL(arg.spec);
-            }
-            return arg;
-          });
-          notifications.push({ name, arguments: args });
-        };
-      }
-
-      if (name in target) {
-        return target[name];
-      }
-      return undefined;
-    },
-  });
-  PlacesUtils.bookmarks.addObserver(observer);
-  return observer;
-}
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests that each nsINavBookmarksObserver method gets the correct input.
+// Tests that each bookmark event gets the correct input.
 
 var gUnfiledFolderId;
 
 var gBookmarksObserver = {
   expected: [],
   setup(expected) {
     this.expected = expected;
     this.deferred = PromiseUtils.defer();
@@ -30,44 +30,19 @@ var gBookmarksObserver = {
       }
     }
 
     if (this.expected.length === 0) {
       this.deferred.resolve();
     }
   },
 
-  validate(aMethodName, aArguments) {
-    Assert.equal(this.expected[0].name, aMethodName);
-
-    let args = this.expected.shift().args;
-    Assert.equal(aArguments.length, args.length);
-    for (let i = 0; i < aArguments.length; i++) {
-      Assert.ok(
-        args[i].check(aArguments[i]),
-        aMethodName + "(args[" + i + "]: " + args[i].name + ")"
-      );
-    }
-
-    if (this.expected.length === 0) {
-      this.deferred.resolve();
-    }
-  },
-
   handlePlacesEvents(events) {
     this.validateEvents(events);
   },
-
-  // nsINavBookmarkObserver
-  onItemChanged() {
-    return this.validate("onItemChanged", arguments);
-  },
-
-  // nsISupports
-  QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]),
 };
 
 var gBookmarkSkipObserver = {
   skipTags: true,
 
   expected: null,
   setup(expected) {
     this.expected = expected;
@@ -83,62 +58,47 @@ var gBookmarkSkipObserver = {
       Assert.equal(expectedEventType, event.type);
     }
 
     if (this.expected.length === 0) {
       this.deferred.resolve();
     }
   },
 
-  validate(aMethodName) {
-    Assert.equal(this.expected.shift(), aMethodName);
-    if (this.expected.length === 0) {
-      this.deferred.resolve();
-    }
-  },
-
   handlePlacesEvents(events) {
     this.validateEvents(events);
   },
-
-  // nsINavBookmarkObserver
-  onItemChanged() {
-    return this.validate("onItemChanged", arguments);
-  },
-
-  // nsISupports
-  QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]),
 };
 
 add_task(async function setup() {
-  PlacesUtils.bookmarks.addObserver(gBookmarksObserver);
-  PlacesUtils.bookmarks.addObserver(gBookmarkSkipObserver);
   gUnfiledFolderId = await PlacesUtils.promiseItemId(
     PlacesUtils.bookmarks.unfiledGuid
   );
   gBookmarksObserver.handlePlacesEvents = gBookmarksObserver.handlePlacesEvents.bind(
     gBookmarksObserver
   );
   gBookmarkSkipObserver.handlePlacesEvents = gBookmarkSkipObserver.handlePlacesEvents.bind(
     gBookmarkSkipObserver
   );
   PlacesUtils.observers.addListener(
     [
       "bookmark-added",
       "bookmark-removed",
       "bookmark-moved",
+      "bookmark-tags-changed",
       "bookmark-title-changed",
     ],
     gBookmarksObserver.handlePlacesEvents
   );
   PlacesUtils.observers.addListener(
     [
       "bookmark-added",
       "bookmark-removed",
       "bookmark-moved",
+      "bookmark-tags-changed",
       "bookmark-title-changed",
     ],
     gBookmarkSkipObserver.handlePlacesEvents
   );
 });
 
 add_task(async function bookmarkItemAdded_bookmark() {
   const title = "Bookmark 1";
@@ -263,17 +223,17 @@ add_task(async function bookmarkItemAdde
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     title,
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
   });
   await promise;
 });
 
-add_task(async function onItemChanged_title_bookmark() {
+add_task(async function bookmarkTitleChanged() {
   let bm = await PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     index: 0,
   });
   const title = "New title";
   let promise = Promise.all([
     gBookmarkSkipObserver.setup(["bookmark-title-changed"]),
     gBookmarksObserver.setup([
@@ -299,25 +259,28 @@ add_task(async function onItemChanged_ti
         ],
       },
     ]),
   ]);
   await PlacesUtils.bookmarks.update({ guid: bm.guid, title });
   await promise;
 });
 
-add_task(async function onItemChanged_tags_bookmark() {
+add_task(async function bookmarkTagsChanged() {
   let bm = await PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     index: 0,
   });
   let uri = Services.io.newURI(bm.url.href);
   const TAG = "tag";
   let promise = Promise.all([
-    gBookmarkSkipObserver.setup(["onItemChanged", "onItemChanged"]),
+    gBookmarkSkipObserver.setup([
+      "bookmark-tags-changed",
+      "bookmark-tags-changed",
+    ]),
     gBookmarksObserver.setup([
       {
         eventType: "bookmark-added", // This is the tag folder.
         args: [
           { name: "id", check: v => typeof v == "number" && v > 0 },
           { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
           { name: "index", check: v => v === 0 },
           {
@@ -366,42 +329,41 @@ add_task(async function onItemChanged_ta
           {
             name: "source",
             check: v =>
               Object.values(PlacesUtils.bookmarks.SOURCES).includes(v),
           },
         ],
       },
       {
-        name: "onItemChanged",
+        eventType: "bookmark-tags-changed",
         args: [
-          { name: "itemId", check: v => typeof v == "number" && v > 0 },
-          { name: "property", check: v => v === "tags" },
-          { name: "isAnno", check: v => v === false },
-          { name: "newValue", check: v => v === "" },
-          { name: "lastModified", check: v => typeof v == "number" && v > 0 },
+          { name: "id", check: v => typeof v == "number" && v > 0 },
           {
             name: "itemType",
             check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK,
           },
-          { name: "parentId", check: v => v === gUnfiledFolderId },
           {
             name: "guid",
             check: v => typeof v == "string" && PlacesUtils.isValidGuid(v),
           },
           {
             name: "parentGuid",
             check: v => typeof v == "string" && PlacesUtils.isValidGuid(v),
           },
-          { name: "oldValue", check: v => typeof v == "string" },
+          { name: "lastModified", check: v => typeof v == "number" && v > 0 },
           {
             name: "source",
             check: v =>
               Object.values(PlacesUtils.bookmarks.SOURCES).includes(v),
           },
+          {
+            name: "isTagging",
+            check: v => v === false,
+          },
         ],
       },
       {
         eventType: "bookmark-removed", // This is the tag.
         args: [
           { name: "id", check: v => typeof v == "number" && v > 0 },
           { name: "parentId", check: v => typeof v == "number" && v > 0 },
           { name: "index", check: v => v === 0 },
@@ -420,44 +382,42 @@ add_task(async function onItemChanged_ta
           },
           {
             name: "source",
             check: v =>
               Object.values(PlacesUtils.bookmarks.SOURCES).includes(v),
           },
         ],
       },
-
       {
-        name: "onItemChanged",
+        eventType: "bookmark-tags-changed",
         args: [
-          { name: "itemId", check: v => typeof v == "number" && v > 0 },
-          { name: "property", check: v => v === "tags" },
-          { name: "isAnno", check: v => v === false },
-          { name: "newValue", check: v => v === "" },
-          { name: "lastModified", check: v => typeof v == "number" && v > 0 },
+          { name: "id", check: v => typeof v == "number" && v > 0 },
           {
             name: "itemType",
             check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK,
           },
-          { name: "parentId", check: v => v === gUnfiledFolderId },
           {
             name: "guid",
             check: v => typeof v == "string" && PlacesUtils.isValidGuid(v),
           },
           {
             name: "parentGuid",
             check: v => typeof v == "string" && PlacesUtils.isValidGuid(v),
           },
-          { name: "oldValue", check: v => typeof v == "string" },
+          { name: "lastModified", check: v => typeof v == "number" && v > 0 },
           {
             name: "source",
             check: v =>
               Object.values(PlacesUtils.bookmarks.SOURCES).includes(v),
           },
+          {
+            name: "isTagging",
+            check: v => v === false,
+          },
         ],
       },
       {
         eventType: "bookmark-removed", // This is the tag folder.
         args: [
           { name: "id", check: v => typeof v == "number" && v > 0 },
           { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
           { name: "index", check: v => v === 0 },
@@ -947,19 +907,29 @@ add_task(async function bookmarkItemRemo
     title: BMTITLE,
   });
 
   await PlacesUtils.bookmarks.remove(folder);
   await promise;
 });
 
 add_task(function cleanup() {
-  PlacesUtils.bookmarks.removeObserver(gBookmarksObserver);
-  PlacesUtils.bookmarks.removeObserver(gBookmarkSkipObserver);
   PlacesUtils.observers.removeListener(
-    ["bookmark-added"],
+    [
+      "bookmark-added",
+      "bookmark-removed",
+      "bookmark-moved",
+      "bookmark-tags-changed",
+      "bookmark-title-changed",
+    ],
     gBookmarksObserver.handlePlacesEvents
   );
   PlacesUtils.observers.removeListener(
-    ["bookmark-added"],
+    [
+      "bookmark-added",
+      "bookmark-removed",
+      "bookmark-moved",
+      "bookmark-tags-changed",
+      "bookmark-title-changed",
+    ],
     gBookmarkSkipObserver.handlePlacesEvents
   );
 });
rename from toolkit/components/places/tests/unit/test_onItemChanged_tags.js
rename to toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js
--- a/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
+++ b/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js
@@ -1,68 +1,55 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // This test checks that changing a tag for a bookmark with multiple tags
-// notifies OnItemChanged("tags") only once, and not once per tag.
+// notifies bookmark-tags-changed event only once, and not once per tag.
 
 add_task(async function run_test() {
   let tags = ["a", "b", "c"];
   let uri = Services.io.newURI("http://1.moz.org/");
 
   let bookmark = await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     url: uri,
     title: "Bookmark 1",
   });
   PlacesUtils.tagging.tagURI(uri, tags);
 
   let promise = PromiseUtils.defer();
 
   let bookmarksObserver = {
-    QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]),
-
     _changedCount: 0,
-    onItemChanged(
-      aItemId,
-      aProperty,
-      aIsAnnotationProperty,
-      aValue,
-      aLastModified,
-      aItemType,
-      aParentId,
-      aGuid
-    ) {
-      if (aProperty == "tags") {
-        Assert.equal(aGuid, bookmark.guid);
-        this._changedCount++;
-      }
-    },
     handlePlacesEvents(events) {
       for (let event of events) {
         switch (event.type) {
           case "bookmark-removed":
             if (event.guid == bookmark.guid) {
               PlacesUtils.observers.removeListener(
                 ["bookmark-removed"],
                 this.handlePlacesEvents
               );
               Assert.equal(this._changedCount, 2);
               promise.resolve();
             }
+            break;
+          case "bookmark-tags-changed":
+            Assert.equal(event.guid, bookmark.guid);
+            this._changedCount++;
+            break;
         }
       }
     },
   };
-  PlacesUtils.bookmarks.addObserver(bookmarksObserver);
   bookmarksObserver.handlePlacesEvents = bookmarksObserver.handlePlacesEvents.bind(
     bookmarksObserver
   );
   PlacesUtils.observers.addListener(
-    ["bookmark-removed"],
+    ["bookmark-removed", "bookmark-tags-changed"],
     bookmarksObserver.handlePlacesEvents
   );
 
   PlacesUtils.tagging.tagURI(uri, ["d"]);
   PlacesUtils.tagging.tagURI(uri, ["e"]);
 
   await promise;
 
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -34,16 +34,17 @@ skip-if = os == "linux" # Bug 821781
 [test_486978_sort_by_date_queries.js]
 [test_536081.js]
 [test_1085291.js]
 [test_1105208.js]
 [test_1105866.js]
 [test_1606731.js]
 [test_asyncExecuteLegacyQueries.js]
 [test_async_transactions.js]
+[test_bookmark-tags-changed_frequency.js]
 [test_bookmarks_json.js]
 [test_bookmarks_json_corrupt.js]
 [test_bookmarks_html.js]
 [test_bookmarks_html_corrupt.js]
 [test_bookmarks_html_escape_entities.js]
 [test_bookmarks_html_import_tags.js]
 [test_bookmarks_html_singleframe.js]
 [test_bookmarks_restore_notification.js]
@@ -70,17 +71,16 @@ skip-if = os == "linux" # Bug 821781
 [test_missing_builtin_folders.js]
 support-files = missingBuiltIn.sqlite
 [test_missing_root_folder.js]
 support-files = noRoot.sqlite
 [test_multi_observation.js]
 [test_multi_word_tags.js]
 [test_nsINavHistoryViewer.js]
 [test_null_interfaces.js]
-[test_onItemChanged_tags.js]
 [test_origins.js]
 [test_origins_parsing.js]
 [test_pageGuid_bookmarkGuid.js]
 [test_frecency_observers.js]
 [test_PlacesDBUtils_removeOldCorruptDBs.js]
 [test_placeURIs.js]
 [test_PlacesUtils_invalidateCachedGuidFor.js]
 [test_PlacesUtils_invalidateCachedGuids.js]