diff --git a/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js b/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js index e68fd2dafab..31a19ebc1fe 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js @@ -1,72 +1,31 @@ -import I18n from "I18n"; - import GlimmerComponent from "discourse/components/glimmer"; import Composer from "discourse/models/composer"; import { getOwner } from "discourse-common/lib/get-owner"; import PermissionType from "discourse/models/permission-type"; -import discourseDebounce from "discourse-common/lib/debounce"; +import { customSectionLinks } from "discourse/lib/sidebar/custom-topics-section-links"; +import EverythingSectionLink from "discourse/lib/sidebar/topics-section/everything-section-link"; +import TrackedSectionLink from "discourse/lib/sidebar/topics-section/tracked-section-link"; +import BookmarkedSectionLink from "discourse/lib/sidebar/topics-section/bookmarked-section-link"; import { action } from "@ember/object"; import { next } from "@ember/runloop"; -import { tracked } from "@glimmer/tracking"; + +const DEFAULT_SECTION_LINKS = [ + EverythingSectionLink, + TrackedSectionLink, + BookmarkedSectionLink, +]; export default class SidebarTopicsSection extends GlimmerComponent { - @tracked totalUnread = 0; - @tracked totalNew = 0; - - constructor(owner, args) { - super(owner, args); - this._refreshSectionCounts(); - - this.topicTrackingState.onStateChange( - this._topicTrackingStateUpdated.bind(this) - ); - } - - _topicTrackingStateUpdated() { - // refreshing section counts by looping through the states in topicTrackingState is an expensive operation so - // we debounce this. - discourseDebounce(this, this._refreshSectionCounts, 100); - } - - _refreshSectionCounts() { - let totalUnread = 0; - let totalNew = 0; - - this.topicTrackingState.forEachTracked((topic, isNew, isUnread) => { - if (isNew) { - totalNew += 1; - } else if (isUnread) { - totalUnread += 1; + get sectionLinks() { + return [...DEFAULT_SECTION_LINKS, ...customSectionLinks].map( + (sectionLinkClass) => { + return new sectionLinkClass({ + topicTrackingState: this.topicTrackingState, + currentUser: this.currentUser, + }); } - }); - - this.totalUnread = totalUnread; - this.totalNew = totalNew; - } - - get everythingSectionLinkBadgeText() { - if (this.totalUnread > 0) { - return I18n.t("sidebar.unread_count", { - count: this.totalUnread, - }); - } else if (this.totalNew > 0) { - return I18n.t("sidebar.new_count", { - count: this.totalNew, - }); - } else { - return; - } - } - - get everythingSectionLinkRoute() { - if (this.totalUnread > 0) { - return "discovery.unread"; - } else if (this.totalNew > 0) { - return "discovery.new"; - } else { - return "discovery.latest"; - } + ); } @action diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 74aff2737d3..e586be839c6 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -94,6 +94,7 @@ import { import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser"; import { downloadCalendar } from "discourse/lib/download-calendar"; import { consolePrefix } from "discourse/lib/source-identifier"; +import { addSectionLink } from "discourse/lib/sidebar/custom-topics-section-links"; // If you add any methods to the API ensure you bump up the version number // based on Semantic Versioning 2.0.0. Please update the changelog at @@ -1622,6 +1623,44 @@ class PluginApi { customizeComposerText(callbacks) { registerCustomizationCallback(callbacks); } + + /** + * EXPERIMENTAL. Do not use. + * Support for adding a link under Sidebar topics section by returning a class which extends from the BaseSectionLink + * class interface. See `lib/sidebar/topics-section/base-section-link.js` for documentation on the BaseSectionLink class + * interface. + * + * ``` + * api.addTopicsSectionLink((baseSectionLink) => { + * return class CustomSectionLink extends baseSectionLink { + * get name() { + * returns "bookmarked" + * } + * + * get route() { + * returns "userActivity.bookmarks" + * } + * + * get title() { + * return I18n.t("sidebar.sections.topics.links.bookmarked.title"); + * } + * + * get text() { + * return I18n.t("sidebar.sections.topics.links.bookmarked.content"); + * } + * } + * }) + * ``` + * + * @callback addTopicsSectionLinkCallback + * @param {BaseSectionLink} baseSectionLink Factory class to inherit from. + * @returns {BaseSectionLink} A class that extends BaseSectionLink. + * + * @param {addTopicsSectionLinkCallback} callback + */ + async addTopicsSectionLink(callback) { + addSectionLink(callback); + } } // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/custom-topics-section-links.js b/app/assets/javascripts/discourse/app/lib/sidebar/custom-topics-section-links.js new file mode 100644 index 00000000000..22543a0a043 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/custom-topics-section-links.js @@ -0,0 +1,19 @@ +import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link"; + +export let customSectionLinks = []; + +/** + * Appends an additional section link under the topics section + * @callback addSectionLinkCallback + * @param {BaseSectionLink} baseSectionLink Factory class to inherit from. + * @returns {BaseSectionLink} A class that extends BaseSectionLink. + * + * @param {addTopicsSectionLinkCallback} callback + */ +export function addSectionLink(callback) { + customSectionLinks.push(callback.call(this, BaseSectionLink)); +} + +export function resetDefaultSectionLinks() { + customSectionLinks = []; +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js new file mode 100644 index 00000000000..61ed6b53ac4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js @@ -0,0 +1,63 @@ +/** + * Base class representing a sidebar topics section link interface. + */ +export default class BaseSectionLink { + constructor({ topicTrackingState, currentUser } = {}) { + this.topicTrackingState = topicTrackingState; + this.currentUser = currentUser; + } + + /** + * @returns {string} The name of the section link + */ + get name() { + this._notImplemented(); + } + + /** + * @returns {string} Ember route + */ + get route() { + this._notImplemented(); + } + + /** + * @returns {Object} Model for component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo + */ + get model() {} + + /** + * @returns {Object} Query parameters for component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo + */ + get query() { + return {}; + } + + /** + * @returns {String} current-when for component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo + */ + get currentWhen() {} + + /** + * @returns {string} Title for the link + */ + get title() { + this._notImplemented(); + } + + /** + * @returns {string} Text for the link + */ + get text() { + this._notImplemented(); + } + + /** + * @returns {string} Text for the badge within the link + */ + get badgeText() {} + + _notImplemented() { + throw "not implemented"; + } +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/bookmarked-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/bookmarked-section-link.js new file mode 100644 index 00000000000..bee631e2b60 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/bookmarked-section-link.js @@ -0,0 +1,25 @@ +import I18n from "I18n"; + +import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link"; + +export default class BookmarkedSectionLink extends BaseSectionLink { + get name() { + return "bookmarked"; + } + + get route() { + return "userActivity.bookmarks"; + } + + get model() { + return this.currentUser; + } + + get title() { + return I18n.t("sidebar.sections.topics.links.bookmarked.title"); + } + + get text() { + return I18n.t("sidebar.sections.topics.links.bookmarked.content"); + } +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js new file mode 100644 index 00000000000..261364a76e4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js @@ -0,0 +1,87 @@ +import I18n from "I18n"; + +import { tracked } from "@glimmer/tracking"; + +import discourseDebounce from "discourse-common/lib/debounce"; +import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link"; + +export default class EverythingSectionLink extends BaseSectionLink { + @tracked totalUnread = 0; + @tracked totalNew = 0; + + constructor() { + super(...arguments); + + this._refreshCounts(); + + this.topicTrackingState.onStateChange( + this._topicTrackingStateUpdated.bind(this) + ); + } + + _topicTrackingStateUpdated() { + // refreshing section counts by looping through the states in topicTrackingState is an expensive operation so + // we debounce this. + discourseDebounce(this, this._refreshCounts, 100); + } + + _refreshCounts() { + let totalUnread = 0; + let totalNew = 0; + + this.topicTrackingState.forEachTracked((topic, isNew, isUnread) => { + if (isNew) { + totalNew += 1; + } else if (isUnread) { + totalUnread += 1; + } + }); + + this.totalUnread = totalUnread; + this.totalNew = totalNew; + } + + get name() { + return "everything"; + } + + get query() { + return { f: undefined }; + } + + get title() { + return I18n.t("sidebar.sections.topics.links.everything.title"); + } + + get text() { + return I18n.t("sidebar.sections.topics.links.everything.content"); + } + + get currentWhen() { + return "discovery.latest discovery.new discovery.unread discovery.top"; + } + + get badgeText() { + if (this.totalUnread > 0) { + return I18n.t("sidebar.unread_count", { + count: this.totalUnread, + }); + } else if (this.totalNew > 0) { + return I18n.t("sidebar.new_count", { + count: this.totalNew, + }); + } else { + return; + } + } + + get route() { + if (this.totalUnread > 0) { + return "discovery.unread"; + } else if (this.totalNew > 0) { + return "discovery.new"; + } else { + return "discovery.latest"; + } + } +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js new file mode 100644 index 00000000000..4822162fca0 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js @@ -0,0 +1,25 @@ +import I18n from "I18n"; + +import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link"; + +export default class TrackedSectionLink extends BaseSectionLink { + get name() { + return "tracked"; + } + + get route() { + return "discovery.latest"; + } + + get query() { + return { f: "tracked" }; + } + + get title() { + return I18n.t("sidebar.sections.topics.links.tracked.title"); + } + + get text() { + return I18n.t("sidebar.sections.topics.links.tracked.content"); + } +} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs index 70750f06eeb..049cf540b19 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs @@ -4,7 +4,7 @@ @route={{@route}} @query={{@query}} @models={{if @model (array @model) (if @models @models (array))}} - @current-when={{@current-when}} + @current-when={{@currentWhen}} @title={{@title}} > {{@content}} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs index 7d394723c53..1c31a57a701 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs @@ -8,26 +8,15 @@ @headerAction={{this.composeTopic}} @headerActionTitle={{i18n "sidebar.sections.topics.header_action_title"}}> - - - - - + {{#each this.sectionLinks as |sectionLink|}} + + {{/each}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js index f49f582d680..9bcfe629223 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js @@ -12,6 +12,7 @@ import { import { isLegacyEmber } from "discourse-common/config/environment"; import topicFixtures from "discourse/tests/fixtures/discovery-fixtures"; import { cloneJSON } from "discourse-common/lib/object"; +import { withPluginApi } from "discourse/lib/plugin-api"; acceptance("Sidebar - Topics Section", function (needs) { needs.user({ experimental_sidebar_enabled: true }); @@ -407,4 +408,57 @@ acceptance("Sidebar - Topics Section", function (needs) { ); } ); + + conditionalTest( + "adding section link via plugin API", + !isLegacyEmber(), + async function (assert) { + withPluginApi("1.2.0", (api) => { + api.addTopicsSectionLink((baseSectionLink) => { + return class CustomSectionLink extends baseSectionLink { + get name() { + return "user-summary"; + } + + get route() { + return "user.summary"; + } + + get model() { + return this.currentUser; + } + + get title() { + return `${this.currentUser.username} summary`; + } + + get text() { + return "my summary"; + } + }; + }); + }); + + await visit("/"); + await click(".sidebar-section-link-user-summary"); + + assert.strictEqual( + currentURL(), + "/u/eviltrout/summary", + "links to the right URL" + ); + + assert.strictEqual( + query(".sidebar-section-link-user-summary").textContent.trim(), + "my summary", + "displays the right text for the link" + ); + + assert.strictEqual( + query(".sidebar-section-link-user-summary").title, + "eviltrout summary", + "displays the right title for the link" + ); + } + ); }); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 4261b2992d5..daa06030ec3 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -63,6 +63,7 @@ import { setTestPresence, } from "discourse/lib/user-presence"; import PreloadStore from "discourse/lib/preload-store"; +import { resetDefaultSectionLinks as resetTopicsSectionLinks } from "discourse/lib/sidebar/custom-topics-section-links"; const LEGACY_ENV = !setupApplicationTest; @@ -186,6 +187,7 @@ function testCleanup(container, app) { clearPresenceCallbacks(); } restoreBaseUri(); + resetTopicsSectionLinks(); } export function discourseModule(name, options) {