diff --git a/app/assets/javascripts/discourse/app/components/sidebar/common/categories-section.js b/app/assets/javascripts/discourse/app/components/sidebar/common/categories-section.js index 9a9cd4e0816..b51c37c47b7 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/common/categories-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/common/categories-section.js @@ -20,6 +20,7 @@ export default class SidebarCommonCategoriesSection extends Component { new CategorySectionLink({ category, topicTrackingState: this.topicTrackingState, + currentUser: this.currentUser, }) ); diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/sidebar.js b/app/assets/javascripts/discourse/app/controllers/preferences/sidebar.js index 2b712f0290e..b4c1067d8f3 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/sidebar.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/sidebar.js @@ -1,14 +1,29 @@ import Controller from "@ember/controller"; import { action } from "@ember/object"; import { tracked } from "@glimmer/tracking"; +import I18n from "I18n"; import { popupAjaxError } from "discourse/lib/ajax-error"; +export const DEFAULT_LIST_DESTINATION = "default"; +export const UNREAD_LIST_DESTINATION = "unread_new"; + export default class extends Controller { @tracked saved = false; @tracked selectedSidebarCategories = []; @tracked selectedSidebarTagNames = []; + sidebarListDestinations = [ + { + name: I18n.t("user.experimental_sidebar.list_destination_default"), + value: DEFAULT_LIST_DESTINATION, + }, + { + name: I18n.t("user.experimental_sidebar.list_destination_unread_new"), + value: UNREAD_LIST_DESTINATION, + }, + ]; + @action save() { const initialSidebarCategoryIds = this.model.sidebarCategoryIds; @@ -20,6 +35,11 @@ export default class extends Controller { this.model.set("sidebar_tag_names", this.selectedSidebarTagNames); + this.model.set( + "user_option.sidebar_list_destination", + this.newSidebarListDestination + ); + this.model .save() .then((result) => { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js index 39873ea966f..197c2f96697 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/common/community-section/everything-section-link.js @@ -2,6 +2,7 @@ import I18n from "I18n"; import { tracked } from "@glimmer/tracking"; import BaseSectionLink from "discourse/lib/sidebar/base-community-section-link"; +import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar"; export default class EverythingSectionLink extends BaseSectionLink { @tracked totalUnread = 0; @@ -63,6 +64,14 @@ export default class EverythingSectionLink extends BaseSectionLink { } get route() { + if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) { + if (this.totalUnread > 0) { + return "discovery.unread"; + } + if (this.totalNew > 0) { + return "discovery.new"; + } + } return "discovery.latest"; } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js index 4cdd34bc946..5057dc399af 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js @@ -4,14 +4,16 @@ import { tracked } from "@glimmer/tracking"; import { bind } from "discourse-common/utils/decorators"; import Category from "discourse/models/category"; +import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar"; export default class CategorySectionLink { @tracked totalUnread = 0; @tracked totalNew = 0; - constructor({ category, topicTrackingState }) { + constructor({ category, topicTrackingState, currentUser }) { this.category = category; this.topicTrackingState = topicTrackingState; + this.currentUser = currentUser; this.refreshCounts(); } @@ -79,6 +81,14 @@ export default class CategorySectionLink { } get route() { + if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) { + if (this.totalUnread > 0) { + return "discovery.unreadCategory"; + } + if (this.totalNew > 0) { + return "discovery.newCategory"; + } + } return "discovery.category"; } } diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 70e1abd31f2..d80c75fc1f2 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -1,7 +1,15 @@ import EmberObject, { computed, get, getProperties } from "@ember/object"; import cookie, { removeCookie } from "discourse/lib/cookie"; import { defaultHomepage, escapeExpression } from "discourse/lib/utilities"; -import { alias, equal, filterBy, gt, mapBy, or } from "@ember/object/computed"; +import { + alias, + equal, + filterBy, + gt, + mapBy, + or, + readOnly, +} from "@ember/object/computed"; import getURL, { getURLWithCDN } from "discourse-common/lib/get-url"; import { A } from "@ember/array"; import Badge from "discourse/models/badge"; @@ -109,6 +117,7 @@ let userOptionFields = [ "seen_popups", "default_calendar", "bookmark_auto_delete_preference", + "sidebar_list_destination", ]; export function addSaveableUserOptionField(fieldName) { @@ -341,6 +350,8 @@ const User = RestModel.extend({ ); }, + sidebarListDestination: readOnly("user_option.sidebar_list_destination"), + changeUsername(new_username) { return ajax(userPath(`${this.username_lower}/preferences/username`), { type: "PUT", diff --git a/app/assets/javascripts/discourse/app/routes/preferences-sidebar.js b/app/assets/javascripts/discourse/app/routes/preferences-sidebar.js index 26e50bf84bf..7dbfb121ebb 100644 --- a/app/assets/javascripts/discourse/app/routes/preferences-sidebar.js +++ b/app/assets/javascripts/discourse/app/routes/preferences-sidebar.js @@ -12,6 +12,7 @@ export default RestrictedUserRoute.extend({ if (this.siteSettings.tagging_enabled) { props.selectedSidebarTagNames = user.sidebarTagNames; } + props.newSidebarListDestination = user.sidebarListDestination; controller.setProperties(props); }, diff --git a/app/assets/javascripts/discourse/app/templates/preferences/sidebar.hbs b/app/assets/javascripts/discourse/app/templates/preferences/sidebar.hbs index 61a6286787a..70270db4004 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences/sidebar.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences/sidebar.hbs @@ -35,4 +35,13 @@ {{/if}} +
+ {{i18n "user.experimental_sidebar.navigation_section"}} + +
+ + +
+
+ diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js index e1c07c679e2..d47e21dca7a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js @@ -15,6 +15,7 @@ import Site from "discourse/models/site"; import discoveryFixture from "discourse/tests/fixtures/discovery-fixtures"; import categoryFixture from "discourse/tests/fixtures/category-fixtures"; import { cloneJSON } from "discourse-common/lib/object"; +import { NotificationLevels } from "discourse/lib/notification-levels"; acceptance( "Sidebar - Logged on user - Categories Section - allow_uncategorized_topics disabled", @@ -292,6 +293,124 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) { ); }); + test("clicking section links - sidebar_list_destination set to unread/new and no unread or new topics", async function (assert) { + updateCurrentUser({ + user_option: { + sidebar_list_destination: "unread_new", + }, + }); + const { category1 } = setupUserSidebarCategories(); + + await visit("/"); + + await click(`.sidebar-section-link-${category1.slug}`); + + assert.strictEqual( + currentURL(), + `/c/${category1.slug}/${category1.id}`, + "it should transition to the category1 default view page" + ); + + assert.strictEqual( + count(".sidebar-section-categories .sidebar-section-link.active"), + 1, + "only one link is marked as active" + ); + + assert.ok( + exists(`.sidebar-section-link-${category1.slug}.active`), + "the category1 section link is marked as active" + ); + }); + + test("clicking section links - sidebar_list_destination set to unread/new with new topics", async function (assert) { + const { category1 } = setupUserSidebarCategories(); + const topicTrackingState = this.container.lookup( + "service:topic-tracking-state" + ); + topicTrackingState.states.set("t112", { + last_read_post_number: null, + id: 112, + notification_level: NotificationLevels.TRACKING, + category_id: category1.id, + created_in_new_period: true, + }); + updateCurrentUser({ + user_option: { + sidebar_list_destination: "unread_new", + }, + }); + + await visit("/"); + + await click(`.sidebar-section-link-${category1.slug}`); + + assert.strictEqual( + currentURL(), + `/c/${category1.slug}/${category1.id}/l/new`, + "it should transition to the category1 new page" + ); + + assert.strictEqual( + count(".sidebar-section-categories .sidebar-section-link.active"), + 1, + "only one link is marked as active" + ); + + assert.ok( + exists(`.sidebar-section-link-${category1.slug}.active`), + "the category1 section link is marked as active" + ); + }); + + test("clicking section links - sidebar_list_destination set to unread/new with new and unread topics", async function (assert) { + const { category1 } = setupUserSidebarCategories(); + const topicTrackingState = this.container.lookup( + "service:topic-tracking-state" + ); + topicTrackingState.states.set("t112", { + last_read_post_number: null, + id: 112, + notification_level: NotificationLevels.TRACKING, + category_id: category1.id, + created_in_new_period: true, + }); + topicTrackingState.states.set("t113", { + last_read_post_number: 1, + highest_post_number: 2, + id: 113, + notification_level: NotificationLevels.TRACKING, + category_id: category1.id, + created_in_new_period: true, + }); + updateCurrentUser({ + user_option: { + sidebar_list_destination: "unread_new", + }, + }); + + await visit("/"); + + await click(`.sidebar-section-link-${category1.slug}`); + + assert.strictEqual( + currentURL(), + `/c/${category1.slug}/${category1.id}/l/unread`, + "it should transition to the category1 unread page" + ); + + assert.strictEqual( + count(".sidebar-section-categories .sidebar-section-link.active"), + 1, + "only one link is marked as active" + ); + + assert.ok( + exists(`.sidebar-section-link-${category1.slug}.active`), + "the category1 section link is marked as active" + ); + }); + test("category section link for category with 3-digit hex code for color", async function (assert) { const { category1 } = setupUserSidebarCategories(); category1.set("color", "888"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js index 4230f0ed932..c8fda4f2152 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js @@ -160,6 +160,121 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { ); }); + test("clicking on everything link - sidebar_list_destination set to unread/new and no unread or new topics", async function (assert) { + updateCurrentUser({ + user_option: { + sidebar_list_destination: "unread_new", + }, + }); + + await visit("/t/280"); + await click(".sidebar-section-community .sidebar-section-link-everything"); + assert.strictEqual( + currentURL(), + "/latest", + "it should transition to the latest page" + ); + + assert.strictEqual( + count(".sidebar-section-community .sidebar-section-link.active"), + 1, + "only one link is marked as active" + ); + + assert.ok( + exists( + ".sidebar-section-community .sidebar-section-link-everything.active" + ), + "the everything link is marked as active" + ); + }); + + test("clicking on everything link - sidebar_list_destination set to unread/new with new topics", async function (assert) { + const topicTrackingState = this.container.lookup( + "service:topic-tracking-state" + ); + topicTrackingState.states.set("t112", { + last_read_post_number: null, + id: 112, + notification_level: NotificationLevels.TRACKING, + category_id: 2, + created_in_new_period: true, + }); + updateCurrentUser({ + user_option: { + sidebar_list_destination: "unread_new", + }, + }); + await visit("/t/280"); + await click(".sidebar-section-community .sidebar-section-link-everything"); + + assert.strictEqual( + currentURL(), + "/new", + "it should transition to the new page" + ); + + assert.strictEqual( + count(".sidebar-section-community .sidebar-section-link.active"), + 1, + "only one link is marked as active" + ); + + assert.ok( + exists( + ".sidebar-section-community .sidebar-section-link-everything.active" + ), + "the everything link is marked as active" + ); + }); + + test("clicking on everything link - sidebar_list_destination set to unread/new with new and unread topics", async function (assert) { + const topicTrackingState = this.container.lookup( + "service:topic-tracking-state" + ); + topicTrackingState.states.set("t112", { + last_read_post_number: null, + id: 112, + notification_level: NotificationLevels.TRACKING, + category_id: 2, + created_in_new_period: true, + }); + topicTrackingState.states.set("t113", { + last_read_post_number: 1, + highest_post_number: 2, + id: 113, + notification_level: NotificationLevels.TRACKING, + category_id: 2, + created_in_new_period: true, + }); + updateCurrentUser({ + user_option: { + sidebar_list_destination: "unread_new", + }, + }); + await visit("/t/280"); + await click(".sidebar-section-community .sidebar-section-link-everything"); + + assert.strictEqual( + currentURL(), + "/unread", + "it should transition to the unread page" + ); + + assert.strictEqual( + count(".sidebar-section-community .sidebar-section-link.active"), + 1, + "only one link is marked as active" + ); + + assert.ok( + exists( + ".sidebar-section-community .sidebar-section-link-everything.active" + ), + "the everything link is marked as active" + ); + }); + test("clicking on tracked link", async function (assert) { await visit("/t/280"); await click(".sidebar-section-community .sidebar-section-link-tracked"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js index 023a119a262..a78df47e32e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js @@ -46,6 +46,9 @@ acceptance("User Preferences - Sidebar", function (needs) { { name: "monkey", pm_only: false }, { name: "gazelle", pm_only: false }, ], + user_option: { + sidebar_list_destination: "unread_new", + }, }, }); } diff --git a/app/models/user_option.rb b/app/models/user_option.rb index ac724dbfe66..8add4161d91 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -14,6 +14,7 @@ class UserOption < ActiveRecord::Base scope :human_users, -> { where('user_id > 0') } enum default_calendar: { none_selected: 0, ics: 1, google: 2 }, _scopes: false + enum sidebar_list_destination: { none_selected: 0, default: 0, unread_new: 1 }, _prefix: "sidebar_list" def self.ensure_consistency! sql = <<~SQL @@ -269,6 +270,7 @@ end # bookmark_auto_delete_preference :integer default(3), not null # enable_experimental_sidebar :boolean default(FALSE) # seen_popups :integer is an Array +# sidebar_list_destination :integer default("none_selected"), not null # # Indexes # diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index 19e7c794b0b..582e00b7f23 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -34,7 +34,8 @@ class UserOptionSerializer < ApplicationSerializer :timezone, :skip_new_user_tips, :default_calendar, - :oldest_search_log_date + :oldest_search_log_date, + :sidebar_list_destination def auto_track_topics_after_msecs object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs @@ -51,4 +52,8 @@ class UserOptionSerializer < ApplicationSerializer def theme_ids object.theme_ids.presence || [SiteSetting.default_theme_id] end + + def sidebar_list_destination + object.sidebar_list_none_selected? ? SiteSetting.default_sidebar_list_destination : object.sidebar_list_destination + end end diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 31feebbf91d..ad7271c0678 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -48,7 +48,8 @@ class UserUpdater :timezone, :skip_new_user_tips, :seen_popups, - :default_calendar + :default_calendar, + :sidebar_list_destination ] NOTIFICATION_SCHEDULE_ATTRS = -> { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8b97e26c1d1..7bcc8992ddb 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1183,6 +1183,10 @@ en: categories_section_instruction: "Selected categories will be displayed under Sidebar's categories section." tags_section: "Tags Section" tags_section_instruction: "Selected tags will be displayed under Sidebar's tags section." + navigation_section: "Navigation" + list_destination_instruction: "When I click a topic list link in the sidebar with new or unread topics, take me to" + list_destination_default: "Default" + list_destination_unread_new: "New/Unread" change: "change" featured_topic: "Featured Topic" moderator: "%{user} is a moderator" diff --git a/config/site_settings.yml b/config/site_settings.yml index 0ea994e947c..e7bca7d67d5 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2056,6 +2056,13 @@ sidebar: type: tag_list default: "" client: true + default_sidebar_list_destination: + hidden: true + default: "default" + type: "list" + choices: + - "default" + - "unread_new" embedding: embed_by_username: diff --git a/db/migrate/20221013045158_add_sidebar_list_destination_to_user_option.rb b/db/migrate/20221013045158_add_sidebar_list_destination_to_user_option.rb new file mode 100644 index 00000000000..8e6a64ede1c --- /dev/null +++ b/db/migrate/20221013045158_add_sidebar_list_destination_to_user_option.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSidebarListDestinationToUserOption < ActiveRecord::Migration[7.0] + def change + add_column :user_options, :sidebar_list_destination, :integer, default: 0, null: false + end +end diff --git a/spec/requests/api/schemas/json/user_get_response.json b/spec/requests/api/schemas/json/user_get_response.json index 3ccc50d8df1..d669aa5cb44 100644 --- a/spec/requests/api/schemas/json/user_get_response.json +++ b/spec/requests/api/schemas/json/user_get_response.json @@ -767,6 +767,9 @@ }, "oldest_search_log_date": { "type": ["string", "null"] + }, + "sidebar_list_destination": { + "type": "string" } }, "required": [ diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index b5abe360d24..dc82b84251c 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -426,4 +426,13 @@ RSpec.describe UserSerializer do end include_examples "#display_sidebar_tags", UserSerializer + + describe "#sidebar_list_destination" do + it "returns choosen value or default" do + expect(serializer.as_json[:user_option][:sidebar_list_destination]).to eq(SiteSetting.default_sidebar_list_destination) + + user.user_option.update!(sidebar_list_destination: "unread_new") + expect(serializer.as_json[:user_option][:sidebar_list_destination]).to eq("unread_new") + end + end end