FEATURE: allow user to set preferred sidebar list destination (#18594)

User can choose between latest or new/unread and that preference will affect behavior of sidebar links.
This commit is contained in:
Krzysztof Kotlarek 2022-10-18 13:21:52 +11:00 committed by GitHub
parent daa8aedccf
commit 243efa8931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 340 additions and 4 deletions

View File

@ -20,6 +20,7 @@ export default class SidebarCommonCategoriesSection extends Component {
new CategorySectionLink({
category,
topicTrackingState: this.topicTrackingState,
currentUser: this.currentUser,
})
);

View File

@ -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) => {

View File

@ -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";
}

View File

@ -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";
}
}

View File

@ -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",

View File

@ -12,6 +12,7 @@ export default RestrictedUserRoute.extend({
if (this.siteSettings.tagging_enabled) {
props.selectedSidebarTagNames = user.sidebarTagNames;
}
props.newSidebarListDestination = user.sidebarListDestination;
controller.setProperties(props);
},

View File

@ -35,4 +35,13 @@
</div>
{{/if}}
<div class="control-group preferences-sidebar-navigation">
<legend class="control-label">{{i18n "user.experimental_sidebar.navigation_section"}}</legend>
<div class="controls controls-dropdown">
<label>{{i18n "user.experimental_sidebar.list_destination_instruction"}}</label>
<ComboBox @valueProperty="value" @content={{this.sidebarListDestinations}} @value={{this.newSidebarListDestination}} @onChange={{action (mut this.newSidebarListDestination)}} />
</div>
</div>
<SaveControls @model={{this.model}} @action={{action "save"}} @saved={{this.saved}} />

View File

@ -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");

View File

@ -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");

View File

@ -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",
},
},
});
}

View File

@ -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
#

View File

@ -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

View File

@ -48,7 +48,8 @@ class UserUpdater
:timezone,
:skip_new_user_tips,
:seen_popups,
:default_calendar
:default_calendar,
:sidebar_list_destination
]
NOTIFICATION_SCHEDULE_ATTRS = -> {

View File

@ -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"

View File

@ -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:

View File

@ -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

View File

@ -767,6 +767,9 @@
},
"oldest_search_log_date": {
"type": ["string", "null"]
},
"sidebar_list_destination": {
"type": "string"
}
},
"required": [

View File

@ -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