diff --git a/app/javascript/application.ts b/app/javascript/application.ts index 55decb5ce..dd48e3b02 100644 --- a/app/javascript/application.ts +++ b/app/javascript/application.ts @@ -146,6 +146,7 @@ var StoryView = Backbone.NativeView.extend({ itemRead: function() { this.el.classList.toggle("read", this.model.get("is_read")); + this.el.dispatchEvent(new CustomEvent("story:read-changed", { bubbles: true })); }, itemSelected: function() { @@ -169,6 +170,7 @@ var StoryView = Backbone.NativeView.extend({ keepUnreadToggleKeepUnreadValue: String(jsonModel.keep_unread), starToggleIdValue: String(jsonModel.id), starToggleStarredValue: String(jsonModel.is_starred), + unreadCountTarget: "story", }); return this; }, @@ -257,10 +259,6 @@ var StoryList = Backbone.Collection.extend({ this.at(this.cursorPosition).toggleKeepUnread(); }, - unreadCount: function() { - return this.where({is_read: false}).length; - }, - unselectAll: function() { _.invoke(this.selected(), "unselect"); }, @@ -290,7 +288,6 @@ var AppView = Backbone.NativeView.extend({ this.listenTo(this.stories, 'add', this.addOne); this.listenTo(this.stories, 'reset', this.addAll); - this.listenTo(this.stories, 'all', this.render); }, loadData: function(data) { @@ -309,16 +306,6 @@ var AppView = Backbone.NativeView.extend({ this.stories.openCurrentSelection(); }, - render: function() { - var unreadCount = this.stories.unreadCount(); - - if (unreadCount === 0) { - document.title = window.i18n.titleName; - } else { - document.title = "(" + unreadCount + ") " + window.i18n.titleName; - } - }, - toggleCurrent: function() { this.stories.toggleCurrent(); }, diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts index 806d7496a..6a53fd8da 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.ts @@ -20,3 +20,6 @@ application.register("mark-all-as-read", MarkAllAsReadController); import StarToggleController from "./star_toggle_controller"; application.register("star-toggle", StarToggleController); + +import UnreadCountController from "./unread_count_controller"; +application.register("unread-count", UnreadCountController); diff --git a/app/javascript/controllers/unread_count_controller.ts b/app/javascript/controllers/unread_count_controller.ts new file mode 100644 index 000000000..4dae1c806 --- /dev/null +++ b/app/javascript/controllers/unread_count_controller.ts @@ -0,0 +1,39 @@ +import {Controller} from "@hotwired/stimulus"; + +/* + * Mirrors the number of unread stories into the document title, e.g. + * "(3) Stringer". The DOM is the source of truth: any story target without + * the "read" class counts as unread. Target callbacks re-count when stories + * are added or removed; read-state flips are announced by whoever owns them + * (currently the Backbone StoryView) as a bubbling "story:read-changed" + * event, routed here via data-action on the container. + */ +export default class extends Controller { + static override targets = ["story"]; + + static override values = {title: String}; + + declare titleValue: string; + + override connect(): void { + this.update(); + } + + storyTargetConnected(): void { + this.update(); + } + + storyTargetDisconnected(): void { + this.update(); + } + + update(): void { + const unread = this.element.querySelectorAll(".story:not(.read)").length; + + if (unread === 0) { + document.title = this.titleValue; + } else { + document.title = `(${unread}) ${this.titleValue}`; + } + } +} diff --git a/app/views/feeds/show.html.erb b/app/views/feeds/show.html.erb index 8a539df9a..3dcfeed74 100644 --- a/app/views/feeds/show.html.erb +++ b/app/views/feeds/show.html.erb @@ -14,7 +14,7 @@ <%= render "stories/js", { stories: @stories } %> -