FEATURE: New topics vs replies toggle for the new new view (#22920)

This PR adds a new toggle to switch the (new) /new list between showing topics with new replies (a.k.a unread topics), new topics, or everything mixed together.
This commit is contained in:
Osama Sayegh 2023-08-18 07:44:04 +03:00 committed by GitHub
parent 79e3d4e2bd
commit 09d3709ec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1009 additions and 18 deletions

View File

@ -0,0 +1,13 @@
<div
class="topic-replies-toggle-wrapper"
{{on "click" (action this.click)}}
{{! template-lint-disable no-invalid-interactive }}
>
{{raw
"list/new-list-header-controls"
current=@current
newRepliesCount=@newRepliesCount
newTopicsCount=@newTopicsCount
noStaticLabel=true
}}
</div>

View File

@ -0,0 +1,14 @@
import Component from "@glimmer/component";
export default class NewListHeaderControlsWrapper extends Component {
click(e) {
const target = e.target;
if (target.closest("button.topics-replies-toggle.all")) {
this.args.changeNewListSubset(null);
} else if (target.closest("button.topics-replies-toggle.topics")) {
this.args.changeNewListSubset("topics");
} else if (target.closest("button.topics-replies-toggle.replies")) {
this.args.changeNewListSubset("replies");
}
}
}

View File

@ -13,6 +13,10 @@
listTitle=this.listTitle
bulkSelectEnabled=this.bulkSelectEnabled
canDoBulkActions=this.canDoBulkActions
showTopicsAndRepliesToggle=this.showTopicsAndRepliesToggle
newListSubset=this.newListSubset
newRepliesCount=this.newRepliesCount
newTopicsCount=this.newTopicsCount
}}
</thead>

View File

