FEATURE: New and Unread messages for user personal messages. (#13603)

* FEATURE: New and Unread messages for user personal messages.

Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
This commit is contained in:
Alan Guo Xiang Tan 2021-08-02 12:41:41 +08:00 committed by GitHub
parent fe3e18f981
commit 016efeadf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1274 additions and 431 deletions

View File

@ -154,10 +154,14 @@ export default Controller.extend(bufferedProperty("model"), {
showCategoryChooser: not("model.isPrivateMessage"), showCategoryChooser: not("model.isPrivateMessage"),
gotoInbox(name) { gotoInbox(name) {
let url = userPath(this.get("currentUser.username_lower") + "/messages"); let url = userPath(`${this.get("currentUser.username_lower")}/messages`);
if (name) { if (name) {
url = url + "/group/" + name; url = `${url}/group/${name}`;
} else {
url = `${url}/personal`;
} }
DiscourseURL.routeTo(url); DiscourseURL.routeTo(url);
}, },

View File

@ -3,6 +3,10 @@ import { action } from "@ember/object";
import { alias, and, equal } from "@ember/object/computed"; import { alias, and, equal } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { VIEW_NAME_WARNINGS } from "discourse/routes/user-private-messages-warnings"; import { VIEW_NAME_WARNINGS } from "discourse/routes/user-private-messages-warnings";
import I18n from "I18n";
export const PERSONAL_INBOX = "__personal_inbox__";
const ALL_INBOX = "__all_inbox__";
export default Controller.extend({ export default Controller.extend({
user: controller(), user: controller(),
@ -10,19 +14,101 @@ export default Controller.extend({
pmView: false, pmView: false,
viewingSelf: alias("user.viewingSelf"), viewingSelf: alias("user.viewingSelf"),
isGroup: equal("pmView", "groups"), isGroup: equal("pmView", "groups"),
group: null,
groupFilter: alias("group.name"),
currentPath: alias("router._router.currentPath"), currentPath: alias("router._router.currentPath"),
pmTaggingEnabled: alias("site.can_tag_pms"), pmTaggingEnabled: alias("site.can_tag_pms"),
tagId: null, tagId: null,
showNewPM: and("user.viewingSelf", "currentUser.can_send_private_messages"), showNewPM: and("user.viewingSelf", "currentUser.can_send_private_messages"),
@discourseComputed("inboxes", "isAllInbox")
displayGlobalFilters(inboxes, isAllInbox) {
if (inboxes.length === 0) {
return true;
}
if (inboxes.length && isAllInbox) {
return true;
}
return false;
},
@discourseComputed("inboxes")
sectionClass(inboxes) {
const defaultClass = "user-secondary-navigation user-messages";
return inboxes.length
? `${defaultClass} user-messages-inboxes`
: defaultClass;
},
@discourseComputed("pmView")
isPersonalInbox(pmView) {
return pmView && pmView.startsWith("personal");
},
@discourseComputed("isPersonalInbox", "group.name")
isAllInbox(isPersonalInbox, groupName) {
return !this.isPersonalInbox && !groupName;
},
@discourseComputed("isPersonalInbox", "group.name")
selectedInbox(isPersonalInbox, groupName) {
if (groupName) {
return groupName;
}
return isPersonalInbox ? PERSONAL_INBOX : ALL_INBOX;
},
@discourseComputed("viewingSelf", "pmView", "currentUser.admin") @discourseComputed("viewingSelf", "pmView", "currentUser.admin")
showWarningsWarning(viewingSelf, pmView, isAdmin) { showWarningsWarning(viewingSelf, pmView, isAdmin) {
return pmView === VIEW_NAME_WARNINGS && !viewingSelf && !isAdmin; return pmView === VIEW_NAME_WARNINGS && !viewingSelf && !isAdmin;
}, },
@discourseComputed("model.groups")
inboxes(groups) {
const groupsWithMessages = groups?.filter((group) => {
return group.has_messages;
});
if (!groupsWithMessages || groupsWithMessages.length === 0) {
return [];
}
const inboxes = [];
inboxes.push({
id: ALL_INBOX,
name: I18n.t("user.messages.all"),
});
inboxes.push({
id: PERSONAL_INBOX,
name: I18n.t("user.messages.personal"),
icon: "envelope",
});
groupsWithMessages.forEach((group) => {
inboxes.push({ id: group.name, name: group.name, icon: "users" });
});
return inboxes;
},
@action @action
changeGroupNotificationLevel(notificationLevel) { changeGroupNotificationLevel(notificationLevel) {
this.group.setNotification(notificationLevel, this.get("user.model.id")); this.group.setNotification(notificationLevel, this.get("user.model.id"));
}, },
@action
updateInbox(inbox) {
if (inbox === ALL_INBOX) {
this.transitionToRoute("userPrivateMessages.index");
} else if (inbox === PERSONAL_INBOX) {
this.transitionToRoute("userPrivateMessages.personal");
} else {
this.transitionToRoute("userPrivateMessages.group", inbox);
}
},
}); });

View File

@ -1,5 +1,6 @@
export function findOrResetCachedTopicList(session, filter) { export function findOrResetCachedTopicList(session, filter) {
const lastTopicList = session.get("topicList"); const lastTopicList = session.get("topicList");
if (lastTopicList && lastTopicList.filter === filter) { if (lastTopicList && lastTopicList.filter === filter) {
return lastTopicList; return lastTopicList;
} else { } else {

View File

@ -140,11 +140,20 @@ export default function () {
"userPrivateMessages", "userPrivateMessages",
{ path: "/messages", resetNamespace: true }, { path: "/messages", resetNamespace: true },
function () { function () {
this.route("sent"); this.route("new");
this.route("unread");
this.route("archive"); this.route("archive");
this.route("sent");
this.route("personal");
this.route("personalSent", { path: "personal/sent" });
this.route("personalNew", { path: "personal/new" });
this.route("personalUnread", { path: "personal/unread" });
this.route("personalArchive", { path: "personal/archive" });
this.route("warnings"); this.route("warnings");
this.route("group", { path: "group/:name" }); this.route("group", { path: "group/:name" });
this.route("groupArchive", { path: "group/:name/archive" }); this.route("groupArchive", { path: "group/:name/archive" });
this.route("groupNew", { path: "group/:name/new" });
this.route("groupUnread", { path: "group/:name/unread" });
this.route("tags"); this.route("tags");
this.route("tagsShow", { path: "tags/:id" }); this.route("tagsShow", { path: "tags/:id" });
} }

View File

@ -0,0 +1,68 @@
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({
groupName: null,
titleToken() {
const groupName = this.groupName;
if (groupName) {
let title = groupName.capitalize();
if (viewName !== "index") {
title = `${title} ${I18n.t("user.messages." + viewName)}`;
}
return [title, I18n.t(`user.private_messages`)];
}
},
model(params) {
const username = this.modelFor("user").get("username_lower");
let filter = `topics/private-messages-group/${username}/${params.name}`;
if (viewName !== "index") {
filter = `${filter}/${viewName}`;
}
const lastTopicList = findOrResetCachedTopicList(this.session, filter);
return lastTopicList
? lastTopicList
: this.store.findFiltered("topicList", { filter });
},
afterModel(model) {
const filters = model.get("filter").split("/");
let groupName;
if (viewName !== "index") {
groupName = filters[filters.length - 2];
} else {
groupName = filters.pop();
}
const group = this.modelFor("user")
.get("groups")
.filterBy("name", groupName)[0];
this.setProperties({ groupName: groupName, group });
},
setupController() {
this._super.apply(this, arguments);
this.controllerFor("user-private-messages").set("group", this.group);
if (channel) {
this.controllerFor("user-topics-list").subscribe(
`/private-messages/group/${this.get(
"groupName"
).toLowerCase()}/${channel}`
);
}
},
});
};

View File

@ -23,7 +23,9 @@ export default (viewName, path, channel) => {
model() { model() {
const filter = const filter =
"topics/" + path + "/" + this.modelFor("user").get("username_lower"); "topics/" + path + "/" + this.modelFor("user").get("username_lower");
const lastTopicList = findOrResetCachedTopicList(this.session, filter); const lastTopicList = findOrResetCachedTopicList(this.session, filter);
return lastTopicList return lastTopicList
? lastTopicList ? lastTopicList
: this.store.findFiltered("topicList", { filter }); : this.store.findFiltered("topicList", { filter });
@ -49,6 +51,7 @@ export default (viewName, path, channel) => {
this.controllerFor("user-private-messages").setProperties({ this.controllerFor("user-private-messages").setProperties({
archive: false, archive: false,
pmView: viewName, pmView: viewName,
group: null,
}); });
this.searchService.set("contextType", "private_messages"); this.searchService.set("contextType", "private_messages");

View File

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

View File

@ -1,48 +1,3 @@
import I18n from "I18n"; import createPMRoute from "discourse/routes/build-private-messages-group-route";
import createPMRoute from "discourse/routes/build-private-messages-route";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
export default createPMRoute("groups", "private-messages-groups").extend({ export default createPMRoute("archive", "archive");
groupName: null,
titleToken() {
const groupName = this.groupName;
if (groupName) {
return [
`${groupName.capitalize()} ${I18n.t("user.messages.archive")}`,
I18n.t("user.private_messages"),
];
}
},
model(params) {
const username = this.modelFor("user").get("username_lower");
const filter = `topics/private-messages-group/${username}/${params.name}/archive`;
const lastTopicList = findOrResetCachedTopicList(this.session, filter);
return lastTopicList
? lastTopicList
: this.store.findFiltered("topicList", { filter });
},
afterModel(model) {
const split = model.get("filter").split("/");
const groupName = split[split.length - 2];
this.set("groupName", groupName);
const group = this.modelFor("user")
.get("groups")
.filterBy("name", groupName)[0];
this.controllerFor("user-private-messages").set("group", group);
},
setupController(controller, model) {
this._super.apply(this, arguments);
const split = model.get("filter").split("/");
const group = split[split.length - 2];
this.controllerFor("user-private-messages").set("groupFilter", group);
this.controllerFor("user-private-messages").set("archive", true);
this.controllerFor("user-topics-list").subscribe(
`/private-messages/group/${group}/archive`
);
},
});

View File

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

View File

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

View File

@ -1,42 +1,3 @@
import I18n from "I18n"; import createPMRoute from "discourse/routes/build-private-messages-group-route";
import createPMRoute from "discourse/routes/build-private-messages-route";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
export default createPMRoute("groups", "private-messages-groups").extend({ export default createPMRoute("index", "inbox");
groupName: null,
titleToken() {
const groupName = this.groupName;
if (groupName) {
return [groupName.capitalize(), I18n.t("user.private_messages")];
}
},
model(params) {
const username = this.modelFor("user").get("username_lower");
const filter = `topics/private-messages-group/${username}/${params.name}`;
const lastTopicList = findOrResetCachedTopicList(this.session, filter);
return lastTopicList
? lastTopicList
: this.store.findFiltered("topicList", { filter });
},
afterModel(model) {
const groupName = model.get("filter").split("/").pop();
this.set("groupName", groupName);
const group = this.modelFor("user")
.get("groups")
.filterBy("name", groupName)[0];
this.controllerFor("user-private-messages").set("group", group);
},
setupController(controller, model) {
this._super.apply(this, arguments);
const group = model.get("filter").split("/").pop();
this.controllerFor("user-private-messages").set("groupFilter", group);
this.controllerFor("user-private-messages").set("archive", false);
this.controllerFor("user-topics-list").subscribe(
`/private-messages/group/${group}`
);
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,11 @@ export default DiscourseRoute.extend({
}, },
setupController(controller, user) { setupController(controller, user) {
const composerController = this.controllerFor("composer");
controller.set("model", user); controller.set("model", user);
if (this.currentUser) { if (this.currentUser) {
const composerController = this.controllerFor("composer");
Draft.get("new_private_message").then((data) => { Draft.get("new_private_message").then((data) => {
if (data.draft) { if (data.draft) {
composerController.open({ composerController.open({

View File

@ -1,14 +1,29 @@
{{#d-section class="user-secondary-navigation" pageClass="user-messages"}} {{#d-section class=sectionClass pageClass="user-messages"}}
{{#unless site.mobileView}} {{#if inboxes.length}}
{{#if showNewPM}} <div class="inboxes-controls">
{{d-button class="btn-primary new-private-message" action=(route-action "composePrivateMessage") icon="envelope" label="user.new_private_message"}} {{combo-box
content=inboxes
classNames="user-messages-inboxes-drop"
value=selectedInbox
onChange=(action "updateInbox")
options=(hash
filterable=true
)
}}
{{#if (and group site.mobileView)}}
{{group-notifications-button
value=group.group_user.notification_level
onChange=(action "changeGroupNotificationLevel")
}}
{{/if}}
</div>
{{/if}} {{/if}}
{{/unless}}
{{#mobile-nav class="messages-nav" desktopClass="nav-stacked action-list"}} {{#mobile-nav class="messages-nav" desktopClass="nav-stacked action-list"}}
{{#if isAllInbox}}
<li class="noGlyph"> <li class="noGlyph">
{{#link-to "userPrivateMessages.index" model}} {{#link-to "userPrivateMessages.index" model}}
{{i18n "user.messages.inbox"}} {{i18n "user.messages.latest"}}
{{/link-to}} {{/link-to}}
</li> </li>
<li class="noGlyph"> <li class="noGlyph">
@ -16,34 +31,81 @@
{{i18n "user.messages.sent"}} {{i18n "user.messages.sent"}}
{{/link-to}} {{/link-to}}
</li> </li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.new" model}}
{{i18n "user.messages.new"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.unread" model}}
{{i18n "user.messages.unread"}}
{{/link-to}}
</li>
<li class="noGlyph"> <li class="noGlyph">
{{#link-to "userPrivateMessages.archive" model}} {{#link-to "userPrivateMessages.archive" model}}
{{i18n "user.messages.archive"}} {{i18n "user.messages.archive"}}
{{/link-to}} {{/link-to}}
</li> </li>
{{plugin-outlet name="user-messages-nav" connectorTagName="li" args=(hash model=model)}} {{/if}}
{{#each model.groups as |group|}}
{{#if group.has_messages}} {{#if group}}
<li> <li class="noGlyph">
{{#link-to "userPrivateMessages.group" group.name}} {{#link-to "userPrivateMessages.group" group.name}}
{{d-icon "users"}} {{i18n "user.messages.latest"}}
{{capitalize-string group.name}}
{{/link-to}} {{/link-to}}
</li> </li>
<li class="archive"> <li class="noGlyph">
{{#link-to "userPrivateMessages.groupNew" group.name}}
{{i18n "user.messages.new"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.groupUnread" group.name}}
{{i18n "user.messages.unread"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.groupArchive" group.name}} {{#link-to "userPrivateMessages.groupArchive" group.name}}
{{i18n "user.messages.archive"}} {{i18n "user.messages.archive"}}
{{/link-to}} {{/link-to}}
</li> </li>
{{/if}} {{/if}}
{{/each}}
{{#if pmTaggingEnabled}} {{#if isPersonalInbox}}
<li class="noGlyph"> <li class="noGlyph">
{{#link-to "userPrivateMessages.personal" model}}
{{i18n "user.messages.latest"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.personalSent" model}}
{{i18n "user.messages.sent"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.personalNew" model}}
{{i18n "user.messages.new"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.personalUnread" model}}
{{i18n "user.messages.unread"}}
{{/link-to}}
</li>
<li class="noGlyph">
{{#link-to "userPrivateMessages.personalArchive" model}}
{{i18n "user.messages.archive"}}
{{/link-to}}
</li>
{{/if}}
{{#if displayGlobalFilters}}
{{#if pmTaggingEnabled}}
<li class="noGlyph tags">
{{#link-to "userPrivateMessages.tags" model}} {{#link-to "userPrivateMessages.tags" model}}
{{i18n "user.messages.tags"}} {{i18n "user.messages.tags"}}
{{/link-to}} {{/link-to}}
</li>
{{#if tagId}} {{#if tagId}}
<li class="archive"> <li class="archive">
{{#link-to "userPrivateMessages.tagsShow" tagId}} {{#link-to "userPrivateMessages.tagsShow" tagId}}
@ -51,31 +113,36 @@
{{/link-to}} {{/link-to}}
</li> </li>
{{/if}} {{/if}}
</li>
{{/if}}
{{plugin-outlet name="user-messages-nav" connectorTagName="li" args=(hash model=model)}}
{{/if}} {{/if}}
{{/mobile-nav}} {{/mobile-nav}}
{{/d-section}} {{/d-section}}
<section class="user-content"> {{#if (and site.mobileView showNewPM)}}
<div class="list-actions"> {{d-button class="btn-primary new-private-message" action=(route-action "composePrivateMessage") icon="envelope" label="user.new_private_message"}}
{{#if site.mobileView}} {{/if}}
{{#if showNewPM}}
{{d-button
class="btn-primary new-private-message"
action=(route-action "composePrivateMessage")
icon="envelope"
label="user.new_private_message"}}
{{/if}}
{{/if}}
{{#if isGroup}} {{#unless site.mobileView}}
<section class="user-additional-controls">
{{#if group}}
{{group-notifications-button {{group-notifications-button
value=group.group_user.notification_level value=group.group_user.notification_level
onChange=(action "changeGroupNotificationLevel") onChange=(action "changeGroupNotificationLevel")
}} }}
{{/if}} {{/if}}
</div> {{#if showNewPM}}
{{d-button class="btn-primary new-private-message" action=(route-action "composePrivateMessage") icon="envelope" label="user.new_private_message"}}
{{/if}}
</section>
{{/unless}}
<section class="user-content">
{{#if showWarningsWarning}} {{#if showWarningsWarning}}
<div class="alert alert-info">{{html-safe (i18n "admin.user.warnings_list_warning")}}</div> <div class="alert alert-info">{{html-safe (i18n "admin.user.warnings_list_warning")}}</div>
{{/if}} {{/if}}
{{outlet}} {{outlet}}
</section> </section>

View File

@ -0,0 +1,131 @@
import {
acceptance,
count,
exists,
} from "discourse/tests/helpers/qunit-helpers";
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import { PERSONAL_INBOX } from "discourse/controllers/user-private-messages";
acceptance(
"User Private Messages - user with no group messages",
function (needs) {
needs.user();
needs.site({
can_tag_pms: true,
});
test("viewing messages", async function (assert) {
await visit("/u/eviltrout/messages");
assert.equal(count(".topic-list-item"), 1, "displays the topic list");
assert.ok(
!exists(".user-messages-inboxes-drop"),
"does not display inboxes dropdown"
);
assert.ok(exists(".messages-nav .tags"), "displays the tags filter");
assert.ok(
!exists(".group-notifications-button"),
"displays the group notifications button"
);
});
}
);
acceptance(
"User Private Messages - user with group messages",
function (needs) {
needs.user();
needs.site({
can_tag_pms: true,
});
needs.pretender((server, helper) => {
server.get("/topics/private-messages-all/:username.json", () => {
return helper.response({
topic_list: {
topics: [
{ id: 1, posters: [] },
{ id: 2, posters: [] },
{ id: 3, posters: [] },
],
},
});
});
server.get(
"/topics/private-messages-group/:username/:group_name.json",
() => {
return helper.response({
topic_list: {
topics: [
{ id: 1, posters: [] },
{ id: 2, posters: [] },
],
},
});
}
);
});
test("viewing messages", async function (assert) {
await visit("/u/charlie/messages");
assert.equal(
count(".topic-list-item"),
3,
"displays the right topic list"
);
assert.ok(
exists(".user-messages-inboxes-drop"),
"displays inboxes dropdown"
);
assert.ok(exists(".messages-nav .tags"), "displays the tags filter");
await selectKit(".user-messages-inboxes-drop").expand();
await selectKit(".user-messages-inboxes-drop").selectRowByValue(
PERSONAL_INBOX
);
assert.equal(
count(".topic-list-item"),
1,
"displays the right topic list"
);
assert.ok(
!exists(".messages-nav .tags"),
"does not display the tags filter"
);
await selectKit(".user-messages-inboxes-drop").expand();
await selectKit(".user-messages-inboxes-drop").selectRowByValue(
"awesome_group"
);
assert.equal(
count(".topic-list-item"),
2,
"displays the right topic list"
);
assert.ok(
exists(".group-notifications-button"),
"displays the group notifications button"
);
assert.ok(
!exists(".messages-nav .tags"),
"does not display the tags filter"
);
});
}
);

View File

@ -38,11 +38,6 @@ acceptance("User Routes", function (needs) {
assert.ok($("body.user-invites-page").length, "has the body class"); assert.ok($("body.user-invites-page").length, "has the body class");
}); });
test("Messages", async function (assert) {
await visit("/u/eviltrout/messages");
assert.ok($("body.user-messages-page").length, "has the body class");
});
test("Notifications", async function (assert) { test("Notifications", async function (assert) {
await visit("/u/eviltrout/notifications"); await visit("/u/eviltrout/notifications");
assert.ok($("body.user-notifications-page").length, "has the body class"); assert.ok($("body.user-notifications-page").length, "has the body class");

View File

@ -2648,6 +2648,33 @@ export default {
default_notification_level: 3, default_notification_level: 3,
membership_request_template: null, membership_request_template: null,
}, },
{
id: 14,
automatic: false,
name: "awesome_group",
display_name: "awesome_group",
user_count: 3,
mentionable_level: 0,
messageable_level: 0,
visibility_level: 0,
automatic_membership_email_domains: null,
primary_group: false,
title: null,
grant_trust_level: null,
incoming_email: null,
has_messages: true,
flair_url: null,
flair_bg_color: null,
flair_color: null,
bio_raw: null,
bio_cooked: null,
public_admission: false,
public_exit: false,
allow_membership_requests: false,
full_name: null,
default_notification_level: 3,
membership_request_template: null,
},
], ],
group_users: [ group_users: [
{ group_id: 10, user_id: 5, notification_level: 3 }, { group_id: 10, user_id: 5, notification_level: 3 },

View File

@ -207,12 +207,14 @@ export function applyDefaultHandlers(pretender) {
}); });
}); });
pretender.get("/topics/private-messages/eviltrout.json", () => { [
"/topics/private-messages-all/:username.json",
"/topics/private-messages/:username.json",
"/topics/private-messages-warnings/eviltrout.json",
].forEach((url) => {
pretender.get(url, () => {
return response(fixturesByUrl["/topics/private-messages/eviltrout.json"]); return response(fixturesByUrl["/topics/private-messages/eviltrout.json"]);
}); });
pretender.get("/topics/private-messages-warnings/eviltrout.json", () => {
return response(fixturesByUrl["/topics/private-messages/eviltrout.json"]);
}); });
pretender.get("/topics/feature_stats.json", () => { pretender.get("/topics/feature_stats.json", () => {

View File

@ -31,6 +31,7 @@
.user-content { .user-content {
min-width: 100%; min-width: 100%;
} }
.user-additional-controls + .user-content, .user-additional-controls + .user-content,
.user-secondary-navigation + .user-content { .user-secondary-navigation + .user-content {
grid-column-start: 2; grid-column-start: 2;

View File

@ -66,12 +66,10 @@
.nav-stacked { .nav-stacked {
@extend %nav; @extend %nav;
padding: 0; padding: 0;
overflow: hidden;
background: var(--primary-low); background: var(--primary-low);
li { li {
border-bottom: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low);
position: relative;
&:last-of-type { &:last-of-type {
border-bottom: 0; border-bottom: 0;
@ -89,6 +87,7 @@
line-height: $line-height-small; line-height: $line-height-small;
cursor: pointer; cursor: pointer;
color: var(--primary); color: var(--primary);
@include ellipsis;
&.active { &.active {
color: var(--secondary); color: var(--secondary);

View File

@ -6,10 +6,6 @@
margin-top: 10px; margin-top: 10px;
} }
} }
.show-mores {
position: absolute;
}
} }
.form-horizontal .control-group.category { .form-horizontal .control-group.category {
@ -20,10 +16,20 @@
font-size: 1.5em; font-size: 1.5em;
text-align: center; text-align: center;
} }
.user-secondary-navigation { .user-secondary-navigation {
min-width: 150px; min-width: 150px;
.combo-box {
width: 100%;
&:not(:last-of-type) {
margin-bottom: 0.875em;
}
}
.nav-stacked { .nav-stacked {
background-color: transparent; background-color: transparent;
margin: 0;
li { li {
border-bottom: none; border-bottom: none;
@ -47,6 +53,42 @@
} }
} }
} }
.select-kit + .messages-nav {
margin-top: 1em;
}
.inboxes-controls {
margin-bottom: 0.75em;
}
&.user-messages {
--left-padding: 0.8em;
.user-messages-inboxes-drop {
padding: 0 1em 0 0;
.select-kit-header {
padding-left: var(--left-padding);
}
.select-kit-selected-name {
overflow: hidden;
}
}
.nav-stacked {
a {
padding-left: calc(
var(--left-padding) - 1px
); // 1px accounts for border on select-kit elements above
}
}
}
}
.user-additional-controls {
button {
margin-bottom: 1em;
}
} }
.user-content { .user-content {
@ -226,6 +268,20 @@ table.user-invite-list {
} }
} }
.user-messages-page {
.topic-list th {
padding-top: 4px;
}
.show-mores {
position: absolute;
}
}
.user-messages {
margin-right: 0.2em;
}
.user-preferences { .user-preferences {
padding-top: 10px; padding-top: 10px;
padding-left: 30px; padding-left: 30px;

View File

@ -4,8 +4,7 @@
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto; grid-template-rows: auto auto auto;
grid-row-gap: 20px; grid-gap: 16px;
grid-column-gap: 16px;
.user-primary-navigation { .user-primary-navigation {
grid-column-start: 1; grid-column-start: 1;
grid-row-start: 1; grid-row-start: 1;
@ -30,6 +29,71 @@
grid-row-start: 3; grid-row-start: 3;
grid-column-start: 1; grid-column-start: 1;
} }
// specific to messages
.user-messages.user-messages-inboxes {
grid-row-start: 2;
grid-column-start: 1;
grid-column-end: 3;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
+ .user-additional-controls {
grid-row-start: 2;
grid-column-start: 1;
}
}
.inboxes-controls {
display: flex;
}
.user-messages-inboxes-drop {
padding: 0;
flex: 1 1 auto;
.select-kit-header {
padding: 8px 10px;
.caret-icon {
color: var(--primary-medium);
}
}
}
.messages-nav {
grid-column-start: 2;
grid-column-end: 3;
grid-row-start: 1;
}
.new-private-message {
grid-row-start: 1;
grid-column-start: 2;
}
.group-notifications-button {
margin-left: 8px;
.select-kit-header {
height: 100%;
.selected-name .name {
display: none;
}
}
}
}
.user-messages-page {
.paginated-topics-list {
margin-top: 0;
}
.show-mores {
margin-top: 0.5em;
}
} }
.user-main { .user-main {
@ -166,6 +230,10 @@
flex: 1 1 25%; flex: 1 1 25%;
margin-left: auto; margin-left: auto;
.btn {
margin-bottom: 16px;
}
ul { ul {
margin: 0; margin: 0;
display: flex; display: flex;
@ -223,6 +291,7 @@
.user-main .collapsed-info.about .details { .user-main .collapsed-info.about .details {
display: flex; display: flex;
margin-bottom: 16px;
.user-profile-avatar { .user-profile-avatar {
margin: 0; margin: 0;
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -146,35 +146,27 @@ class ListController < ApplicationController
end end
def self.generate_message_route(action) def self.generate_message_route(action)
case action define_method action do
when :private_messages_tag
define_method("#{action}") do
raise Discourse::NotFound if !guardian.can_tag_pms?
message_route(action) message_route(action)
end end
when :private_messages_group, :private_messages_group_archive
define_method("#{action}") do
group = Group.find_by("LOWER(name) = ?", params[:group_name].downcase)
raise Discourse::NotFound if !group
raise Discourse::NotFound unless guardian.can_see_group_messages?(group)
message_route(action)
end
else
define_method("#{action}") do
message_route(action)
end
end
end end
def message_route(action) def message_route(action)
target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) }, [:user_stat, :user_option]) target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) }, [:user_stat, :user_option])
case action case action
when :private_messages_tag
raise Discourse::NotFound if !guardian.can_tag_pms?
when :private_messages_warnings when :private_messages_warnings
guardian.ensure_can_see_warnings!(target_user) guardian.ensure_can_see_warnings!(target_user)
when :private_messages_group, :private_messages_group_archive
group = Group.find_by("LOWER(name) = ?", params[:group_name].downcase)
raise Discourse::NotFound if !group
raise Discourse::NotFound unless guardian.can_see_group_messages?(group)
else else
guardian.ensure_can_see_private_messages!(target_user.id) guardian.ensure_can_see_private_messages!(target_user.id)
end end
list_opts = build_topic_list_options list_opts = build_topic_list_options
list = generate_list_for(action.to_s, target_user, list_opts) list = generate_list_for(action.to_s, target_user, list_opts)
url_prefix = "topics" url_prefix = "topics"
@ -187,11 +179,19 @@ class ListController < ApplicationController
private_messages private_messages
private_messages_sent private_messages_sent
private_messages_unread private_messages_unread
private_messages_new
private_messages_archive private_messages_archive
private_messages_group private_messages_group
private_messages_group_new
private_messages_group_unread
private_messages_group_archive private_messages_group_archive
private_messages_tag
private_messages_warnings private_messages_warnings
private_messages_all
private_messages_all_sent
private_messages_all_unread
private_messages_all_new
private_messages_all_archive
private_messages_tag
}.each do |action| }.each do |action|
generate_message_route(action) generate_message_route(action)
end end

View File

@ -136,16 +136,16 @@ class Tag < ActiveRecord::Base
WHERE topic_tags.topic_id IN ( WHERE topic_tags.topic_id IN (
SELECT topic_id SELECT topic_id
FROM topic_allowed_users FROM topic_allowed_users
WHERE user_id = #{user_id} WHERE user_id = #{user_id.to_i}
UNION UNION
SELECT tg.topic_id SELECT tg.topic_id
FROM topic_allowed_groups tg FROM topic_allowed_groups tg
JOIN group_users gu ON gu.user_id = #{user_id} JOIN group_users gu ON gu.user_id = #{user_id.to_i}
AND gu.group_id = tg.group_id AND gu.group_id = tg.group_id
) )
GROUP BY tags.name GROUP BY tags.name
ORDER BY count DESC ORDER BY count DESC
LIMIT #{limit} LIMIT #{limit.to_i}
SQL SQL
end end

View File

@ -545,8 +545,9 @@ class TopicTrackingState
group_user_ids = group.users.pluck(:id) group_user_ids = group.users.pluck(:id)
next if group_user_ids.blank? next if group_user_ids.blank?
group_channels = [] group_channels = []
group_channels << "/private-messages/group/#{group.name.downcase}" channel_prefix = "/private-messages/group/#{group.name.downcase}"
group_channels << "#{group_channels.first}/archive" if group_archive group_channels << "#{channel_prefix}/inbox"
group_channels << "#{channel_prefix}/archive" if group_archive
group_channels.each { |channel| channels[channel] = group_user_ids } group_channels.each { |channel| channels[channel] = group_user_ids }
end end

View File

@ -1172,9 +1172,13 @@ en:
rejected_posts: "rejected posts" rejected_posts: "rejected posts"
messages: messages:
all: "All" all: "all inboxes"
inbox: "Inbox" inbox: "Inbox"
personal: "Personal"
latest: "Latest"
sent: "Sent" sent: "Sent"
unread: "Unread"
new: "New"
archive: "Archive" archive: "Archive"
groups: "My Groups" groups: "My Groups"
move_to_inbox: "Move to Inbox" move_to_inbox: "Move to Inbox"

View File

@ -447,9 +447,10 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username } get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username } get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username } get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/personal" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/personal/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username } get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username }
get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username } get "#{root_path}/:username/messages/group/:group_name/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username }
get "#{root_path}/:username/messages/tags/:tag_id" => "user_actions#private_messages", constraints: StaffConstraint.new
get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormat.username }, defaults: { format: :json } get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormat.username }, defaults: { format: :json }
get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormat.username } }.merge(index == 1 ? { as: 'user' } : {})) get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormat.username } }.merge(index == 1 ? { as: 'user' } : {}))
put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormat.username }, defaults: { format: :json } put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormat.username }, defaults: { format: :json }
@ -764,17 +765,25 @@ Discourse::Application.routes.draw do
scope "/topics", username: RouteFormat.username do scope "/topics", username: RouteFormat.username do
get "created-by/:username" => "list#topics_by", as: "topics_by", defaults: { format: :json } get "created-by/:username" => "list#topics_by", as: "topics_by", defaults: { format: :json }
get "private-messages-all/:username" => "list#private_messages_all", as: "topics_private_messages_all", defaults: { format: :json }
get "private-messages-all-sent/:username" => "list#private_messages_all_sent", as: "topics_private_messages_all_sent", defaults: { format: :json }
get "private-messages-all-new/:username" => "list#private_messages_all_new", as: "topics_private_messages_all_new", defaults: { format: :json }
get "private-messages-all-unread/:username" => "list#private_messages_all_unread", as: "topics_private_messages_all_unread", defaults: { format: :json }
get "private-messages-all-archive/:username" => "list#private_messages_all_archive", as: "topics_private_messages_all_archive", defaults: { format: :json }
get "private-messages/:username" => "list#private_messages", as: "topics_private_messages", defaults: { format: :json } get "private-messages/:username" => "list#private_messages", as: "topics_private_messages", defaults: { format: :json }
get "private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", defaults: { format: :json } get "private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", defaults: { format: :json }
get "private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", defaults: { format: :json } get "private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", defaults: { format: :json }
get "private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", defaults: { format: :json } get "private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", defaults: { format: :json }
get "private-messages-tags/:username/:tag_id.json" => "list#private_messages_tag", as: "topics_private_messages_tag", defaults: { format: :json } get "private-messages-tags/:username/:tag_id.json" => "list#private_messages_tag", as: "topics_private_messages_tag", defaults: { format: :json }
get "private-messages-new/:username" => "list#private_messages_new", as: "topics_private_messages_new", defaults: { format: :json }
get "private-messages-warnings/:username" => "list#private_messages_warnings", as: "topics_private_messages_warnings", defaults: { format: :json } get "private-messages-warnings/:username" => "list#private_messages_warnings", as: "topics_private_messages_warnings", defaults: { format: :json }
get "groups/:group_name" => "list#group_topics", as: "group_topics", group_name: RouteFormat.username get "groups/:group_name" => "list#group_topics", as: "group_topics", group_name: RouteFormat.username
scope "/private-messages-group/:username", group_name: RouteFormat.username do scope "/private-messages-group/:username", group_name: RouteFormat.username do
get ":group_name.json" => "list#private_messages_group", as: "topics_private_messages_group" get ":group_name.json" => "list#private_messages_group", as: "topics_private_messages_group"
get ":group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive" get ":group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive"
get ":group_name/new.json" => "list#private_messages_group_new", as: "topics_private_messages_group_new"
get ":group_name/unread.json" => "list#private_messages_group_unread", as: "topics_private_messages_group_unread"
end end
end end

View File

@ -6,6 +6,8 @@
# #
class TopicQuery class TopicQuery
include PrivateMessageLists
PG_MAX_INT ||= 2147483647 PG_MAX_INT ||= 2147483647
DEFAULT_PER_PAGE_COUNT ||= 30 DEFAULT_PER_PAGE_COUNT ||= 30
@ -293,12 +295,6 @@ class TopicQuery
end end
end end
def not_archived(list, user)
list.joins("LEFT JOIN user_archived_messages um
ON um.user_id = #{user.id.to_i} AND um.topic_id = topics.id")
.where('um.user_id IS NULL')
end
def list_group_topics(group) def list_group_topics(group)
list = default_results.where(" list = default_results.where("
topics.user_id IN ( topics.user_id IN (
@ -309,79 +305,6 @@ class TopicQuery
create_list(:group_topics, {}, list) create_list(:group_topics, {}, list)
end end
def list_private_messages(user)
list = private_messages_for(user, :user)
list = not_archived(list, user)
.where('NOT (topics.participant_count = 1 AND topics.user_id = ? AND topics.moderator_posts_count = 0)', user.id)
create_list(:private_messages, {}, list)
end
def list_private_messages_archive(user)
list = private_messages_for(user, :user)
list = list.joins(:user_archived_messages).where('user_archived_messages.user_id = ?', user.id)
create_list(:private_messages, {}, list)
end
def list_private_messages_sent(user)
list = private_messages_for(user, :user)
list = list.where('EXISTS (
SELECT 1 FROM posts
WHERE posts.topic_id = topics.id AND
posts.user_id = ?
)', user.id)
list = not_archived(list, user)
create_list(:private_messages, {}, list)
end
def list_private_messages_unread(user)
list = private_messages_for(user, :user)
list = TopicQuery.unread_filter(
list,
staff: user.staff?
)
first_unread_pm_at = UserStat.where(user_id: user.id).pluck_first(:first_unread_pm_at)
list = list.where("topics.updated_at >= ?", first_unread_pm_at) if first_unread_pm_at
create_list(:private_messages, {}, list)
end
def list_private_messages_group(user)
list = private_messages_for(user, :group)
group = Group.where('name ilike ?', @options[:group_name]).select(:id, :publish_read_state).first
publish_read_state = !!group&.publish_read_state
list = list.joins("LEFT JOIN group_archived_messages gm ON gm.topic_id = topics.id AND
gm.group_id = #{group&.id&.to_i}")
list = list.where("gm.id IS NULL")
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_archive(user)
list = private_messages_for(user, :group)
group_id = Group.where('name ilike ?', @options[:group_name]).pluck_first(:id)
list = list.joins("JOIN group_archived_messages gm ON gm.topic_id = topics.id AND
gm.group_id = #{group_id.to_i}")
create_list(:private_messages, {}, list)
end
def list_private_messages_tag(user)
list = private_messages_for(user, :all)
list = list.joins("JOIN topic_tags tt ON tt.topic_id = topics.id
JOIN tags t ON t.id = tt.tag_id AND t.name = '#{@options[:tags][0]}'")
create_list(:private_messages, {}, list)
end
def list_private_messages_warnings(user)
list = private_messages_for(user, :user)
list = list.where('topics.subtype = ?', TopicSubtype.moderator_warning)
# Exclude official warnings that the user created, instead of received
list = list.where('topics.user_id <> ?', user.id)
create_list(:private_messages, {}, list)
end
def list_category_topic_ids(category) def list_category_topic_ids(category)
query = default_results(category: category.id) query = default_results(category: category.id)
pinned_ids = query.where('topics.pinned_at IS NOT NULL AND topics.category_id = ?', category.id).limit(nil).order('pinned_at DESC').pluck(:id) pinned_ids = query.where('topics.pinned_at IS NOT NULL AND topics.category_id = ?', category.id).limit(nil).order('pinned_at DESC').pluck(:id)
@ -590,50 +513,6 @@ class TopicQuery
DEFAULT_PER_PAGE_COUNT DEFAULT_PER_PAGE_COUNT
end end
def private_messages_for(user, type)
options = @options
options.reverse_merge!(per_page: per_page_setting)
result = Topic.includes(:allowed_users)
result = result.includes(:tags) if SiteSetting.tagging_enabled
if type == :group
result = result.joins(
"INNER JOIN topic_allowed_groups tag ON tag.topic_id = topics.id AND tag.group_id IN (SELECT id FROM groups WHERE LOWER(name) = '#{PG::Connection.escape_string(@options[:group_name].downcase)}')"
)
unless user.admin?
result = result.joins("INNER JOIN group_users gu ON gu.group_id = tag.group_id AND gu.user_id = #{user.id.to_i}")
end
elsif type == :user
result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{user.id.to_i})")
elsif type == :all
result = result.where("topics.id IN (
SELECT topic_id
FROM topic_allowed_users
WHERE user_id = #{user.id.to_i}
UNION ALL
SELECT topic_id FROM topic_allowed_groups
WHERE group_id IN (
SELECT group_id FROM group_users WHERE user_id = #{user.id.to_i}
)
)")
end
result = result.joins("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")
.private_messages
result = result.limit(options[:per_page]) unless options[:limit] == false
result = result.visible if options[:visible] || @user.nil? || @user.regular?
if options[:page]
offset = options[:page].to_i * options[:per_page]
result = result.offset(offset) if offset > 0
end
result
end
def apply_shared_drafts(result, category_id, options) def apply_shared_drafts(result, category_id, options)
# PERF: avoid any penalty if there are no shared drafts enabled # PERF: avoid any penalty if there are no shared drafts enabled
@ -955,7 +834,7 @@ class TopicQuery
list list
end end
def remove_muted_tags(list, user, opts = nil) def remove_muted_tags(list, user, opts = {})
if !SiteSetting.tagging_enabled || SiteSetting.remove_muted_tags_from_latest == 'never' if !SiteSetting.tagging_enabled || SiteSetting.remove_muted_tags_from_latest == 'never'
return list return list
end end
@ -1149,18 +1028,4 @@ class TopicQuery
result.order('topics.bumped_at DESC') result.order('topics.bumped_at DESC')
end end
private
def append_read_state(list, group)
group_id = group&.id
return list if group_id.nil?
selected_values = list.select_values.empty? ? ['topics.*'] : list.select_values
selected_values << "COALESCE(tg.last_read_post_number, 0) AS last_read_post_number"
list
.joins("LEFT OUTER JOIN topic_groups tg ON topics.id = tg.topic_id AND tg.group_id = #{group_id}")
.select(*selected_values)
end
end end

View File

@ -0,0 +1,256 @@
# frozen_string_literal: true
class TopicQuery
module PrivateMessageLists
def list_private_messages_all(user)
list = private_messages_for(user, :all)
list = filter_archived(list, user, archived: false)
create_list(:private_messages, {}, list)
end
def list_private_messages_all_sent(user)
list = private_messages_for(user, :all)
list = list.where(<<~SQL, user.id)
EXISTS (
SELECT 1 FROM posts
WHERE posts.topic_id = topics.id AND posts.user_id = ?
)
SQL
list = filter_archived(list, user, archived: false)
create_list(:private_messages, {}, list)
end
def list_private_messages_all_archive(user)
list = private_messages_for(user, :all)
list = filter_archived(list, user, archived: true)
create_list(:private_messages, {}, list)
end
def list_private_messages_all_new(user)
list_private_messages_new(user, :all)
end
def list_private_messages_all_unread(user)
list_private_messages_unread(user, :all)
end
def list_private_messages(user)
list = private_messages_for(user, :user)
list = not_archived(list, user)
create_list(:private_messages, {}, list)
end
def list_private_messages_archive(user)
list = private_messages_for(user, :user)
list = list.joins(:user_archived_messages).where('user_archived_messages.user_id = ?', user.id)
create_list(:private_messages, {}, list)
end
def list_private_messages_sent(user)
list = private_messages_for(user, :user)
list = list.where(<<~SQL, user.id)
EXISTS (
SELECT 1 FROM posts
WHERE posts.topic_id = topics.id AND posts.user_id = ?
)
SQL
list = not_archived(list, user)
create_list(:private_messages, {}, list)
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 = remove_muted_tags(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
create_list(:private_messages, {}, list)
end
def list_private_messages_group(user)
list = private_messages_for(user, :group)
list = list.joins(<<~SQL)
LEFT JOIN group_archived_messages gm
ON gm.topic_id = topics.id AND gm.group_id = #{group.id.to_i}
SQL
list = list.where("gm.id IS NULL")
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_archive(user)
list = private_messages_for(user, :group)
list = list.joins(<<~SQL)
INNER JOIN group_archived_messages gm
ON gm.topic_id = topics.id AND gm.group_id = #{group.id.to_i}
SQL
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_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
)
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
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_warnings(user)
list = private_messages_for(user, :user)
list = list.where('topics.subtype = ?', TopicSubtype.moderator_warning)
# Exclude official warnings that the user created, instead of received
list = list.where('topics.user_id <> ?', user.id)
create_list(:private_messages, {}, list)
end
def private_messages_for(user, type)
options = @options
options.reverse_merge!(per_page: per_page_setting)
result = Topic.includes(:allowed_users)
result = result.includes(:tags) if SiteSetting.tagging_enabled
if type == :group
result = result.joins(
"INNER JOIN topic_allowed_groups tag ON tag.topic_id = topics.id AND tag.group_id IN (SELECT id FROM groups WHERE LOWER(name) = '#{PG::Connection.escape_string(@options[:group_name].downcase)}')"
)
unless user.admin?
result = result.joins("INNER JOIN group_users gu ON gu.group_id = tag.group_id AND gu.user_id = #{user.id.to_i}")
end
elsif type == :user
result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{user.id.to_i})")
elsif type == :all
result = result.where("topics.id IN (
SELECT topic_id
FROM topic_allowed_users
WHERE user_id = #{user.id.to_i}
UNION ALL
SELECT topic_id FROM topic_allowed_groups
WHERE group_id IN (
SELECT group_id FROM group_users WHERE user_id = #{user.id.to_i}
)
)")
end
result = result.joins("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")
.private_messages
result = result.limit(options[:per_page]) unless options[:limit] == false
result = result.visible if options[:visible] || @user.nil? || @user.regular?
if options[:page]
offset = options[:page].to_i * options[:per_page]
result = result.offset(offset) if offset > 0
end
result
end
def list_private_messages_tag(user)
list = private_messages_for(user, :all)
list = list.joins("JOIN topic_tags tt ON tt.topic_id = topics.id
JOIN tags t ON t.id = tt.tag_id AND t.name = '#{@options[:tags][0]}'")
create_list(:private_messages, {}, list)
end
private
def append_read_state(list, group)
group_id = group.id
return list if group_id.nil?
selected_values = list.select_values.empty? ? ['topics.*'] : list.select_values
selected_values << "COALESCE(tg.last_read_post_number, 0) AS last_read_post_number"
list
.joins("LEFT OUTER JOIN topic_groups tg ON topics.id = tg.topic_id AND tg.group_id = #{group_id}")
.select(*selected_values)
end
def filter_archived(list, user, archived: true)
list = list.joins(<<~SQL)
LEFT JOIN group_archived_messages gm ON gm.topic_id = topics.id
LEFT JOIN user_archived_messages um
ON um.user_id = #{user.id.to_i}
AND um.topic_id = topics.id
SQL
list =
if archived
list.where("um.user_id IS NOT NULL OR gm.topic_id IS NOT NULL")
else
list.where("um.user_id IS NULL AND gm.topic_id IS NULL")
end
list
end
def not_archived(list, user)
list.joins("LEFT JOIN user_archived_messages um
ON um.user_id = #{user.id.to_i} AND um.topic_id = topics.id")
.where('um.user_id IS NULL')
end
def group
@group ||= begin
Group
.where('name ilike ?', @options[:group_name])
.select(:id, :publish_read_state)
.first
end
end
end
end

View File

@ -1067,7 +1067,6 @@ describe TopicQuery do
expect(TopicQuery.new(user, tags: [tag.name]).list_private_messages_tag(user).topics).to eq([private_message]) expect(TopicQuery.new(user, tags: [tag.name]).list_private_messages_tag(user).topics).to eq([private_message])
end end
end end
end end
@ -1193,75 +1192,6 @@ describe TopicQuery do
end end
end end
describe '#list_private_messages_group' do
fab!(:group) { Fabricate(:group) }
let!(:group_message) do
Fabricate(:private_message_topic,
allowed_groups: [group],
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: Fabricate(:user)),
]
)
end
before do
group.add(creator)
end
it 'should return the right list for a group user' do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(creator)
.topics
expect(topics).to contain_exactly(group_message)
end
it 'should return the right list for an admin not part of the group' do
group.update!(name: group.name.capitalize)
topics = TopicQuery.new(nil, group_name: group.name.upcase)
.list_private_messages_group(Fabricate(:admin))
.topics
expect(topics).to contain_exactly(group_message)
end
it "should not allow a moderator not part of the group to view the group's messages" do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(Fabricate(:moderator))
.topics
expect(topics).to eq([])
end
it "should not allow a user not part of the group to view the group's messages" do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(Fabricate(:user))
.topics
expect(topics).to eq([])
end
context "Calculating minimum unread count for a topic" do
before { group.update!(publish_read_state: true) }
let(:listed_message) do
TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(creator)
.topics.first
end
it 'returns the last read post number' do
topic_group = TopicGroup.create!(
topic: group_message, group: group, last_read_post_number: 10
)
expect(listed_message.last_read_post_number).to eq(topic_group.last_read_post_number)
end
end
end
context "shared drafts" do context "shared drafts" do
fab!(:category) { Fabricate(:category_with_definition) } fab!(:category) { Fabricate(:category_with_definition) }
fab!(:shared_drafts_category) { Fabricate(:category_with_definition) } fab!(:shared_drafts_category) { Fabricate(:category_with_definition) }
@ -1349,16 +1279,4 @@ describe TopicQuery do
end end
end end
end end
describe '#list_private_messages' do
it "includes topics with moderator posts" do
private_message_topic = Fabricate(:private_message_post, user: user).topic
expect(TopicQuery.new(user).list_private_messages(user).topics).to be_empty
private_message_topic.add_moderator_post(admin, "Thank you for your flag")
expect(TopicQuery.new(user).list_private_messages(user).topics).to eq([private_message_topic])
end
end
end end

View File

@ -0,0 +1,305 @@
# frozen_string_literal: true
require 'rails_helper'
describe TopicQuery::PrivateMessageLists do
fab!(:user) { Fabricate(:user) }
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
describe '#list_private_messages_all' do
it 'returns a list of all private messages that a user has access to' do
topics = TopicQuery.new(nil).list_private_messages_all(user).topics
expect(topics).to contain_exactly(group_message, private_message)
end
it 'does not include user or group archived messages' do
UserArchivedMessage.archive!(user.id, group_message)
UserArchivedMessage.archive!(user.id, private_message)
topics = TopicQuery.new(nil).list_private_messages_all(user).topics
expect(topics).to eq([])
GroupArchivedMessage.archive!(user_2.id, group_message)
topics = TopicQuery.new(nil).list_private_messages_all(user_2).topics
expect(topics).to contain_exactly(private_message)
end
end
describe '#list_private_messages_all_sent' do
it 'returns a list of all private messages that a user has sent' do
topics = TopicQuery.new(nil).list_private_messages_all_sent(user_2).topics
expect(topics).to eq([])
create_post(user: user_2, topic: private_message)
topics = TopicQuery.new(nil).list_private_messages_all_sent(user_2).topics
expect(topics).to contain_exactly(private_message)
create_post(user: user_2, topic: group_message)
topics = TopicQuery.new(nil).list_private_messages_all_sent(user_2).topics
expect(topics).to contain_exactly(private_message, group_message)
end
it 'does not include user or group archived messages' do
create_post(user: user_2, topic: private_message)
create_post(user: user_2, topic: group_message)
UserArchivedMessage.archive!(user_2.id, private_message)
GroupArchivedMessage.archive!(user_2.id, group_message)
topics = TopicQuery.new(nil).list_private_messages_all_sent(user_2).topics
expect(topics).to eq([])
end
end
describe '#list_private_messages_all_archive' do
it 'returns a list of all private messages that has been archived' do
UserArchivedMessage.archive!(user_2.id, private_message)
GroupArchivedMessage.archive!(user_2.id, group_message)
topics = TopicQuery.new(nil).list_private_messages_all_archive(user_2).topics
expect(topics).to contain_exactly(private_message, group_message)
end
end
describe '#list_private_messages_all_new' do
it 'returns a list of new private messages' do
topics = TopicQuery.new(nil).list_private_messages_all_new(user_2).topics
expect(topics).to contain_exactly(private_message, group_message)
TopicUser.find_by(user: user_2, topic: group_message).update!(
last_read_post_number: 1
)
topics = TopicQuery.new(nil).list_private_messages_all_new(user_2).topics
expect(topics).to contain_exactly(private_message)
end
end
describe '#list_private_messages_all_unread' do
it 'returns a list of unread private messages' do
topics = TopicQuery.new(nil).list_private_messages_all_unread(user_2).topics
expect(topics).to eq([])
TopicUser.find_by(user: user_2, topic: group_message).update!(
last_read_post_number: 1
)
create_post(user: user, topic: group_message)
topics = TopicQuery.new(nil).list_private_messages_all_unread(user_2).topics
expect(topics).to contain_exactly(group_message)
end
end
describe '#list_private_messages' do
it 'returns a list of all private messages that a user has access to' do
topics = TopicQuery.new(nil).list_private_messages(user_2).topics
expect(topics).to contain_exactly(private_message)
end
end
describe '#list_private_messages_group' do
it 'should return the right list for a group user' do
group.add(user_2)
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(user_2)
.topics
expect(topics).to contain_exactly(group_message)
end
it 'should return the right list for an admin not part of the group' do
group.update!(name: group.name.capitalize)
topics = TopicQuery.new(nil, group_name: group.name.upcase)
.list_private_messages_group(Fabricate(:admin))
.topics
expect(topics).to contain_exactly(group_message)
end
it "should not allow a moderator not part of the group to view the group's messages" do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(Fabricate(:moderator))
.topics
expect(topics).to eq([])
end
it "should not allow a user not part of the group to view the group's messages" do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(Fabricate(:user))
.topics
expect(topics).to eq([])
end
context "Calculating minimum unread count for a topic" do
before do
group.update!(publish_read_state: true)
group.add(user)
end
let(:listed_message) do
TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group(user)
.topics.first
end
it 'returns the last read post number' do
topic_group = TopicGroup.create!(
topic: group_message, group: group, last_read_post_number: 10
)
expect(listed_message.last_read_post_number).to eq(topic_group.last_read_post_number)
end
end
end
describe '#list_private_messages_group_new' do
it 'returns a list of new private messages for a group that user is a part of' do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group_new(user_2)
.topics
expect(topics).to contain_exactly(group_message)
end
end
describe '#list_private_messages_group_unread' do
it 'returns a list of unread private messages for a group that user is a part of' do
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group_unread(user_2)
.topics
expect(topics).to eq([])
TopicUser.find_by(user: user_2, topic: group_message).update!(
last_read_post_number: 1
)
create_post(user: user, topic: group_message)
topics = TopicQuery.new(nil, group_name: group.name)
.list_private_messages_group_unread(user_2)
.topics
expect(topics).to contain_exactly(group_message)
end
end
describe '#list_private_messages_unread' do
fab!(:user) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) }
fab!(:pm) do
create_post(
user: user,
target_usernames: [user_2.username],
archetype: Archetype.private_message
).topic
end
fab!(:pm_2) do
create_post(
user: user,
target_usernames: [user_2.username],
archetype: Archetype.private_message
).topic
end
fab!(:pm_3) do
create_post(
user: user,
target_usernames: [user_2.username],
archetype: Archetype.private_message
).topic
end
it 'returns a list of private messages with unread posts that user is at least tracking' do
freeze_time 1.minute.from_now do
create_post(user: user_2, topic_id: pm.id)
create_post(user: user_2, topic_id: pm_3.id)
end
TopicUser.find_by(user: user, topic: pm_3).update!(
notification_level: TopicUser.notification_levels[:regular]
)
expect(TopicQuery.new(user).list_private_messages_unread(user).topics)
.to contain_exactly(pm)
end
end
describe '#list_private_messages_new' do
fab!(:user) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) }
fab!(:pm) 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)
end
it 'returns a list of new private messages accounting for muted tags' do
tag = Fabricate(:tag)
pm.tags << tag
TagUser.create!(
tag: tag,
user: user_2,
notification_level: TopicUser.notification_levels[:muted]
)
expect(TopicQuery.new(user_2).list_private_messages_new(user_2).topics)
.to eq([])
end
end
end

View File

@ -205,8 +205,8 @@ describe TopicTrackingState do
expect(messages.map(&:channel)).to contain_exactly( expect(messages.map(&:channel)).to contain_exactly(
'/private-messages/inbox', '/private-messages/inbox',
"/private-messages/group/#{group1.name}", "/private-messages/group/#{group1.name}/inbox",
"/private-messages/group/#{group2.name}" "/private-messages/group/#{group2.name}/inbox"
) )
message = messages.find do |m| message = messages.find do |m|
@ -218,7 +218,7 @@ describe TopicTrackingState do
[group1, group2].each do |group| [group1, group2].each do |group|
message = messages.find do |m| message = messages.find do |m|
m.channel == "/private-messages/group/#{group.name}" m.channel == "/private-messages/group/#{group.name}/inbox"
end end
expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.data["topic_id"]).to eq(private_message_topic.id)
@ -237,9 +237,9 @@ describe TopicTrackingState do
expect(messages.map(&:channel)).to contain_exactly( expect(messages.map(&:channel)).to contain_exactly(
'/private-messages/inbox', '/private-messages/inbox',
"/private-messages/group/#{group1.name}", "/private-messages/group/#{group1.name}/inbox",
"/private-messages/group/#{group1.name}/archive", "/private-messages/group/#{group1.name}/archive",
"/private-messages/group/#{group2.name}", "/private-messages/group/#{group2.name}/inbox",
"/private-messages/group/#{group2.name}/archive", "/private-messages/group/#{group2.name}/archive",
) )
@ -249,11 +249,9 @@ describe TopicTrackingState do
expect(message.user_ids).to eq(private_message_topic.allowed_users.map(&:id)) expect(message.user_ids).to eq(private_message_topic.allowed_users.map(&:id))
[group1, group2].each do |group| [group1, group2].each do |group|
group_channel = "/private-messages/group/#{group.name}"
[ [
group_channel, "/private-messages/group/#{group.name}/inbox",
"#{group_channel}/archive" "/private-messages/group/#{group.name}/archive"
].each do |channel| ].each do |channel|
message = messages.find { |m| m.channel == channel } message = messages.find { |m| m.channel == channel }
expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.data["topic_id"]).to eq(private_message_topic.id)
@ -291,7 +289,7 @@ describe TopicTrackingState do
expected_channels = [ expected_channels = [
'/private-messages/inbox', '/private-messages/inbox',
'/private-messages/sent', '/private-messages/sent',
"/private-messages/group/#{group.name}" "/private-messages/group/#{group.name}/inbox"
] ]
expect(messages.map(&:channel)).to contain_exactly(*expected_channels) expect(messages.map(&:channel)).to contain_exactly(*expected_channels)

View File

@ -473,7 +473,7 @@ describe TagsController do
it "can't see pm tags" do it "can't see pm tags" do
get "/tags/personal_messages/#{regular_user.username}.json" get "/tags/personal_messages/#{regular_user.username}.json"
expect(response).not_to be_successful expect(response.status).to eq(403)
end end
end end
@ -485,7 +485,7 @@ describe TagsController do
it "can't see pm tags for regular user" do it "can't see pm tags for regular user" do
get "/tags/personal_messages/#{regular_user.username}.json" get "/tags/personal_messages/#{regular_user.username}.json"
expect(response).not_to be_successful expect(response.status).to eq(404)
end end
it "can see their own pm tags" do it "can see their own pm tags" do