FEATURE: option to sort user and group private messages. (#25146)

The UI will be the same as the one we're using in the topic list in "latest", "top" etc.,
This commit is contained in:
Vinoth Kannan 2024-01-10 13:33:30 +05:30 committed by GitHub
parent 9e758a3ae7
commit 992211350a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 173 additions and 33 deletions

View File

@ -8,6 +8,9 @@
@bulkSelectHelper={{this.bulkSelectHelper}} @bulkSelectHelper={{this.bulkSelectHelper}}
@canBulkSelect={{this.canBulkSelect}} @canBulkSelect={{this.canBulkSelect}}
@tagsForUser={{this.tagsForUser}} @tagsForUser={{this.tagsForUser}}
@changeSort={{this.changeSort}}
@order={{this.order}}
@ascending={{this.ascending}}
/> />
{{else}} {{else}}
{{#unless this.loadingMore}} {{#unless this.loadingMore}}

View File

@ -1,7 +1,10 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { or, reads } from "@ember/object/computed"; import { or, reads } from "@ember/object/computed";
import { isNone } from "@ember/utils";
import BulkSelectHelper from "discourse/lib/bulk-select-helper"; import BulkSelectHelper from "discourse/lib/bulk-select-helper";
import { defineTrackedProperty } from "discourse/lib/tracked-tools";
import Topic from "discourse/models/topic"; import Topic from "discourse/models/topic";
import { import {
NEW_FILTER, NEW_FILTER,
@ -9,12 +12,20 @@ import {
} from "discourse/routes/build-private-messages-route"; } from "discourse/routes/build-private-messages-route";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
export const queryParams = {
ascending: { replace: true, refreshModel: true, default: false },
order: { replace: true, refreshModel: true },
};
// Lists of topics on a user's page. // Lists of topics on a user's page.
export default class UserTopicsListController extends Controller { export default class UserTopicsListController extends Controller {
@tracked model;
hideCategory = false; hideCategory = false;
showPosters = false; showPosters = false;
channel = null; channel = null;
tagsForUser = null; tagsForUser = null;
queryParams = Object.keys(queryParams);
bulkSelectHelper = new BulkSelectHelper(this); bulkSelectHelper = new BulkSelectHelper(this);
@ -23,6 +34,13 @@ export default class UserTopicsListController extends Controller {
@or("currentUser.canManageTopic", "showDismissRead", "showResetNew") @or("currentUser.canManageTopic", "showDismissRead", "showResetNew")
canBulkSelect; canBulkSelect;
constructor() {
super(...arguments);
for (const [name, info] of Object.entries(queryParams)) {
defineTrackedProperty(this, name, info.default);
}
}
get bulkSelectEnabled() { get bulkSelectEnabled() {
return this.bulkSelectHelper.bulkSelectEnabled; return this.bulkSelectHelper.bulkSelectEnabled;
} }
@ -54,6 +72,28 @@ export default class UserTopicsListController extends Controller {
this.pmTopicTrackingState.stopIncomingTracking(); this.pmTopicTrackingState.stopIncomingTracking();
} }
@action
changeSort(sortBy) {
if (sortBy === this.resolvedOrder) {
this.ascending = !this.resolvedAscending;
} else {
this.ascending = false;
}
this.order = sortBy;
}
get resolvedAscending() {
if (isNone(this.ascending)) {
return this.model.get("params.ascending") === "true";
} else {
return this.ascending.toString() === "true";
}
}
get resolvedOrder() {
return this.order ?? this.model.get("params.order") ?? "activity";
}
@action @action
resetNew() { resetNew() {
const topicIds = this.selected const topicIds = this.selected

View File

@ -728,3 +728,12 @@ export function allowOnlyNumericInput(event, allowNegative = false) {
} }
} }
} }
export function cleanNullQueryParams(params) {
for (const [key, val] of Object.entries(params)) {
if (val === "undefined" || val === "null") {
params[key] = null;
}
}
return params;
}

View File

@ -1,5 +1,6 @@
import { capitalize } from "@ember/string"; import { capitalize } from "@ember/string";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list"; import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
import { cleanNullQueryParams } from "discourse/lib/utilities";
import createPMRoute from "discourse/routes/build-private-messages-route"; import createPMRoute from "discourse/routes/build-private-messages-route";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
@ -21,7 +22,7 @@ export default (inboxType, filter) => {
} }
}, },
model() { model(params = {}) {
const username = this.modelFor("user").get("username_lower"); const username = this.modelFor("user").get("username_lower");
const groupName = this.modelFor("userPrivateMessages.group").name; const groupName = this.modelFor("userPrivateMessages.group").name;
@ -36,18 +37,25 @@ export default (inboxType, filter) => {
topicListFilter topicListFilter
); );
return lastTopicList if (lastTopicList) {
? lastTopicList return lastTopicList;
: this.store }
.findFiltered("topicList", { filter: topicListFilter })
.then((topicList) => { params = cleanNullQueryParams(params);
// andrei: we agreed that this is an anti pattern,
// it's better to avoid mutating a rest model like this return this.store
// this place we'll be refactored later .findFiltered("topicList", {
// see https://github.com/discourse/discourse/pull/14313#discussion_r708784704 filter: topicListFilter,
topicList.set("emptyState", this.emptyState()); params,
return topicList; })
}); .then((topicList) => {
// andrei: we agreed that this is an anti pattern,
// it's better to avoid mutating a rest model like this
// this place we'll be refactored later
// see https://github.com/discourse/discourse/pull/14313#discussion_r708784704
topicList.set("emptyState", this.emptyState());
return topicList;
});
}, },
afterModel(model) { afterModel(model) {

View File

@ -1,6 +1,7 @@
import { action } from "@ember/object"; import { action } from "@ember/object";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list"; import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
import { cleanNullQueryParams } from "discourse/lib/utilities";
import UserAction from "discourse/models/user-action"; import UserAction from "discourse/models/user-action";
import UserTopicListRoute from "discourse/routes/user-topic-list"; import UserTopicListRoute from "discourse/routes/user-topic-list";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
@ -24,7 +25,7 @@ export default (inboxType, path, filter) => {
]; ];
}, },
model() { model(params = {}) {
const topicListFilter = const topicListFilter =
"topics/" + path + "/" + this.modelFor("user").get("username_lower"); "topics/" + path + "/" + this.modelFor("user").get("username_lower");
@ -33,18 +34,25 @@ export default (inboxType, path, filter) => {
topicListFilter topicListFilter
); );
return lastTopicList if (lastTopicList) {
? lastTopicList return lastTopicList;
: this.store }
.findFiltered("topicList", { filter: topicListFilter })
.then((model) => { params = cleanNullQueryParams(params);
// andrei: we agreed that this is an anti pattern,
// it's better to avoid mutating a rest model like this return this.store
// this place we'll be refactored later .findFiltered("topicList", {
// see https://github.com/discourse/discourse/pull/14313#discussion_r708784704 filter: topicListFilter,
model.set("emptyState", this.emptyState()); params,
return model; })
}); .then((model) => {
// andrei: we agreed that this is an anti pattern,
// it's better to avoid mutating a rest model like this
// this place we'll be refactored later
// see https://github.com/discourse/discourse/pull/14313#discussion_r708784704
model.set("emptyState", this.emptyState());
return model;
});
}, },
setupController() { setupController() {
@ -65,6 +73,17 @@ export default (inboxType, path, filter) => {
group: null, group: null,
inbox: inboxType, inbox: inboxType,
}); });
let ascending = userTopicsListController.ascending;
if (ascending === "true") {
ascending = true;
} else if (ascending === "false") {
ascending = false;
}
userTopicsListController.setProperties({
ascending,
});
userTopicsListController.bulkSelectHelper.clear(); userTopicsListController.bulkSelectHelper.clear();
userTopicsListController.subscribe(); userTopicsListController.subscribe();