@ -184,6 +184,17 @@ export default Component.extend(LoadMore, {
},
});
});
onClick("button.topics-replies-toggle", (element) => {
if (element.classList.contains("all")) {
this.changeNewListSubset(null);
} else if (element.classList.contains("topics")) {
this.changeNewListSubset("topics");
} else if (element.classList.contains("replies")) {
this.changeNewListSubset("replies");
}
this.rerender();
});
},
keyDown(e) {

View File

@ -13,6 +13,7 @@ export const queryParams = {
before: { replace: true, refreshModel: true },
bumped_before: { replace: true, refreshModel: true },
f: { replace: true, refreshModel: true },
subset: { replace: true, refreshModel: true },
period: { replace: true, refreshModel: true },
topic_ids: { replace: true, refreshModel: true },
group_name: { replace: true, refreshModel: true },
@ -35,6 +36,13 @@ export function changeSort(sortBy) {
}
}
export function changeNewListSubset(subset) {
this.controller.set("subset", subset);
let model = this.controllerFor("discovery.topics").model;
model.updateNewListSubsetParam(subset);
}
export function resetParams(skipParams = []) {
Object.keys(queryParams).forEach((p) => {
if (!skipParams.includes(p)) {

View File

@ -1,6 +1,6 @@
import { inject as controller } from "@ember/controller";
import { inject as service } from "@ember/service";
import { alias, empty, equal, gt, not, readOnly } from "@ember/object/computed";
import { alias, empty, equal, gt, readOnly } from "@ember/object/computed";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import DismissTopics from "discourse/mixins/dismiss-topics";
import DiscoveryController from "discourse/controllers/discovery";
@ -27,7 +27,6 @@ export default class TopicsController extends DiscoveryController.extend(
expandAllPinned = false;
@alias("currentUser.id") canStar;
@not("new") showTopicPostBadges;
@alias("currentUser.user_option.redirected_to_top.reason") redirectedReason;
@readOnly("model.params.order") order;
@readOnly("model.params.ascending") ascending;
@ -119,7 +118,41 @@ export default class TopicsController extends DiscoveryController.extend(
@discourseComputed("model.filter")
new(filter) {
return filter?.endsWith("new") && !this.currentUser?.new_new_view_enabled;
return filter?.endsWith("new");
}
@discourseComputed("new")
showTopicsAndRepliesToggle(isNew) {
return isNew && this.currentUser?.new_new_view_enabled;
}
@discourseComputed("topicTrackingState.messageCount")
newRepliesCount() {
if (this.currentUser?.new_new_view_enabled) {
return this.topicTrackingState.countUnread({
categoryId: this.category?.id,
noSubcategories: this.noSubcategories,
});
} else {
return 0;
}
}
@discourseComputed("topicTrackingState.messageCount")
newTopicsCount() {
if (this.currentUser?.new_new_view_enabled) {
return this.topicTrackingState.countNew({
categoryId: this.category?.id,
noSubcategories: this.noSubcategories,
});
} else {
return 0;
}
}
@discourseComputed("new")
showTopicPostBadges(isNew) {
return !isNew || this.currentUser?.new_new_view_enabled;
}
@discourseComputed("allLoaded", "model.topics.length")

View File

@ -101,9 +101,45 @@ export default class TagShowController extends DiscoverySortableController.exten
return this._isFilterPage(filter, "unread") && topicsLength > 0;
}
@discourseComputed("list.filter", "list.topics.length")
showResetNew(filter, topicsLength) {
return this._isFilterPage(filter, "new") && topicsLength > 0;
@discourseComputed("list.filter")
new(filter) {
return this._isFilterPage(filter, "new");
}
@discourseComputed("new")
showTopicsAndRepliesToggle(isNew) {
return isNew && this.currentUser?.new_new_view_enabled;
}
@discourseComputed("topicTrackingState.messageCount")
newRepliesCount() {
if (this.currentUser?.new_new_view_enabled) {
return this.topicTrackingState.countUnread({
categoryId: this.category?.id,
noSubcategories: this.noSubcategories,
tagId: this.tag?.id,
});
} else {
return 0;
}
}
@discourseComputed("topicTrackingState.messageCount")
newTopicsCount() {
if (this.currentUser?.new_new_view_enabled) {
return this.topicTrackingState.countNew({
categoryId: this.category?.id,
noSubcategories: this.noSubcategories,
tagId: this.tag?.id,
});
} else {
return 0;
}
}
@discourseComputed("new", "list.topics.length")
showResetNew(isNew, topicsLength) {
return isNew && topicsLength > 0;
}
callResetNew(dismissPosts = false, dismissTopics = false, untrack = false) {
@ -148,6 +184,11 @@ export default class TagShowController extends DiscoverySortableController.exten
}
}
@action
changeNewListSubset(subset) {
this.set("subset", subset);
}
@action
changePeriod(p) {
this.set("period", p);

View File

@ -67,6 +67,18 @@ const TopicList = RestModel.extend({
this.set("params", params);
},
updateNewListSubsetParam(subset) {
let params = Object.assign({}, this.params || {});
if (params.q) {
params = { q: params.q };
} else {
params.subset = subset;
}
this.set("params", params);
},
loadMore() {
if (this.loadingMore) {
return Promise.resolve();

View File

@ -94,45 +94,45 @@ const TopicTrackingState = EmberObject.extend({
this.messageBus.subscribe(
"/latest",
this._processChannelPayload,
meta["/latest"] || messageBusDefaultNewMessageId
meta["/latest"] ?? messageBusDefaultNewMessageId
);
if (this.currentUser) {
this.messageBus.subscribe(
"/new",
this._processChannelPayload,
meta["/new"] || messageBusDefaultNewMessageId
meta["/new"] ?? messageBusDefaultNewMessageId
);
this.messageBus.subscribe(
`/unread`,
this._processChannelPayload,
meta["/unread"] || messageBusDefaultNewMessageId
meta["/unread"] ?? messageBusDefaultNewMessageId
);
this.messageBus.subscribe(
`/unread/${this.currentUser.id}`,
this._processChannelPayload,
meta[`/unread/${this.currentUser.id}`] || messageBusDefaultNewMessageId
meta[`/unread/${this.currentUser.id}`] ?? messageBusDefaultNewMessageId
);
}
this.messageBus.subscribe(
"/delete",
this.onDeleteMessage,
meta["/delete"] || messageBusDefaultNewMessageId
meta["/delete"] ?? messageBusDefaultNewMessageId
);
this.messageBus.subscribe(
"/recover",
this.onRecoverMessage,
meta["/recover"] || messageBusDefaultNewMessageId
meta["/recover"] ?? messageBusDefaultNewMessageId
);
this.messageBus.subscribe(
"/destroy",
this.onDestroyMessage,
meta["/destroy"] || messageBusDefaultNewMessageId
meta["/destroy"] ?? messageBusDefaultNewMessageId
);
},

View File

@ -0,0 +1,13 @@
{{#if view.staticLabel}}
<span class="static-label">{{view.staticLabel}}</span>
{{else}}
<button class="topics-replies-toggle all{{#if view.allActive}} active{{/if}}">
{{view.allButtonLabel}}
</button>
<button class="topics-replies-toggle topics{{#if view.topicsActive}} active{{/if}}">
{{view.topicsButtonLabel}}
</button>
<button class="topics-replies-toggle replies{{#if view.repliesActive}} active{{/if}}">
{{view.repliesButtonLabel}}
</button>
{{/if}}

View File

@ -13,7 +13,11 @@
</span>
{{/if ~}}
{{/if ~}}
<span>{{view.localizedName}}</span>
{{~#if view.showTopicsAndRepliesToggle}}
{{raw "list/new-list-header-controls" current=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount}}
{{else}}
<span>{{view.localizedName}}</span>
{{/if ~}}
{{~#if view.isSorting}}
{{d-icon view.sortIcon}}
{{/if ~}}

View File

@ -6,7 +6,7 @@
{{/if}}
</th>
{{/if}}
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions}}
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions showTopicsAndRepliesToggle=showTopicsAndRepliesToggle newListSubset=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount}}
{{raw-plugin-outlet name="topic-list-header-after-main-link"}}
{{#if showPosters}}
{{raw "topic-list-header-column" order='posters' ariaLabel=(i18n "category.sort_options.posters")}}

View File

@ -0,0 +1,67 @@
import EmberObject from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default EmberObject.extend({
@discourseComputed
topicsActive() {
return this.current === "topics";
},
@discourseComputed
repliesActive() {
return this.current === "replies";
},
@discourseComputed
allActive() {
return !this.topicsActive && !this.repliesActive;
},
@discourseComputed
allButtonLabel() {
const count = this.newRepliesCount + this.newTopicsCount;
if (count > 0) {
return I18n.t("filters.new.all_with_count", { count });
} else {
return I18n.t("filters.new.all");
}
},
@discourseComputed
repliesButtonLabel() {
if (this.newRepliesCount > 0) {
return I18n.t("filters.new.replies_with_count", {
count: this.newRepliesCount,
});
} else {
return I18n.t("filters.new.replies");
}
},
@discourseComputed
topicsButtonLabel() {
if (this.newTopicsCount > 0) {
return I18n.t("filters.new.topics_with_count", {
count: this.newTopicsCount,
});
} else {
return I18n.t("filters.new.topics");
}
},
@discourseComputed
staticLabel() {
if (this.noStaticLabel) {
return null;
}
if (this.newTopicsCount > 0 && this.newRepliesCount > 0) {
return null;
}
if (this.newTopicsCount > 0) {
return this.topicsButtonLabel;
} else {
return this.repliesButtonLabel;
}
},
});

View File

@ -1,6 +1,7 @@
import { inject as service } from "@ember/service";
import { Promise, all } from "rsvp";
import {
changeNewListSubset,
changeSort,
queryParams,
resetParams,
@ -218,6 +219,11 @@ class AbstractCategoryRoute extends DiscourseRoute {
changeSort.call(this, sortBy);
}
@action
changeNewListSubset(subset) {
changeNewListSubset.call(this, subset);
}
@action
resetParams(skipParams = []) {
resetParams.call(this, skipParams);

View File

@ -1,4 +1,5 @@
import {
changeNewListSubset,
changeSort,
queryParams,
resetParams,
@ -165,6 +166,11 @@ class AbstractTopicRoute extends DiscourseRoute {
changeSort.call(this, sortBy);
}
@action
changeNewListSubset(subset) {
changeNewListSubset.call(this, subset);
}
@action
resetParams(skipParams = []) {
resetParams.call(this, skipParams);

View File

@ -37,8 +37,11 @@ export default class DiscoveryFilterRoute extends DiscourseRoute {
});
}
// TODO(tgxworld): This action is required by the `discovery/topics` controller which is not necessary for this route.
// TODO(tgxworld): The following 2 actions are required by the `discovery/topics` controller which is not necessary for this route.
// Figure out a way to remove this.
@action
changeSort() {}
@action
changeNewListSubset() {}
}

View File

@ -94,9 +94,13 @@
@topics={{this.model.topics}}
@discoveryList={{true}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{this.model.params.subset}}
@changeNewListSubset={{route-action "changeNewListSubset"}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
/>
{{/if}}
<span>
<PluginOutlet
@name="after-topic-list"

View File

@ -40,6 +40,14 @@
{{/if}}
{{/if}}
{{#if this.showTopicsAndRepliesToggle}}
<NewListHeaderControlsWrapper
@current={{this.model.params.subset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
@changeNewListSubset={{route-action "changeNewListSubset"}}
/>
{{/if}}
{{#if this.hasTopics}}
<TopicList
@ascending={{this.ascending}}

View File

@ -127,6 +127,14 @@
{{/if}}
{{/if}}
{{#if (and this.showTopicsAndRepliesToggle this.site.mobileView)}}
<NewListHeaderControlsWrapper
@current={{this.subset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
@changeNewListSubset={{action "changeNewListSubset"}}
/>
{{/if}}
{{#if this.list.topics}}
<TopicList
@topics={{this.list.topics}}
@ -144,6 +152,11 @@
@ascending={{this.ascending}}
@changeSort={{action "changeSort"}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{this.subset}}
@changeNewListSubset={{action "changeNewListSubset"}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
/>
{{/if}}
</DiscoveryTopicsList>

View File

@ -32,6 +32,9 @@
&:last-of-type {
padding-right: 10px;
}
th & {
border-bottom: 3px solid var(--primary-low);
}
}
button.bulk-select {
@ -52,6 +55,18 @@
}
}
.topics-replies-toggle {
background: none;
border: none;
line-height: var(--line-height-large);
min-height: 30px;
&.active {
background: var(--quaternary);
color: var(--secondary);
}
}
.badge-notification {
position: relative;
top: -2px;

View File

@ -569,3 +569,22 @@ td .main-link {
align-items: center;
}
}
.topic-replies-toggle-wrapper {
width: 100%;
display: flex;
justify-content: center;
.topics-replies-toggle {
flex-grow: 1;
background: none;
border: none;
padding: 0.75em;
box-shadow: 0 3px 0 var(--primary-low);
&.active {
color: var(--quaternary);
box-shadow: 0 3px 0 var(--quaternary);
}
}
}

View File

@ -3995,6 +3995,12 @@ en:
one: "New (%{count})"
other: "New (%{count})"
help: "topics created in the last few days"
all: "All"
all_with_count: "All (%{count})"
topics: "Topics"
topics_with_count: "Topics (%{count})"
replies: "Replies"
replies_with_count: "Replies (%{count})"
posted:
title: "My Posts"
help: "topics you have posted in"

View File

@ -53,6 +53,7 @@ class TopicQuery
search
q
f
subset
group_name
tags
match_all_tags
@ -304,7 +305,16 @@ class TopicQuery
def list_new
if @user&.new_new_view_enabled?
create_list(:new, { unordered: true }, new_and_unread_results)
list =
case @options[:subset]
when "topics"
new_results
when "replies"
unread_results
else
new_and_unread_results
end
create_list(:new, { unordered: true }, list)
else
create_list(:new, { unordered: true }, new_results)
end

View File

@ -1410,4 +1410,150 @@ RSpec.describe ListController do
end
end
end
describe "#new" do
def extract_topic_ids(response)
response.parsed_body["topic_list"]["topics"].map { |topics| topics["id"] }
end
def make_topic_with_unread_replies(topic, user)
TopicUser.change(
user.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
Fabricate(:post, topic: topic)
topic
end
def make_topic_read(topic, user)
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
topic
end
context "when the user is part of the `experimental_new_new_view_groups` site setting group" do
fab!(:category) { Fabricate(:category) }
fab!(:tag) { Fabricate(:tag) }
fab!(:new_reply) { make_topic_with_unread_replies(Fabricate(:post).topic, user) }
fab!(:new_topic) { Fabricate(:post).topic }
fab!(:old_topic) { make_topic_read(Fabricate(:post).topic, user) }
fab!(:new_reply_in_category) do
make_topic_with_unread_replies(
Fabricate(:post, topic: Fabricate(:topic, category: category)).topic,
user,
)
end
fab!(:new_topic_in_category) do
Fabricate(:post, topic: Fabricate(:topic, category: category)).topic
end
fab!(:old_topic_in_category) do
make_topic_read(Fabricate(:post, topic: Fabricate(:topic, category: category)).topic, user)
end
fab!(:new_reply_with_tag) do
make_topic_with_unread_replies(
Fabricate(:post, topic: Fabricate(:topic, tags: [tag])).topic,
user,
)
end
fab!(:new_topic_with_tag) { Fabricate(:post, topic: Fabricate(:topic, tags: [tag])).topic }
fab!(:old_topic_with_tag) do
make_topic_read(Fabricate(:post, topic: Fabricate(:topic, tags: [tag])).topic, user)
end
before do
make_topic_read(topic, user)
SiteSetting.experimental_new_new_view_groups = group.name
group.add(user)
sign_in(user)
end
it "returns new topics and topics with new replies" do
get "/new.json"
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(
new_reply.id,
new_topic.id,
new_reply_in_category.id,
new_topic_in_category.id,
new_reply_with_tag.id,
new_topic_with_tag.id,
)
end
context "when the subset param is set to topics" do
it "returns only new topics" do
get "/new.json", params: { subset: "topics" }
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(
new_topic.id,
new_topic_in_category.id,
new_topic_with_tag.id,
)
end
end
context "when the subset param is set to replies" do
it "returns only topics with new replies" do
get "/new.json", params: { subset: "replies" }
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(
new_reply.id,
new_reply_in_category.id,
new_reply_with_tag.id,
)
end
end
context "when filtering the list to a specific category" do
it "returns new topics in that category" do
get "/c/#{category.slug}/#{category.id}/l/new.json"
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(new_topic_in_category.id, new_reply_in_category.id)
end
it "respects the subset param" do
get "/c/#{category.slug}/#{category.id}/l/new.json", params: { subset: "topics" }
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(new_topic_in_category.id)
get "/c/#{category.slug}/#{category.id}/l/new.json", params: { subset: "replies" }
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(new_reply_in_category.id)
end
end
context "when filtering the list to topics with a specific tag" do
it "returns new topics with the specified tag" do
get "/tag/#{tag.name}/l/new.json"
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(new_topic_with_tag.id, new_reply_with_tag.id)
end
it "respects the subset param" do
get "/tag/#{tag.name}/l/new.json", params: { subset: "topics" }
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(new_topic_with_tag.id)
get "/tag/#{tag.name}/l/new.json", params: { subset: "replies" }
ids = extract_topic_ids(response)
expect(ids).to contain_exactly(new_reply_with_tag.id)
end
end
end
end
end

View File

@ -0,0 +1,462 @@
# frozen_string_literal: true
describe "New topic list", type: :system do
fab!(:user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group, users: [user]) }
fab!(:category) { Fabricate(:category) }
fab!(:tag) { Fabricate(:tag) }
fab!(:new_reply) do
Fabricate(:post).topic.tap do |topic|
TopicUser.change(
user.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
Fabricate(:post, topic: topic)
end
end
fab!(:new_topic) { Fabricate(:post).topic }
fab!(:old_topic) do
Fabricate(:post).topic.tap { |topic| TopicUser.update_last_read(user, topic.id, 1, 1, 1) }
end
fab!(:new_reply_in_category) do
Fabricate(:post, topic: Fabricate(:topic, category: category)).topic.tap do |topic|
TopicUser.change(
user.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
Fabricate(:post, topic: topic)
end
end
fab!(:new_topic_in_category) do
Fabricate(:post, topic: Fabricate(:topic, category: category)).topic
end
fab!(:old_topic_in_category) do
Fabricate(:post, topic: Fabricate(:topic, category: category)).topic.tap do |topic|
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
end
end
fab!(:new_reply_with_tag) do
Fabricate(:post, topic: Fabricate(:topic, tags: [tag])).topic.tap do |topic|
TopicUser.change(
user.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
Fabricate(:post, topic: topic)
end
end
fab!(:new_topic_with_tag) { Fabricate(:post, topic: Fabricate(:topic, tags: [tag])).topic }
fab!(:old_topic_with_tag) do
Fabricate(:post, topic: Fabricate(:topic, tags: [tag])).topic.tap do |topic|
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
end
end
let(:topic_list) { PageObjects::Components::TopicList.new }
let(:tabs_toggle) { PageObjects::Components::NewTopicListToggle.new }
before { sign_in(user) }
shared_examples "new list new topics and replies toggle" do
context "when the new new view is enabled" do
before { SiteSetting.experimental_new_new_view_groups = group.name }
it "shows all new topics and replies by default" do
visit("/new")
expect(topic_list).to have_topics(count: 6)
[
new_reply,
new_topic,
new_reply_in_category,
new_topic_in_category,
new_reply_with_tag,
new_topic_with_tag,
].each { |topic| expect(topic_list).to have_topic(topic) }
expect(tabs_toggle.all_tab).to have_count(6)
expect(tabs_toggle.replies_tab).to have_count(3)
expect(tabs_toggle.topics_tab).to have_count(3)
expect(tabs_toggle.all_tab).to be_active
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_inactive
end
it "respects the subset query param and activates the appropriate tab" do
visit("/new?subset=topics")
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_active
visit("/new?subset=replies")
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(tabs_toggle.topics_tab).to be_inactive
end
it "shows only new topics when the user switches to the Topics tab" do
visit("/new")
tabs_toggle.topics_tab.click
expect(topic_list).to have_topics(count: 3)
[new_topic, new_topic_in_category, new_topic_with_tag].each do |topic|
expect(topic_list).to have_topic(topic)
end
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_active
expect(tabs_toggle.all_tab).to have_count(6)
expect(tabs_toggle.replies_tab).to have_count(3)
expect(tabs_toggle.topics_tab).to have_count(3)
expect(page).to have_current_path("/new?subset=topics")
end
it "shows only topics with new replies when the user switches to the Replies tab" do
visit("/new")
tabs_toggle.replies_tab.click
expect(topic_list).to have_topics(count: 3)
[new_reply, new_reply_in_category, new_reply_with_tag].each do |topic|
expect(topic_list).to have_topic(topic)
end
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(tabs_toggle.topics_tab).to be_inactive
expect(tabs_toggle.all_tab).to have_count(6)
expect(tabs_toggle.replies_tab).to have_count(3)
expect(tabs_toggle.topics_tab).to have_count(3)
expect(page).to have_current_path("/new?subset=replies")
end
it "strips out the subset query params when switching back to the All tab from any of the other tabs" do
visit("/new")
tabs_toggle.replies_tab.click
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(page).to have_current_path("/new?subset=replies")
tabs_toggle.all_tab.click
expect(tabs_toggle.all_tab).to be_active
expect(tabs_toggle.replies_tab).to be_inactive
expect(page).to have_current_path("/new")
end
it "live-updates the counts shown on the tabs" do
visit("/new")
expect(tabs_toggle.all_tab).to have_count(6)
expect(tabs_toggle.replies_tab).to have_count(3)
expect(tabs_toggle.topics_tab).to have_count(3)
TopicUser.update_last_read(user, new_reply_in_category.id, 2, 1, 1)
expect(tabs_toggle.all_tab).to have_count(5)
expect(tabs_toggle.replies_tab).to have_count(2)
expect(tabs_toggle.topics_tab).to have_count(3)
TopicUser.update_last_read(user, new_topic.id, 1, 1, 1)
expect(tabs_toggle.all_tab).to have_count(4)
expect(tabs_toggle.replies_tab).to have_count(2)
expect(tabs_toggle.topics_tab).to have_count(2)
end
context "when the /new topic list is scoped to a category" do
it "shows new topics and replies in the category" do
visit("/c/#{category.slug}/#{category.id}/l/new")
expect(topic_list).to have_topics(count: 2)
expect(topic_list).to have_topic(new_reply_in_category)
expect(topic_list).to have_topic(new_topic_in_category)
expect(tabs_toggle.all_tab).to be_active
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_inactive
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
end
it "shows only new topics in the category when the user switches to the Topics tab" do
visit("/c/#{category.slug}/#{category.id}/l/new")
tabs_toggle.topics_tab.click
expect(topic_list).to have_topics(count: 1)
expect(topic_list).to have_topic(new_topic_in_category)
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_active
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
expect(page).to have_current_path(
"/c/#{category.slug}/#{category.id}/l/new?subset=topics",
)
end
it "shows only topics with new replies in the category when the user switches to the Replies tab" do
visit("/c/#{category.slug}/#{category.id}/l/new")
tabs_toggle.replies_tab.click
expect(topic_list).to have_topics(count: 1)
expect(topic_list).to have_topic(new_reply_in_category)
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(tabs_toggle.topics_tab).to be_inactive
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
expect(page).to have_current_path(
"/c/#{category.slug}/#{category.id}/l/new?subset=replies",
)
end
it "respects the subset query param and activates the appropriate tab" do
visit("/c/#{category.slug}/#{category.id}/l/new?subset=topics")
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_active
visit("/c/#{category.slug}/#{category.id}/l/new?subset=replies")
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(tabs_toggle.topics_tab).to be_inactive
end
it "live-updates the counts shown on the tabs" do
Fabricate(:post, topic: Fabricate(:topic, category: category))
visit("/c/#{category.slug}/#{category.id}/l/new")
expect(tabs_toggle.all_tab).to have_count(3)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(2)
TopicUser.update_last_read(user, new_topic_in_category.id, 1, 1, 1)
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
end
end
context "when the /new topic list is scoped to a tag" do
it "shows new topics and replies with the tag" do
visit("/tag/#{tag.name}/l/new")
expect(topic_list).to have_topics(count: 2)
[new_reply_with_tag, new_topic_with_tag].each do |topic|
expect(topic_list).to have_topic(topic)
end
expect(tabs_toggle.all_tab).to be_active
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_inactive
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
end
it "shows only new topics with the tag when the user switches to the Topics tab" do
visit("/tag/#{tag.name}/l/new")
tabs_toggle.topics_tab.click
expect(topic_list).to have_topics(count: 1)
expect(topic_list).to have_topic(new_topic_with_tag)
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_active
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
expect(page).to have_current_path("/tag/#{tag.name}/l/new?subset=topics")
end
it "shows only topics with new replies with the tag when the user switches to the Replies tab" do
visit("/tag/#{tag.name}/l/new")
tabs_toggle.replies_tab.click
expect(topic_list).to have_topics(count: 1)
expect(topic_list).to have_topic(new_reply_with_tag)
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(tabs_toggle.topics_tab).to be_inactive
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
expect(page).to have_current_path("/tag/#{tag.name}/l/new?subset=replies")
end
it "respects the subset query param and activates the appropriate tab" do
visit("/tag/#{tag.name}/l/new?subset=topics")
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_inactive
expect(tabs_toggle.topics_tab).to be_active
visit("/tag/#{tag.name}/l/new?subset=replies")
expect(tabs_toggle.all_tab).to be_inactive
expect(tabs_toggle.replies_tab).to be_active
expect(tabs_toggle.topics_tab).to be_inactive
end
it "live-updates the counts shown on the tabs" do
Fabricate(:post, topic: Fabricate(:topic, tags: [tag]))
visit("/tag/#{tag.name}/l/new")
expect(tabs_toggle.all_tab).to have_count(3)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(2)
TopicUser.update_last_read(user, new_topic_with_tag.id, 1, 1, 1)
expect(tabs_toggle.all_tab).to have_count(2)
expect(tabs_toggle.replies_tab).to have_count(1)
expect(tabs_toggle.topics_tab).to have_count(1)
end
end
end
context "when the new new view is not enabled" do
before { SiteSetting.experimental_new_new_view_groups = "" }
it "doesn't show the tabs toggle" do
visit("/new")
expect(tabs_toggle).to be_not_rendered
end
end
end
context "when on mobile", mobile: true do
include_examples "new list new topics and replies toggle"
context "when there are no new topics" do
before do
SiteSetting.experimental_new_new_view_groups = group.name
[new_topic, new_topic_in_category, new_topic_with_tag].each do |topic|
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
end
end
it "keeps the Topics tab even when there are no new topics" do
visit("/new")
expect(tabs_toggle.all_tab).to be_visible
expect(tabs_toggle.replies_tab).to be_visible
expect(tabs_toggle.topics_tab).to be_visible
expect(tabs_toggle.all_tab).to have_count(3)
expect(tabs_toggle.replies_tab).to have_count(3)
expect(tabs_toggle.topics_tab).to have_count(0)
end
end
context "when there are no new replies" do
before do
SiteSetting.experimental_new_new_view_groups = group.name
[new_reply, new_reply_in_category, new_reply_with_tag].each do |topic|
TopicUser.update_last_read(user, topic.id, 2, 1, 1)
end
end
it "keeps the Replies tab even when there are no new replies" do
visit("/new")
expect(tabs_toggle.all_tab).to be_visible
expect(tabs_toggle.replies_tab).to be_visible
expect(tabs_toggle.topics_tab).to be_visible
expect(tabs_toggle.all_tab).to have_count(3)
expect(tabs_toggle.replies_tab).to have_count(0)
expect(tabs_toggle.topics_tab).to have_count(3)
end
end
end
context "when on desktop" do
include_examples "new list new topics and replies toggle"
context "when there's only new topics" do
before do
SiteSetting.experimental_new_new_view_groups = group.name
[new_reply, new_reply_in_category, new_reply_with_tag].each do |topic|
TopicUser.update_last_read(user, topic.id, 2, 1, 1)
end
end
it "doesn't render the toggle and only shows a static label for new topics" do
visit("/new")
expect(tabs_toggle).to be_not_rendered
expect(find(".topic-list-header .static-label").text).to eq(
I18n.t("js.filters.new.topics_with_count", count: 3),
)
end
end
context "when there's only new replies" do
before do
SiteSetting.experimental_new_new_view_groups = group.name
[new_topic, new_topic_in_category, new_topic_with_tag].each do |topic|
TopicUser.update_last_read(user, topic.id, 1, 1, 1)
end
end
it "doesn't render the toggle and only shows a static label for new replies" do
visit("/new")
expect(tabs_toggle).to be_not_rendered
expect(find(".topic-list-header .static-label").text).to eq(
I18n.t("js.filters.new.replies_with_count", count: 3),
)
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module PageObjects
module Components
class NewTopicListToggle < PageObjects::Components::Base
COMMON_SELECTOR = ".topics-replies-toggle"
ALL_SELECTOR = "#{COMMON_SELECTOR}.all"
REPLIES_SELECTOR = "#{COMMON_SELECTOR}.replies"
TOPICS_SELECTOR = "#{COMMON_SELECTOR}.topics"
def not_rendered?
has_no_css?(COMMON_SELECTOR)
end
def all_tab
@all_tab ||= PageObjects::Components::NewTopicListToggleTab.new("all", ALL_SELECTOR)
end
def replies_tab
@replies_tab ||=
PageObjects::Components::NewTopicListToggleTab.new("replies", REPLIES_SELECTOR)
end
def topics_tab
@topics_tab ||=
PageObjects::Components::NewTopicListToggleTab.new("topics", TOPICS_SELECTOR)
end
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module PageObjects
module Components
class NewTopicListToggleTab < PageObjects::Components::Base
def initialize(name, selector)
super()
@name = name
@selector = selector
end
def active?
has_css?("#{@selector}.active")
end
def inactive?
has_no_css?("#{@selector}.active") && has_css?(@selector)
end
def visible?
has_css?(@selector)
end
def has_count?(count)
expected_label =
(
if count > 0
I18n.t("js.filters.new.#{@name}_with_count", count: count)
else
I18n.t("js.filters.new.#{@name}")
end
)
has_selector?(@selector, text: expected_label)
end
def click
find(@selector).click
end
end
end
end