FEATURE: Allow selective dismissal of new and unread topics (#12976)

FEATURE: Allow selective dismissal of new and unread topics (#12976)

This PR improves the UI of bulk select so that its context is applied to the Dismiss Unread and Dismiss New buttons. Regular users (not just staff) are now able to use topic bulk selection on the /new and /unread routes to perform these dismiss actions more selectively.

For Dismiss Unread, there is a new count in the text of the button and in the modal when one or more topic is selected with the bulk select checkboxes.

For Dismiss New, there is a count in the button text, and we have added functionality to the server side to accept an array of topic ids to dismiss new for, instead of always having to dismiss all new, the same as the bulk dismiss unread functionality. To clean things up, the DismissTopics service has been rolled into the TopicsBulkAction service.

We now also show the top Dismiss/Dismiss New button based on whether the bottom one is in the viewport, not just based on the topic count.

diff --git a/app/assets/javascripts/discourse/app/components/bulk-select-button.js b/app/assets/javascripts/discourse/app/components/bulk-select-button.js
index 9f46112..e3359e1 100644
--- a/app/assets/javascripts/discourse/app/components/bulk-select-button.js
+++ b/app/assets/javascripts/discourse/app/components/bulk-select-button.js
@@ -1,5 +1,6 @@
 import Component from "@ember/component";
 import { schedule } from "@ember/runloop";
+import { reads } from "@ember/object/computed";
 import showModal from "discourse/lib/show-modal";
 
 export default Component.extend({
@@ -17,6 +18,8 @@ export default Component.extend({
     });
   },
 
+  canDoBulkActions: reads("currentUser.staff"),
+
   actions: {
     showBulkActions() {
       const controller = showModal("topic-bulk-actions", {
diff --git a/app/assets/javascripts/discourse/app/components/topic-dismiss-buttons.js b/app/assets/javascripts/discourse/app/components/topic-dismiss-buttons.js
new file mode 100644
index 0000000..5092bfe
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-dismiss-buttons.js
@@ -0,0 +1,105 @@
+import { action } from "@ember/object";
+import showModal from "discourse/lib/show-modal";
+import { later } from "@ember/runloop";
+import isElementInViewport from "discourse/lib/is-element-in-viewport";
+import discourseComputed, { on } from "discourse-common/utils/decorators";
+import I18n from "I18n";
+import Component from "@ember/component";
+
+export default Component.extend({
+  tagName: "",
+  classNames: ["topic-dismiss-buttons"],
+
+  position: null,
+  selectedTopics: null,
+  model: null,
+
+  @discourseComputed("position")
+  containerClass(position) {
+    return `dismiss-container-${position}`;
+  },
+
+  @discourseComputed("position")
+  dismissReadId(position) {
+    return `dismiss-topics-${position}`;
+  },
+
+  @discourseComputed("position")
+  dismissNewId(position) {
+    return `dismiss-new-${position}`;
+  },
+
+  @discourseComputed(
+    "position",
+    "isOtherDismissUnreadButtonVisible",
+    "isOtherDismissNewButtonVisible"
+  )
+  showBasedOnPosition(
+    position,
+    isOtherDismissUnreadButtonVisible,
+    isOtherDismissNewButtonVisible
+  ) {
+    if (position !== "top") {
+      return true;
+    }
+
+    return !(
+      isOtherDismissUnreadButtonVisible || isOtherDismissNewButtonVisible
+    );
+  },
+
+  @discourseComputed("selectedTopics.length")
+  dismissLabel(selectedTopicCount) {
+    if (selectedTopicCount === 0) {
+      return I18n.t("topics.bulk.dismiss_button");
+    }
+    return I18n.t("topics.bulk.dismiss_button_with_selected", {
+      count: selectedTopicCount,
+    });
+  },
+
+  @discourseComputed("selectedTopics.length")
+  dismissNewLabel(selectedTopicCount) {
+    if (selectedTopicCount === 0) {
+      return I18n.t("topics.bulk.dismiss_new");
+    }
+    return I18n.t("topics.bulk.dismiss_new_with_selected", {
+      count: selectedTopicCount,
+    });
+  },
+
+  // we want to only render the Dismiss... button at the top of the
+  // page if the user cannot see the bottom Dismiss... button based on their
+  // viewport, or if too many topics fill the page
+  @on("didInsertElement")
+  _determineOtherDismissVisibility() {
+    later(() => {
+      if (this.position === "top") {
+        this.set(
+          "isOtherDismissUnreadButtonVisible",
+          isElementInViewport(document.getElementById("dismiss-topics-bottom"))
+        );
+        this.set(
+          "isOtherDismissNewButtonVisible",
+          isElementInViewport(document.getElementById("dismiss-new-bottom"))
+        );
+      } else {
+        this.set("isOtherDismissUnreadButtonVisible", true);
+        this.set("isOtherDismissNewButtonVisible", true);
+      }
+    });
+  },
+
+  @action
+  dismissReadPosts() {
+    let dismissTitle = "topics.bulk.dismiss_read";
+    if (this.selectedTopics.length > 0) {
+      dismissTitle = "topics.bulk.dismiss_read_with_selected";
+    }
+    showModal("dismiss-read", {
+      titleTranslated: I18n.t(dismissTitle, {
+        count: this.selectedTopics.length,
+      }),
+    });
+  },
+});
diff --git a/app/assets/javascripts/discourse/app/components/watch-read.js b/app/assets/javascripts/discourse/app/components/watch-read.js
index 6603068..e375f4e 100644
--- a/app/assets/javascripts/discourse/app/components/watch-read.js
+++ b/app/assets/javascripts/discourse/app/components/watch-read.js
@@ -13,7 +13,7 @@ export default Component.extend({
     if (path === "faq" || path === "guidelines") {
       $(window).on("load.faq resize.faq scroll.faq", () => {
         const faqUnread = !currentUser.get("read_faq");
-        if (faqUnread && isElementInViewport($(".contents p").last())) {
+        if (faqUnread && isElementInViewport($(".contents p").last()[0])) {
           this.action();
         }
       });
diff --git a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js
index 6e33eb2..99afc1d 100644
--- a/app/assets/javascripts/discourse/app/controllers/discovery/topics.js
+++ b/app/assets/javascripts/discourse/app/controllers/discovery/topics.js
@@ -18,7 +18,6 @@ import discourseComputed from "discourse-common/utils/decorators";
 import { endWith } from "discourse/lib/computed";
 import { routeAction } from "discourse/helpers/route-action";
 import { inject as service } from "@ember/service";
-import showModal from "discourse/lib/show-modal";
 import { userPath } from "discourse/lib/url";
 
 const controllerOpts = {
@@ -39,6 +38,18 @@ const controllerOpts = {
   order: readOnly("model.params.order"),
   ascending: readOnly("model.params.ascending"),
 
+  selected: null,
+
+  @discourseComputed("model.filter", "model.topics.length")
+  showDismissRead(filter, topicsLength) {
+    return this._isFilterPage(filter, "unread") && topicsLength > 0;
+  },
+
+  @discourseComputed("model.filter", "model.topics.length")
+  showResetNew(filter, topicsLength) {
+    return this._isFilterPage(filter, "new") && topicsLength > 0;
+  },
+
   actions: {
     changeSort() {
       deprecated(
@@ -98,17 +109,20 @@ const controllerOpts = {
         (this.router.currentRoute.queryParams["f"] ||
           this.router.currentRoute.queryParams["filter"]) === "tracked";
 
-      Topic.resetNew(this.category, !this.noSubcategories, tracked).then(() =>
+      let topicIds = this.selected
+        ? this.selected.map((topic) => topic.id)
+        : null;
+
+      Topic.resetNew(this.category, !this.noSubcategories, {
+        tracked,
+        topicIds,
+      }).then(() =>
         this.send(
           "refresh",
           tracked ? { skipResettingParams: ["filter", "f"] } : {}
         )
       );
     },
-
-    dismissReadPosts() {
-      showModal("dismiss-read", { title: "topics.bulk.dismiss_read" });
-    },
   },
 
   afterRefresh(filter, list, listModel = list) {
@@ -122,32 +136,6 @@ const controllerOpts = {
     this.send("loadingComplete");
   },
 
-  isFilterPage: function (filter, filterType) {
-    if (!filter) {
-      return false;
-    }
-    return filter.match(new RegExp(filterType + "$", "gi")) ? true : false;
-  },
-
-  @discourseComputed("model.filter", "model.topics.length")
-  showDismissRead(filter, topicsLength) {
-    return this.isFilterPage(filter, "unread") && topicsLength > 0;
-  },
-
-  @discourseComputed("model.filter", "model.topics.length")
-  showResetNew(filter, topicsLength) {
-    return this.isFilterPage(filter, "new") && topicsLength > 0;
-  },
-
-  @discourseComputed("model.filter", "model.topics.length")
-  showDismissAtTop(filter, topicsLength) {
-    return (
-      (this.isFilterPage(filter, "new") ||
-        this.isFilterPage(filter, "unread")) &&
-      topicsLength >= 15
-    );
-  },
-
   hasTopics: gt("model.topics.length", 0),
   allLoaded: empty("model.more_topics_url"),
   latest: endWith("model.filter", "latest"),

[... diff too long, it was truncated ...]

GitHub sha: 7a79bd7d

1 Like

This commit appears in #12976 which was approved by CvX. It was merged by martin.

This commit has been mentioned on Discourse Meta. There might be relevant details there: