FEATURE: Dismiss new and unread for PM inboxes.

This commit is contained in:
Alan Guo Xiang Tan 2021-07-30 17:00:48 +08:00
parent d3779d4cf7
commit 2c046cc670
29 changed files with 769 additions and 152 deletions

View File

@ -44,7 +44,7 @@ export default Controller.extend({
@discourseComputed("pmView")
isPersonalInbox(pmView) {
return pmView && pmView.startsWith("personal");
return pmView && pmView.startsWith("user");
},
@discourseComputed("isPersonalInbox", "group.name")

View File

@ -1,6 +1,16 @@
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import { action } from "@ember/object";
import Topic from "discourse/models/topic";
import {
NEW_FILTER,
UNREAD_FILTER,
} from "discourse/routes/build-private-messages-route";
// Lists of topics on a user's page.
export default Controller.extend(BulkTopicSelection, {
@ -12,18 +22,17 @@ export default Controller.extend(BulkTopicSelection, {
channel: null,
tagsForUser: null,
init() {
this._super(...arguments);
@on("init")
_initialize() {
this.newIncoming = [];
},
saveScrollPosition: function () {
saveScrollPosition() {
this.session.set("topicListScrollPosition", $(window).scrollTop());
},
@observes("model.canLoadMore")
_showFooter: function () {
_showFooter() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
},
@ -32,6 +41,16 @@ export default Controller.extend(BulkTopicSelection, {
return incomingCount > 0;
},
@discourseComputed("filter", "model.topics.length")
showResetNew(filter, hasTopics) {
return filter === NEW_FILTER && hasTopics;
},
@discourseComputed("filter", "model.topics.length")
showDismissRead(filter, hasTopics) {
return filter === UNREAD_FILTER && hasTopics;
},
subscribe(channel) {
this.set("channel", channel);
@ -59,15 +78,35 @@ export default Controller.extend(BulkTopicSelection, {
});
},
actions: {
loadMore: function () {
this.model.loadMore();
},
@action
resetNew() {
const topicIds = this.selected
? this.selected.map((topic) => topic.id)
: null;
showInserted() {
this.model.loadBefore(this.newIncoming);
this._resetTracking();
return false;
},
const opts = {
inbox: this.inbox,
topicIds: topicIds,
};
if (this.group) {
opts.groupName = this.group.name;
}
Topic.pmResetNew(opts).then(() => {
this.send("refresh");
});
},
@action
loadMore() {
this.model.loadMore();
},
@action
showInserted() {
this.model.loadBefore(this.newIncoming);
this._resetTracking();
return false;
},
});

View File

@ -747,6 +747,14 @@ Topic.reopenClass({
if (options.tagName) {
data.tag_name = options.tagName;
}
if (options.private_message_inbox) {
data.private_message_inbox = options.private_message_inbox;
if (options.group_name) {
data.group_name = options.group_name;
}
}
}
return ajax("/topics/bulk", {
@ -778,6 +786,24 @@ Topic.reopenClass({
return ajax("/topics/reset-new", { type: "PUT", data });
},
pmResetNew(opts = {}) {
const data = {};
if (opts.topicIds) {
data.topic_ids = opts.topicIds;
}
if (opts.inbox) {
data.inbox = opts.inbox;
if (opts.groupName) {
data.group_name = opts.groupName;
}
}
return ajax("/topics/pm-reset-new", { type: "PUT", data });
},
idForSlug(slug) {
return ajax(`/t/id_for/${slug}`);
},

View File

@ -2,8 +2,8 @@ import I18n from "I18n";
import createPMRoute from "discourse/routes/build-private-messages-route";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
export default (viewName, channel) => {
return createPMRoute("groups", "private-messages-groups").extend({
export default (inboxType, filter) => {
return createPMRoute(inboxType, "private-messages-groups", filter).extend({
groupName: null,
titleToken() {
@ -12,8 +12,8 @@ export default (viewName, channel) => {
if (groupName) {
let title = groupName.capitalize();
if (viewName !== "index") {
title = `${title} ${I18n.t("user.messages." + viewName)}`;
if (filter !== "inbox") {
title = `${title} ${I18n.t("user.messages." + filter)}`;
}
return [title, I18n.t(`user.private_messages`)];
@ -22,24 +22,27 @@ export default (viewName, channel) => {
model(params) {
const username = this.modelFor("user").get("username_lower");
let filter = `topics/private-messages-group/${username}/${params.name}`;
let topicListFilter = `topics/private-messages-group/${username}/${params.name}`;
if (viewName !== "index") {
filter = `${filter}/${viewName}`;
if (filter !== "inbox") {
topicListFilter = `${topicListFilter}/${filter}`;
}
const lastTopicList = findOrResetCachedTopicList(this.session, filter);
const lastTopicList = findOrResetCachedTopicList(
this.session,
topicListFilter
);
return lastTopicList
? lastTopicList
: this.store.findFiltered("topicList", { filter });
: this.store.findFiltered("topicList", { filter: topicListFilter });
},
afterModel(model) {
const filters = model.get("filter").split("/");
let groupName;
if (viewName !== "index") {
if (filter !== "inbox") {
groupName = filters[filters.length - 2];
} else {
groupName = filters.pop();
@ -55,14 +58,21 @@ export default (viewName, channel) => {
setupController() {
this._super.apply(this, arguments);
this.controllerFor("user-private-messages").set("group", this.group);
this.controllerFor("user-topics-list").set("group", this.group);
if (channel) {
if (filter) {
this.controllerFor("user-topics-list").subscribe(
`/private-messages/group/${this.get(
"groupName"
).toLowerCase()}/${channel}`
).toLowerCase()}/${filter}`
);
}
},
dismissReadOptions() {
return {
group_name: this.get("groupName"),
};
},
});
};

View File

@ -2,41 +2,49 @@ import I18n from "I18n";
import UserAction from "discourse/models/user-action";
import UserTopicListRoute from "discourse/routes/user-topic-list";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
import { action } from "@ember/object";
export const NEW_FILTER = "new";
export const UNREAD_FILTER = "unread";
// A helper to build a user topic list route
export default (viewName, path, channel) => {
export default (inboxType, path, filter) => {
return UserTopicListRoute.extend({
userActionType: UserAction.TYPES.messages_received,
titleToken() {
const key = viewName === "index" ? "inbox" : viewName;
return [I18n.t(`user.messages.${key}`), I18n.t("user.private_messages")];
return [
I18n.t(`user.messages.${filter}`),
I18n.t("user.private_messages"),
];
},
actions: {
didTransition() {
this.controllerFor("user-topics-list")._showFooter();
return true;
},
@action
didTransition() {
this.controllerFor("user-topics-list")._showFooter();
return true;
},
model() {
const filter =
const topicListFilter =
"topics/" + path + "/" + this.modelFor("user").get("username_lower");
const lastTopicList = findOrResetCachedTopicList(this.session, filter);
const lastTopicList = findOrResetCachedTopicList(
this.session,
topicListFilter
);
return lastTopicList
? lastTopicList
: this.store.findFiltered("topicList", { filter });
: this.store.findFiltered("topicList", { filter: topicListFilter });
},
setupController() {
this._super.apply(this, arguments);
if (channel) {
if (filter) {
this.controllerFor("user-topics-list").subscribe(
`/private-messages/${channel}`
`/private-messages/${filter}`
);
}
@ -46,11 +54,14 @@ export default (viewName, path, channel) => {
tagsForUser: this.modelFor("user").get("username_lower"),
selected: [],
showToggleBulkSelect: true,
filter: filter,
group: null,
inbox: inboxType,
});
this.controllerFor("user-private-messages").setProperties({
archive: false,
pmView: viewName,
pmView: inboxType,
group: null,
});
@ -65,5 +76,20 @@ export default (viewName, path, channel) => {
this.controllerFor("user").get("model.searchContext")
);
},
dismissReadOptions() {
return {};
},
@action
dismissReadTopics(dismissTopics) {
const operationType = dismissTopics ? "topics" : "posts";
const controller = this.controllerFor("user-topics-list");
controller.send("dismissRead", operationType, {
private_message_inbox: inboxType,
...this.dismissReadOptions(),
});
},
});
};

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-group-route";
export default createPMRoute("archive", "archive");
export default createPMRoute("group", "archive");

View File

@ -1,3 +1,4 @@
import createPMRoute from "discourse/routes/build-private-messages-group-route";
import { NEW_FILTER } from "discourse/routes/build-private-messages-route";
export default createPMRoute("new", null /* no message bus notifications */);
export default createPMRoute("group", NEW_FILTER);

View File

@ -1,3 +1,4 @@
import createPMRoute from "discourse/routes/build-private-messages-group-route";
import { UNREAD_FILTER } from "discourse/routes/build-private-messages-route";
export default createPMRoute("unread", null /* no message bus notifications */);
export default createPMRoute("group", UNREAD_FILTER);

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-group-route";
export default createPMRoute("index", "inbox");
export default createPMRoute("group", "inbox");

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute("index", "private-messages-all", "inbox");
export default createPMRoute("all", "private-messages-all", "inbox");

View File

@ -1,7 +1,6 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
import {
NEW_FILTER,
default as createPMRoute,
} from "discourse/routes/build-private-messages-route";
export default createPMRoute(
"new",
"private-messages-all-new",
null /* no message bus notifications */
);
export default createPMRoute("all", "private-messages-all-new", NEW_FILTER);

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute("personal", "private-messages-archive", "archive");
export default createPMRoute("user", "private-messages-archive", "archive");

View File

@ -1,7 +1,6 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
import {
NEW_FILTER,
default as createPMRoute,
} from "discourse/routes/build-private-messages-route";
export default createPMRoute(
"personal",
"private-messages-new",
null /* no message bus notifications */
);
export default createPMRoute("user", "private-messages-new", NEW_FILTER);

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute("personal", "private-messages-sent", "sent");
export default createPMRoute("user", "private-messages-sent", "sent");

View File

@ -1,7 +1,6 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
import {
UNREAD_FILTER,
default as createPMRoute,
} from "discourse/routes/build-private-messages-route";
export default createPMRoute(
"personal",
"private-messages-unread",
null /* no message bus notifications */
);
export default createPMRoute("user", "private-messages-unread", UNREAD_FILTER);

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute("personal", "private-messages", "inbox");
export default createPMRoute("user", "private-messages", "inbox");

View File

@ -1,7 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute(
"sent",
"private-messages-all-sent",
null /* no message bus notifications */
);
export default createPMRoute("all", "private-messages-all-sent", "sent");

View File

@ -1,7 +1,10 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
import {
UNREAD_FILTER,
default as createPMRoute,
} from "discourse/routes/build-private-messages-route";
export default createPMRoute(
"unread",
"all",
"private-messages-all-unread",
null /* no message bus notifications */
UNREAD_FILTER
);

View File

@ -1,12 +1,4 @@
{{#conditional-loading-spinner condition=loading}}
{{#if hasIncoming}}
<div class="show-mores">
<a tabindex="0" href {{action showInserted}} class="alert alert-info clickable">
{{count-i18n key="topic_count_" suffix="latest" count=incomingCount}}
</a>
</div>
{{/if}}
{{#if topics}}
{{topic-list
showPosters=showPosters

View File

@ -5,14 +5,27 @@
{{/unless}}
{{#load-more class="paginated-topics-list" selector=".paginated-topics-list .topic-list tr" action=(action "loadMore")}}
{{topic-dismiss-buttons
position="top"
selectedTopics=selected
model=model
showResetNew=showResetNew
showDismissRead=showDismissRead
resetNew=(action "resetNew")}}
{{#if hasIncoming}}
<div class="show-mores">
<a tabindex="0" href {{action "showInserted"}} class="alert alert-info clickable">
{{count-i18n key="topic_count_" suffix="latest" count=incomingCount}}
</a>
</div>
{{/if}}
{{basic-topic-list topicList=model
hideCategory=hideCategory
showPosters=showPosters
bulkSelectEnabled=bulkSelectEnabled
selected=selected
hasIncoming=hasIncoming
incomingCount=incomingCount
showInserted=(action "showInserted")
tagsForUser=tagsForUser
onScroll=saveScrollPosition
canBulkSelect=canBulkSelect
@ -20,5 +33,13 @@
toggleBulkSelect=(action "toggleBulkSelect")
updateAutoAddTopicsToBulkSelect=(action "updateAutoAddTopicsToBulkSelect")}}
{{topic-dismiss-buttons
position="bottom"
selectedTopics=selected
model=model
showResetNew=showResetNew
showDismissRead=showDismissRead
resetNew=(action "resetNew")}}
{{conditional-loading-spinner condition=model.loadingMore}}
{{/load-more}}

View File

@ -40,12 +40,22 @@ acceptance(
acceptance(
"User Private Messages - user with group messages",
function (needs) {
let fetchedNew;
let fetchUserNew;
let fetchedGroupNew;
needs.user();
needs.site({
can_tag_pms: true,
});
needs.hooks.afterEach(() => {
fetchedNew = false;
fetchedGroupNew = false;
fetchUserNew = false;
});
needs.pretender((server, helper) => {
server.get("/topics/private-messages-all/:username.json", () => {
return helper.response({
@ -59,6 +69,35 @@ acceptance(
});
});
[
"/topics/private-messages-all-new/:username.json",
"/topics/private-messages-all-unread/:username.json",
"/topics/private-messages-new/:username.json",
"/topics/private-messages-unread/:username.json",
"/topics/private-messages-group/:username/:group_name/new.json",
"/topics/private-messages-group/:username/:group_name/unread.json",
].forEach((url) => {
server.get(url, () => {
let topics;
if (fetchedNew || fetchedGroupNew || fetchUserNew) {
topics = [];
} else {
topics = [
{ id: 1, posters: [] },
{ id: 2, posters: [] },
{ id: 3, posters: [] },
];
}
return helper.response({
topic_list: {
topics: topics,
},
});
});
});
server.get(
"/topics/private-messages-group/:username/:group_name.json",
() => {
@ -72,6 +111,157 @@ acceptance(
});
}
);
server.put("/topics/pm-reset-new", (request) => {
const requestBody = request.requestBody;
// No easy way to do this https://github.com/pretenderjs/pretender/issues/159
if (requestBody === "inbox=group&group_name=awesome_group") {
fetchedGroupNew = true;
}
if (requestBody === "inbox=user") {
fetchUserNew = true;
}
if (requestBody === "inbox=all") {
fetchedNew = true;
}
return helper.response({});
});
server.put("/topics/bulk", (request) => {
const requestBody = request.requestBody;
if (requestBody.includes("private_message_inbox=all")) {
fetchedNew = true;
}
if (
requestBody.includes(
"private_message_inbox=group&group_name=awesome_group"
)
) {
fetchedGroupNew = true;
}
if (requestBody.includes("private_message_inbox=user")) {
fetchUserNew = true;
}
return helper.response({});
});
});
test("dismissing all unread messages", async function (assert) {
await visit("/u/charlie/messages/unread");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
await click(".btn.dismiss-read");
await click("#dismiss-read-confirm");
assert.equal(
count(".topic-list-item"),
0,
"displays the right topic list"
);
});
test("dismissing personal unread messages", async function (assert) {
await visit("/u/charlie/messages/personal/unread");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
await click(".btn.dismiss-read");
await click("#dismiss-read-confirm");
assert.equal(
count(".topic-list-item"),
0,
"displays the right topic list"
);
});
test("dismissing group unread messages", async function (assert) {
await visit("/u/charlie/messages/group/awesome_group/unread");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
await click(".btn.dismiss-read");
await click("#dismiss-read-confirm");
assert.equal(
count(".topic-list-item"),
0,
"displays the right topic list"
);
});
test("dismissing all new messages", async function (assert) {
await visit("/u/charlie/messages/new");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
await click(".btn.dismiss-read");
assert.equal(
count(".topic-list-item"),
0,
"displays the right topic list"
);
});
test("dismissing personal new messages", async function (assert) {
await visit("/u/charlie/messages/personal/new");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
await click(".btn.dismiss-read");
assert.equal(
count(".topic-list-item"),
0,
"displays the right topic list"
);
});
test("dismissing new group messages", async function (assert) {
await visit("/u/charlie/messages/group/awesome_group/new");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
await click(".btn.dismiss-read");
assert.equal(
count(".topic-list-item"),
0,
"displays the right topic list"
);
});
test("viewing messages", async function (assert) {

View File

@ -925,26 +925,7 @@ class TopicsController < ApplicationController
end
topic_ids = params[:topic_ids].map { |t| t.to_i }
elsif params[:filter] == 'unread'
tq = TopicQuery.new(current_user)
topics = TopicQuery.unread_filter(tq.joined_topic_user, staff: guardian.is_staff?).listable_topics
topics = TopicQuery.tracked_filter(topics, current_user.id) if params[:tracked].to_s == "true"
if params[:category_id]
if params[:include_subcategories]
topics = topics.where(<<~SQL, category_id: params[:category_id])
category_id in (select id FROM categories WHERE parent_category_id = :category_id) OR
category_id = :category_id
SQL
else
topics = topics.where('category_id = ?', params[:category_id])
end
end
if params[:tag_name].present?
topics = topics.joins(:tags).where("tags.name": params[:tag_name])
end
topic_ids = topics.pluck(:id)
topic_ids = bulk_unread_topic_ids
else
raise ActionController::ParameterMissing.new(:topic_ids)
end
@ -960,6 +941,35 @@ class TopicsController < ApplicationController
render_json_dump topic_ids: changed_topic_ids
end
def private_message_reset_new
topic_query = TopicQuery.new(current_user)
if params[:topic_ids].present?
unless Array === params[:topic_ids]
raise Discourse::InvalidParameters.new(
"Expecting topic_ids to contain a list of topic ids"
)
end
topic_scope = topic_query
.private_messages_for(current_user, :all)
.where("topics.id IN (?)", params[:topic_ids].map(&:to_i))
else
params.require(:inbox)
inbox = params[:inbox].to_s
filter = private_message_filter(topic_query, inbox)
topic_scope = topic_query.filter_private_message_new(current_user, filter)
end
TopicsBulkAction.new(
current_user,
topic_scope.pluck(:id),
type: "dismiss_topics"
).perform!
render json: success_json
end
def reset_new
topic_scope =
if params[:category_id].present?
@ -993,7 +1003,7 @@ class TopicsController < ApplicationController
topic_scope = topic_scope.where(id: topic_ids)
end
dismissed_topic_ids = TopicsBulkAction.new(current_user, [topic_scope.pluck(:id)], type: "dismiss_topics").perform!
dismissed_topic_ids = TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_topics").perform!
TopicTrackingState.publish_dismiss_new(current_user.id, topic_ids: dismissed_topic_ids)
render body: nil
@ -1217,4 +1227,49 @@ class TopicsController < ApplicationController
def pm_has_slots?(pm)
guardian.is_staff? || !pm.reached_recipients_limit?
end
def bulk_unread_topic_ids
topic_query = TopicQuery.new(current_user)
if inbox = params[:private_message_inbox]
filter = private_message_filter(topic_query, inbox)
topics = topic_query.filter_private_messages_unread(current_user, filter)
else
topics = TopicQuery.unread_filter(topic_query.joined_topic_user, staff: guardian.is_staff?).listable_topics
topics = TopicQuery.tracked_filter(topics, current_user.id) if params[:tracked].to_s == "true"
if params[:category_id]
if params[:include_subcategories]
topics = topics.where(<<~SQL, category_id: params[:category_id])
category_id in (select id FROM categories WHERE parent_category_id = :category_id) OR
category_id = :category_id
SQL
else
topics = topics.where('category_id = ?', params[:category_id])
end
end
if params[:tag_name].present?
topics = topics.joins(:tags).where("tags.name": params[:tag_name])
end
end
topics.pluck(:id)
end
def private_message_filter(topic_query, inbox)
case inbox
when "group"
group_name = params[:group_name]
group = Group.find_by("lower(name) = ?", group_name)
raise Discourse::NotFound if !group
raise Discourse::NotFound if !guardian.can_see_group_messages?(group)
topic_query.options[:group_name] = group_name
:group
when "user"
:user
else
:all
end
end
end

View File

@ -758,6 +758,7 @@ Discourse::Application.routes.draw do
put "t/:id/reset-bump-date" => "topics#reset_bump_date"
put "topics/bulk"
put "topics/reset-new" => 'topics#reset_new'
put "topics/pm-reset-new" => 'topics#private_message_reset_new'
post "topics/timings"
get 'topics/similar_to' => 'similar_topics#index'

View File

@ -806,7 +806,6 @@ class TopicQuery
list = list
.references("cu")
.joins("LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id}")
.joins("LEFT JOIN dismissed_topic_users ON dismissed_topic_users.topic_id = topics.id AND dismissed_topic_users.user_id = #{user.id}")
.where("topics.category_id = :category_id
OR COALESCE(category_users.notification_level, :default) <> :muted
OR tu.notification_level > :regular",
@ -877,10 +876,16 @@ class TopicQuery
def remove_dismissed(list, user)
if user
list = list.where("dismissed_topic_users.id IS NULL")
list
.joins(<<~SQL)
LEFT JOIN dismissed_topic_users
ON dismissed_topic_users.topic_id = topics.id
AND dismissed_topic_users.user_id = #{user.id.to_i}
SQL
.where("dismissed_topic_users.id IS NULL")
else
list
end
list
end
def new_messages(params)

View File

@ -63,30 +63,15 @@ class TopicQuery
end
def list_private_messages_new(user, type = :user)
list = TopicQuery.new_filter(
private_messages_for(user, type),
treat_as_new_topic_start_date: user.user_option.treat_as_new_topic_start_date
)
list = filter_private_message_new(user, type)
list = remove_muted_tags(list, user)
list = remove_dismissed(list, user)
create_list(:private_messages, {}, list)
end
def list_private_messages_unread(user, type = :user)
list = TopicQuery.unread_filter(
private_messages_for(user, type),
staff: user.staff?
)
first_unread_pm_at = UserStat
.where(user_id: user.id)
.pluck_first(:first_unread_pm_at)
if first_unread_pm_at
list = list.where("topics.updated_at >= ?", first_unread_pm_at)
end
list = filter_private_messages_unread(user, type)
create_list(:private_messages, {}, list)
end
@ -118,30 +103,14 @@ class TopicQuery
end
def list_private_messages_group_new(user)
list = TopicQuery.new_filter(
private_messages_for(user, :group),
treat_as_new_topic_start_date: user.user_option.treat_as_new_topic_start_date
)
list = filter_private_message_new(user, :group)
publish_read_state = !!group.publish_read_state
list = append_read_state(list, group) if publish_read_state
create_list(:private_messages, { publish_read_state: publish_read_state }, list)
end
def list_private_messages_group_unread(user)
list = TopicQuery.unread_filter(
private_messages_for(user, :group),
staff: user.staff?
)
first_unread_pm_at = UserStat
.where(user_id: user.id)
.pluck_first(:first_unread_pm_at)
if first_unread_pm_at
list = list.where("topics.updated_at >= ?", first_unread_pm_at)
end
list = filter_private_messages_unread(user, :group)
publish_read_state = !!group.publish_read_state
list = append_read_state(list, group) if publish_read_state
create_list(:private_messages, { publish_read_state: publish_read_state }, list)
@ -206,6 +175,30 @@ class TopicQuery
create_list(:private_messages, {}, list)
end
def filter_private_messages_unread(user, type)
list = TopicQuery.unread_filter(
private_messages_for(user, type),
staff: user.staff?
)
first_unread_pm_at = UserStat
.where(user_id: user.id)
.pluck_first(:first_unread_pm_at)
if first_unread_pm_at
list = list.where("topics.updated_at >= ?", first_unread_pm_at)
end
list
end
def filter_private_message_new(user, type)
TopicQuery.new_filter(
private_messages_for(user, type),
treat_as_new_topic_start_date: user.user_option.treat_as_new_topic_start_date
)
end
private
def append_read_state(list, group)

View File

@ -86,7 +86,6 @@ class TopicsBulkAction
.joins("LEFT JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{@user.id}")
.where("topics.created_at >= ?", dismiss_topics_since_date)
.where("topic_users.last_read_post_number IS NULL")
.where("topics.archetype <> ?", Archetype.private_message)
.order("topics.created_at DESC")
.limit(SiteSetting.max_new_topics).map do |topic|
{

View File

@ -5,7 +5,7 @@ require 'rails_helper'
describe TopicsBulkAction do
fab!(:topic) { Fabricate(:topic) }
describe type: "dismiss_topics" do
describe "#dismiss_topics" do
fab!(:user) { Fabricate(:user, created_at: 1.days.ago) }
fab!(:category) { Fabricate(:category) }
fab!(:topic2) { Fabricate(:topic, category: category, created_at: 60.minutes.ago) }
@ -15,6 +15,14 @@ describe TopicsBulkAction do
topic.destroy!
end
it 'dismisses private messages' do
pm = Fabricate(:private_message_topic)
TopicsBulkAction.new(user, [pm.id], type: "dismiss_topics").perform!
expect(DismissedTopicUser.exists?(topic: pm)).to eq(true)
end
it 'dismisses two topics' do
expect { TopicsBulkAction.new(user, [Topic.all.pluck(:id)], type: "dismiss_topics").perform! }.to change { DismissedTopicUser.count }.by(2)
end

View File

@ -282,9 +282,17 @@ describe TopicQuery::PrivateMessageLists do
).topic
end
fab!(:pm_2) do
create_post(
user: user,
target_usernames: [user_2.username],
archetype: Archetype.private_message
).topic
end
it 'returns a list of new private messages' do
expect(TopicQuery.new(user_2).list_private_messages_new(user_2).topics)
.to contain_exactly(pm)
.to contain_exactly(pm, pm_2)
end
it 'returns a list of new private messages accounting for muted tags' do
@ -299,7 +307,14 @@ describe TopicQuery::PrivateMessageLists do
)
expect(TopicQuery.new(user_2).list_private_messages_new(user_2).topics)
.to eq([])
.to contain_exactly(pm_2)
end
it 'returns a list of new private messages accounting for dismissed topics' do
Fabricate(:dismissed_topic_user, topic: pm, user: user_2)
expect(TopicQuery.new(user_2).list_private_messages_new(user_2).topics)
.to contain_exactly(pm_2)
end
end
end

View File

@ -2838,6 +2838,116 @@ RSpec.describe TopicsController do
expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
end
context "private message" do
fab!(:user_2) { Fabricate(:user) }
fab!(:group) do
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap do |g|
g.add(user_2)
end
end
fab!(:group_message) do
create_post(
user: user,
target_group_names: [group.name],
archetype: Archetype.private_message
).topic
end
fab!(:private_message) do
create_post(
user: user,
target_usernames: [user_2.username],
archetype: Archetype.private_message
).topic
end
fab!(:group_pm_topic_user) do
TopicUser.find_by(user: user_2, topic: group_message).tap do |tu|
tu.update!(last_read_post_number: 1)
end
end
fab!(:regular_pm_topic_user) do
TopicUser.find_by(user: user_2, topic: private_message).tap do |tu|
tu.update!(last_read_post_number: 1)
end
end
before do
create_post(user: user, topic: group_message)
create_post(user: user, topic: private_message)
sign_in(user_2)
end
it "can dismiss all user and group private message topics" do
expect do
put "/topics/bulk.json", params: {
filter: "unread",
operation: { type: 'dismiss_posts' },
private_message_inbox: "all"
}
expect(response.status).to eq(200)
end.to change { group_pm_topic_user.reload.last_read_post_number }.from(1).to(2)
.and change { regular_pm_topic_user.reload.last_read_post_number }.from(1).to(2)
end
it "can dismiss all user unread private message topics" do
expect do
put "/topics/bulk.json", params: {
filter: "unread",
operation: { type: 'dismiss_posts' },
private_message_inbox: "user"
}
expect(response.status).to eq(200)
end.to change { regular_pm_topic_user.reload.last_read_post_number }.from(1).to(2)
expect(group_pm_topic_user.reload.last_read_post_number).to eq(1)
end
it "returns the right response when trying to dismiss private messages of an invalid group" do
put "/topics/bulk.json", params: {
filter: "unread",
operation: { type: 'dismiss_posts' },
private_message_inbox: "group",
group_name: 'randomgroup'
}
expect(response.status).to eq(404)
end
it "returns the right response when trying to dismiss private messages of a restricted group" do
sign_in(user)
put "/topics/bulk.json", params: {
filter: "unread",
operation: { type: 'dismiss_posts' },
private_message_inbox: "group",
group_name: group.name
}
expect(response.status).to eq(404)
end
it "can dismiss all group unread private message topics" do
expect do
put "/topics/bulk.json", params: {
filter: "unread",
operation: { type: 'dismiss_posts' },
private_message_inbox: "group",
group_name: group.name
}
expect(response.status).to eq(200)
end.to change { group_pm_topic_user.reload.last_read_post_number }.from(1).to(2)
expect(regular_pm_topic_user.reload.last_read_post_number).to eq(1)
end
end
it "can find unread" do
# mark all unread muted
put "/topics/bulk.json", params: {
@ -4081,4 +4191,133 @@ RSpec.describe TopicsController do
end
end
end
describe '#private_message_reset_new' do
fab!(:user_2) { Fabricate(:user) }
fab!(:group) do
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap do |g|
g.add(user_2)
end
end
fab!(:group_message) do
create_post(
user: user,
target_group_names: [group.name],
archetype: Archetype.private_message
).topic
end
fab!(:private_message) do
create_post(
user: user,
target_usernames: [user_2.username],
archetype: Archetype.private_message
).topic
end
before do
sign_in(user_2)
end
it 'returns the right response when inbox param is missing' do
put "/topics/pm-reset-new.json"
expect(response.status).to eq(400)
end
it "returns the right response when trying to reset new private messages of an invalid group" do
put "/topics/pm-reset-new.json", params: {
inbox: "group",
group_name: "randomgroup"
}
expect(response.status).to eq(404)
end
it "returns the right response when trying to reset new private messages of a restricted group" do
sign_in(user)
put "/topics/pm-reset-new.json", params: {
inbox: "group",
group_name: group.name
}
expect(response.status).to eq(404)
end
it "can reset all new group private messages" do
put "/topics/pm-reset-new.json", params: {
inbox: "group",
group_name: group.name
}
expect(response.status).to eq(200)
expect(DismissedTopicUser.count).to eq(1)
expect(DismissedTopicUser.exists?(topic: group_message, user: user_2))
.to eq(true)
end
it "can reset new personal private messages" do
put "/topics/pm-reset-new.json", params: {
inbox: "user",
}
expect(response.status).to eq(200)
expect(DismissedTopicUser.count).to eq(1)
expect(DismissedTopicUser.exists?(topic: private_message, user: user_2))
.to eq(true)
end
it 'can reset new personal and group private messages' do
put "/topics/pm-reset-new.json", params: {
inbox: "all",
}
expect(response.status).to eq(200)
expect(DismissedTopicUser.count).to eq(2)
expect(DismissedTopicUser.exists?(topic: private_message, user: user_2))
.to eq(true)
expect(DismissedTopicUser.exists?(topic: group_message, user: user_2))
.to eq(true)
end
it 'returns the right response is topic_ids params is not valid' do
put "/topics/pm-reset-new.json", params: {
topic_ids: '1'
}
expect(response.status).to eq(400)
end
it 'can reset new private messages from given topic ids' do
put "/topics/pm-reset-new.json", params: {
topic_ids: [group_message.id, '12345']
}
expect(response.status).to eq(200)
expect(DismissedTopicUser.count).to eq(1)
expect(DismissedTopicUser.exists?(topic: group_message, user: user_2))
.to eq(true)
put "/topics/pm-reset-new.json", params: {
topic_ids: [private_message.id, '12345']
}
expect(response.status).to eq(200)
expect(DismissedTopicUser.exists?(topic: private_message, user: user_2))
.to eq(true)
end
end
end