diff --git a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js index bcdece0d7e9..f50f9bad576 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-private-messages.js +++ b/app/assets/javascripts/discourse/app/controllers/user-private-messages.js @@ -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") diff --git a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js index 61b3c118238..4b3adfba487 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-topics-list.js +++ b/app/assets/javascripts/discourse/app/controllers/user-topics-list.js @@ -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; }, }); diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index a603aeec261..d15d70e903d 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -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}`); }, diff --git a/app/assets/javascripts/discourse/app/routes/build-private-messages-group-route.js b/app/assets/javascripts/discourse/app/routes/build-private-messages-group-route.js index d1aa356eebf..c82d36ccdf4 100644 --- a/app/assets/javascripts/discourse/app/routes/build-private-messages-group-route.js +++ b/app/assets/javascripts/discourse/app/routes/build-private-messages-group-route.js @@ -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"), + }; + }, }); }; diff --git a/app/assets/javascripts/discourse/app/routes/build-private-messages-route.js b/app/assets/javascripts/discourse/app/routes/build-private-messages-route.js index 2eed30a80da..cf2a8c8ae38 100644 --- a/app/assets/javascripts/discourse/app/routes/build-private-messages-route.js +++ b/app/assets/javascripts/discourse/app/routes/build-private-messages-route.js @@ -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(), + }); + }, }); }; diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-group-archive.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-group-archive.js index 42b0d95c916..726f6e15017 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-group-archive.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-group-archive.js @@ -1,3 +1,3 @@ import createPMRoute from "discourse/routes/build-private-messages-group-route"; -export default createPMRoute("archive", "archive"); +export default createPMRoute("group", "archive"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-group-new.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-group-new.js index 25d9693cbed..0a0b1be1aa2 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-group-new.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-group-new.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-group-unread.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-group-unread.js index 9bee9aa9994..4914a9a0bd1 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-group-unread.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-group-unread.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-group.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-group.js index ea93a1fa919..ce17c2fc0a5 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-group.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-group.js @@ -1,3 +1,3 @@ import createPMRoute from "discourse/routes/build-private-messages-group-route"; -export default createPMRoute("index", "inbox"); +export default createPMRoute("group", "inbox"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-index.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-index.js index e973160ef8f..fa4024d5289 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-index.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-index.js @@ -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"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-new.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-new.js index b6f538f6e50..cb064b20c2f 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-new.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-new.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-archive.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-archive.js index 1999282a5ce..17ab77b75cb 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-archive.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-archive.js @@ -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"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-new.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-new.js index e1bb8d75fab..5e0ab10824b 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-new.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-new.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-sent.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-sent.js index a4c68b3a015..0c318870ac6 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-sent.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-sent.js @@ -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"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-unread.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-unread.js index 24dd211d32e..689fff4687c 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-unread.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal-unread.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal.js index 7a13e5d5211..bedfe937aba 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-personal.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-personal.js @@ -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"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-sent.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-sent.js index 6ed2c2d14be..d7a69c493ca 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-sent.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-sent.js @@ -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"); diff --git a/app/assets/javascripts/discourse/app/routes/user-private-messages-unread.js b/app/assets/javascripts/discourse/app/routes/user-private-messages-unread.js index 29b1aba2a8e..d8919152dbf 100644 --- a/app/assets/javascripts/discourse/app/routes/user-private-messages-unread.js +++ b/app/assets/javascripts/discourse/app/routes/user-private-messages-unread.js @@ -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 ); diff --git a/app/assets/javascripts/discourse/app/templates/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/app/templates/components/basic-topic-list.hbs index a715b2268b5..bd33f879ecd 100644 --- a/app/assets/javascripts/discourse/app/templates/components/basic-topic-list.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/basic-topic-list.hbs @@ -1,12 +1,4 @@ {{#conditional-loading-spinner condition=loading}} - {{#if hasIncoming}} -
- - {{count-i18n key="topic_count_" suffix="latest" count=incomingCount}} - -
- {{/if}} - {{#if topics}} {{topic-list showPosters=showPosters diff --git a/app/assets/javascripts/discourse/app/templates/user-topics-list.hbs b/app/assets/javascripts/discourse/app/templates/user-topics-list.hbs index e7f6945a3ac..e6b0a5d237c 100644 --- a/app/assets/javascripts/discourse/app/templates/user-topics-list.hbs +++ b/app/assets/javascripts/discourse/app/templates/user-topics-list.hbs @@ -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}} +
+ + {{count-i18n key="topic_count_" suffix="latest" count=incomingCount}} + +
+ {{/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}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js index b73c982025c..32e00a8cfaf 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js @@ -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) { diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 150c56e7570..a8a713cfcfb 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 61c78af70f4..380e3b00fa5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 44b821dd57c..250c435291b 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -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) diff --git a/lib/topic_query/private_message_lists.rb b/lib/topic_query/private_message_lists.rb index 28534735ada..a41015fecae 100644 --- a/lib/topic_query/private_message_lists.rb +++ b/lib/topic_query/private_message_lists.rb @@ -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) diff --git a/lib/topics_bulk_action.rb b/lib/topics_bulk_action.rb index ad19ac1bcf3..e0a82daa4c8 100644 --- a/lib/topics_bulk_action.rb +++ b/lib/topics_bulk_action.rb @@ -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| { diff --git a/spec/components/topics_bulk_action_spec.rb b/spec/components/topics_bulk_action_spec.rb index 169af2603d0..1cf4a93dbee 100644 --- a/spec/components/topics_bulk_action_spec.rb +++ b/spec/components/topics_bulk_action_spec.rb @@ -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 diff --git a/spec/lib/topic_query/private_message_lists_spec.rb b/spec/lib/topic_query/private_message_lists_spec.rb index 8d37b1d17dd..7ddbd175bc9 100644 --- a/spec/lib/topic_query/private_message_lists_spec.rb +++ b/spec/lib/topic_query/private_message_lists_spec.rb @@ -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 diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 434d83094a1..2a5f2f6e325 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -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