View File

@ -4,7 +4,7 @@ import { isEmpty } from "@ember/utils";
import { queryParams, resetParams } from "discourse/controllers/discovery/list"; import { queryParams, resetParams } from "discourse/controllers/discovery/list";
import { disableImplicitInjections } from "discourse/lib/implicit-injections"; import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { setTopicList } from "discourse/lib/topic-list-tracker"; import { setTopicList } from "discourse/lib/topic-list-tracker";
import { defaultHomepage } from "discourse/lib/utilities"; import { cleanNullQueryParams, defaultHomepage } from "discourse/lib/utilities";
import Session from "discourse/models/session"; import Session from "discourse/models/session";
import Site from "discourse/models/site"; import Site from "discourse/models/site";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
@ -60,11 +60,7 @@ export async function findTopicList(
if (!list) { if (!list) {
// Clean up any string parameters that might slip through // Clean up any string parameters that might slip through
filterParams ||= {}; filterParams ||= {};
for (const [key, val] of Object.entries(filterParams)) { filterParams = cleanNullQueryParams(filterParams);
if (val === "undefined" || val === "null") {
filterParams[key] = null;
}
}
list = await store.findFiltered("topicList", { list = await store.findFiltered("topicList", {
filter, filter,

View File

@ -1,3 +1,4 @@
import { queryParams } from "discourse/controllers/user-topics-list";
import { setTopicList } from "discourse/lib/topic-list-tracker"; import { setTopicList } from "discourse/lib/topic-list-tracker";
import ViewingActionType from "discourse/mixins/viewing-action-type"; import ViewingActionType from "discourse/mixins/viewing-action-type";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
@ -6,6 +7,8 @@ export default DiscourseRoute.extend(ViewingActionType, {
templateName: "user-topics-list", templateName: "user-topics-list",
controllerName: "user-topics-list", controllerName: "user-topics-list",
queryParams,
setupController(controller, model) { setupController(controller, model) {
setTopicList(model); setTopicList(model);

View File

@ -50,6 +50,9 @@
@tagsForUser={{this.tagsForUser}} @tagsForUser={{this.tagsForUser}}
@canBulkSelect={{this.canBulkSelect}} @canBulkSelect={{this.canBulkSelect}}
@bulkSelectHelper={{this.bulkSelectHelper}} @bulkSelectHelper={{this.bulkSelectHelper}}
@changeSort={{this.changeSort}}
@order={{this.order}}
@ascending={{this.ascending}}
/> />
<TopicDismissButtons <TopicDismissButtons

View File

@ -298,6 +298,20 @@ const publishGroupNewToMessageBus = function (opts) {
); );
}; };
acceptance("User Private Messages - sorting", function (needs) {
withGroupMessagesSetup(needs);
test("order by posts_count", async function (assert) {
await visit("/u/eviltrout/messages");
assert.ok(exists(".topic-list-header th.posts.sortable"), "is sortable");
await click(".topic-list-header th.posts.sortable");
assert.ok(exists(".topic-list-header th.posts.sortable.sorting"), "sorted");
});
});
acceptance( acceptance(
"User Private Messages - user with group messages", "User Private Messages - user with group messages",
function (needs) { function (needs) {

View File

@ -263,8 +263,8 @@ class TopicQuery
.joins( .joins(
"LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{user.id.to_i})", "LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{user.id.to_i})",
) )
.order("topics.bumped_at DESC")
result = apply_ordering(result, options) if !options[:skip_ordering]
result = result.includes(:tags) if SiteSetting.tagging_enabled result = result.includes(:tags) if SiteSetting.tagging_enabled
result = result.limit(options[:per_page]) unless options[:limit] == false result = result.limit(options[:per_page]) unless options[:limit] == false
result = result.visible if options[:visible] || @user.nil? || @user.regular? result = result.visible if options[:visible] || @user.nil? || @user.regular?

View File

@ -398,6 +398,25 @@ RSpec.describe ListController do
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
it "should sort group private messages by posts_count" do
topic2 = Fabricate(:private_message_topic, allowed_groups: [group])
topic3 = Fabricate(:private_message_topic, allowed_groups: [group])
2.times { Fabricate(:post, topic: topic2) }
Fabricate(:post, topic: topic3)
sign_in(Fabricate(:admin))
get "/topics/private-messages-group/#{user.username}/#{group.name}.json",
params: {
order: "posts",
}
expect(response.status).to eq(200)
expect(response.parsed_body["topic_list"]["topics"].map { |t| t["id"] }).to eq(
[topic2.id, topic3.id, topic.id],
)
end
end end
describe "with unicode_usernames" do describe "with unicode_usernames" do
@ -859,6 +878,32 @@ RSpec.describe ListController do
json = response.parsed_body json = response.parsed_body
expect(json["topic_list"]["topics"].size).to eq(1) expect(json["topic_list"]["topics"].size).to eq(1)
end end
it "sorts private messages by activity" do
topic_ids = []
[1.year.ago, 1.week.ago, 1.month.ago].each do |date|
pm =
Fabricate(
:private_message_topic,
user: Fabricate(:user),
created_at: date,
bumped_at: date,
)
pm.topic_allowed_users.create!(user: user)
topic_ids << pm.id
end
sign_in(user)
get "/topics/private-messages/#{user.username}.json", params: { order: "activity" }
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["topic_list"]["topics"].pluck("id")).to eq(
[topic_ids[1], topic_ids[2], topic_ids[0]],
)
end
end end
describe "private_messages_sent" do describe "private_messages_sent" do