REFACTOR: Move Page title / focus / counts logic to service

REFACTOR: Move Page title / focus / counts logic to service

We had a handful of methods attached to the root Discourse object related to focus and notification counts.

This patch pulls them out into a service called document-title for updating the title, and a component called d-document to attach and listen for browser events related to focus.

It also removes some computed properties and observers in favor of plain old Javascript objects.

diff --git a/app/assets/javascripts/discourse/app/app.js b/app/assets/javascripts/discourse/app/app.js
index 836ad69..4a17b54 100644
--- a/app/assets/javascripts/discourse/app/app.js
+++ b/app/assets/javascripts/discourse/app/app.js
@@ -2,8 +2,7 @@
 import Application from "@ember/application";
 import { computed } from "@ember/object";
 import { buildResolver } from "discourse-common/resolver";
-import { bind } from "@ember/runloop";
-import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import discourseComputed from "discourse-common/utils/decorators";
 import { default as getURL, getURLWithCDN } from "discourse-common/lib/get-url";
 import deprecated from "discourse-common/lib/deprecated";
 
@@ -11,10 +10,7 @@ const _pluginCallbacks = [];
 
 const Discourse = Application.extend({
   rootElement: "#main",
-  _docTitle: document.title,
   __widget_helpers: {},
-  hasFocus: null,
-  _boundFocusChange: null,
 
   customEvents: {
     paste: "paste"
@@ -23,24 +19,6 @@ const Discourse = Application.extend({
   reset() {
     this._super(...arguments);
     Mousetrap.reset();
-
-    document.removeEventListener("visibilitychange", this._boundFocusChange);
-    document.removeEventListener("resume", this._boundFocusChange);
-    document.removeEventListener("freeze", this._boundFocusChange);
-
-    this._boundFocusChange = null;
-  },
-
-  ready() {
-    this._super(...arguments);
-    this._boundFocusChange = bind(this, this._focusChanged);
-
-    // Default to true
-    this.set("hasFocus", true);
-
-    document.addEventListener("visibilitychange", this._boundFocusChange);
-    document.addEventListener("resume", this._boundFocusChange);
-    document.addEventListener("freeze", this._boundFocusChange);
   },
 
   getURL(url) {
@@ -61,72 +39,6 @@ const Discourse = Application.extend({
 
   Resolver: buildResolver("discourse"),
 
-  @observes("_docTitle", "hasFocus", "contextCount", "notificationCount")
-  _titleChanged() {
-    let title = this._docTitle || this.SiteSettings.title;
-
-    let displayCount = this.displayCount;
-    let dynamicFavicon = this.currentUser && this.currentUser.dynamic_favicon;
-    if (displayCount > 0 && !dynamicFavicon) {
-      title = `(${displayCount}) ${title}`;
-    }
-
-    document.title = title;
-  },
-
-  @discourseComputed("contextCount", "notificationCount")
-  displayCount() {
-    return this.currentUser &&
-      this.currentUser.get("title_count_mode") === "notifications"
-      ? this.notificationCount
-      : this.contextCount;
-  },
-
-  @observes("contextCount", "notificationCount")
-  faviconChanged() {
-    if (this.currentUser && this.currentUser.get("dynamic_favicon")) {
-      let url = this.SiteSettings.site_favicon_url;
-
-      // Since the favicon is cached on the browser for a really long time, we
-      // append the favicon_url as query params to the path so that the cache
-      // is not used when the favicon changes.
-      if (/^http/.test(url)) {
-        url = getURL("/favicon/proxied?" + encodeURIComponent(url));
-      }
-
-      new window.Favcount(url).set(this.displayCount);
-    }
-  },
-
-  updateContextCount(count) {
-    this.set("contextCount", count);
-  },
-
-  updateNotificationCount(count) {
-    if (!this.hasFocus) {
-      this.set("notificationCount", count);
-    }
-  },
-
-  incrementBackgroundContextCount() {
-    if (!this.hasFocus) {
-      this.set("backgroundNotify", true);
-      this.set("contextCount", (this.contextCount || 0) + 1);
-    }
-  },
-
-  @observes("hasFocus")
-  resetCounts() {
-    if (this.hasFocus && this.backgroundNotify) {
-      this.set("contextCount", 0);
-    }
-    this.set("backgroundNotify", false);
-
-    if (this.hasFocus) {
-      this.set("notificationCount", 0);
-    }
-  },
-
   authenticationComplete(options) {
     // TODO, how to dispatch this to the controller without the container?
     const loginController = this.__container__.lookup("controller:login");
@@ -192,19 +104,7 @@ const Discourse = Application.extend({
       }
       return this.currentAssetVersion;
     }
-  }),
-
-  _focusChanged() {
-    if (document.visibilityState === "hidden") {
-      if (this.hasFocus) {
-        this.set("hasFocus", false);
-        this.appEvents.trigger("discourse:focus-changed", false);
-      }
-    } else if (!this.hasFocus) {
-      this.set("hasFocus", true);
-      this.appEvents.trigger("discourse:focus-changed", true);
-    }
-  }
+  })
 });
 
 export default Discourse;
diff --git a/app/assets/javascripts/discourse/app/components/d-document.js b/app/assets/javascripts/discourse/app/components/d-document.js
new file mode 100644
index 0000000..ec022e8
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/d-document.js
@@ -0,0 +1,58 @@
+import Component from "@ember/component";
+import { bind } from "@ember/runloop";
+import { inject as service } from "@ember/service";
+
+export default Component.extend({
+  _boundFocusChange: null,
+  tagName: "",
+  documentTitle: service(),
+
+  didInsertElement() {
+    this._super(...arguments);
+
+    this.documentTitle.setTitle(document.title);
+    this._boundFocusChange = bind(this, this._focusChanged);
+    document.addEventListener("visibilitychange", this._boundFocusChange);
+    document.addEventListener("resume", this._boundFocusChange);
+    document.addEventListener("freeze", this._boundFocusChange);
+    this.session.hasFocus = true;
+
+    this.appEvents.on("notifications:changed", this, this._updateNotifications);
+  },
+
+  willDestroyElement() {
+    this._super(...arguments);
+
+    document.removeEventListener("visibilitychange", this._boundFocusChange);
+    document.removeEventListener("resume", this._boundFocusChange);
+    document.removeEventListener("freeze", this._boundFocusChange);
+    this._boundFocusChange = null;
+
+    this.appEvents.off(
+      "notifications:changed",
+      this,
+      this._updateNotifications
+    );
+  },
+
+  _updateNotifications() {
+    if (!this.currentUser) {
+      return;
+    }
+
+    this.documentTitle.updateNotificationCount(
+      this.currentUser.unread_notifications +
+        this.currentUser.unread_high_priority_notifications
+    );
+  },
+
+  _focusChanged() {
+    if (document.visibilityState === "hidden") {
+      if (this.session.hasFocus) {
+        this.documentTitle.setFocus(false);
+      }
+    } else if (!this.hasFocus) {
+      this.documentTitle.setFocus(true);
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/app/components/discovery-topics-list.js b/app/assets/javascripts/discourse/app/components/discovery-topics-list.js
index 28ab1ef..f84761f 100644
--- a/app/assets/javascripts/discourse/app/components/discovery-topics-list.js
+++ b/app/assets/javascripts/discourse/app/components/discovery-topics-list.js
@@ -3,10 +3,12 @@ import Component from "@ember/component";
 import { on, observes } from "discourse-common/utils/decorators";
 import LoadMore from "discourse/mixins/load-more";
 import UrlRefresh from "discourse/mixins/url-refresh";
+import { inject as service } from "@ember/service";
 
 const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
   classNames: ["contents"],
   eyelineSelector: ".topic-list-item",
+  documentTitle: service(),
 
   @on("didInsertElement")
   @observes("model")
@@ -26,7 +28,7 @@ const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
 
   @observes("incomingCount")
   _updateTitle() {
-    Discourse.updateContextCount(this.incomingCount);
+    this.documentTitle.updateContextCount(this.incomingCount);
   },
 
   saveScrollPosition() {
@@ -35,7 +37,7 @@ const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
 
   actions: {
     loadMore() {
-      Discourse.updateContextCount(0);
+      this.documentTitle.updateContextCount(0);
       this.model.loadMore().then(hasMoreResults => {
         schedule("afterRender", () => this.saveScrollPosition());

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

GitHub sha: 92b26ecb

1 Like

This commit appears in #10347 which was approved by davidtaylorhq. It was merged by eviltrout.