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}}
+
+
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