FEATURE: Display unread and new counts for messages. (#14059)
There are certain design decisions that were made in this commit. Private messages implements its own version of topic tracking state because there are significant differences between regular and private_message topics. Regular topics have to track categories and tags while private messages do not. It is much easier to design the new topic tracking state if we maintain two different classes, instead of trying to mash this two worlds together. One MessageBus channel per user and one MessageBus channel per group. This allows each user and each group to have their own channel backlog instead of having one global channel which requires the client to filter away unrelated messages.
This commit is contained in:
parent
4387bc1261
commit
f66007ec83
|
@ -66,12 +66,28 @@ export default Controller.extend({
|
|||
return pmView === VIEW_NAME_WARNINGS && !viewingSelf && !isAdmin;
|
||||
},
|
||||
|
||||
@discourseComputed("model.groups")
|
||||
inboxes(groups) {
|
||||
const groupsWithMessages = groups?.filter((group) => {
|
||||
return group.has_messages;
|
||||
});
|
||||
@discourseComputed("pmTopicTrackingState.newIncoming.[]", "selectedInbox")
|
||||
newLinkText() {
|
||||
return this._linkText("new");
|
||||
},
|
||||
|
||||
@discourseComputed("selectedInbox", "pmTopicTrackingState.newIncoming.[]")
|
||||
unreadLinkText() {
|
||||
return this._linkText("unread");
|
||||
},
|
||||
|
||||
_linkText(type) {
|
||||
const count = this.pmTopicTrackingState?.lookupCount(type) || 0;
|
||||
|
||||
if (count === 0) {
|
||||
return I18n.t(`user.messages.${type}`);
|
||||
} else {
|
||||
return I18n.t(`user.messages.${type}_with_count`, { count });
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("model.groupsWithMessages")
|
||||
inboxes(groupsWithMessages) {
|
||||
if (!groupsWithMessages || groupsWithMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import discourseComputed, {
|
||||
observes,
|
||||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import { reads } from "@ember/object/computed";
|
||||
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
|
||||
import { action } from "@ember/object";
|
||||
import Topic from "discourse/models/topic";
|
||||
|
@ -18,14 +16,9 @@ export default Controller.extend(BulkTopicSelection, {
|
|||
|
||||
hideCategory: false,
|
||||
showPosters: false,
|
||||
incomingCount: 0,
|
||||
channel: null,
|
||||
tagsForUser: null,
|
||||
|
||||
@on("init")
|
||||
_initialize() {
|
||||
this.newIncoming = [];
|
||||
},
|
||||
pmTopicTrackingState: null,
|
||||
|
||||
saveScrollPosition() {
|
||||
this.session.set("topicListScrollPosition", $(window).scrollTop());
|
||||
|
@ -36,10 +29,7 @@ export default Controller.extend(BulkTopicSelection, {
|
|||
this.set("application.showFooter", !this.get("model.canLoadMore"));
|
||||
},
|
||||
|
||||
@discourseComputed("incomingCount")
|
||||
hasIncoming(incomingCount) {
|
||||
return incomingCount > 0;
|
||||
},
|
||||
incomingCount: reads("pmTopicTrackingState.newIncoming.length"),
|
||||
|
||||
@discourseComputed("filter", "model.topics.length")
|
||||
showResetNew(filter, hasTopics) {
|
||||
|
@ -51,31 +41,16 @@ export default Controller.extend(BulkTopicSelection, {
|
|||
return filter === UNREAD_FILTER && hasTopics;
|
||||
},
|
||||
|
||||
subscribe(channel) {
|
||||
this.set("channel", channel);
|
||||
|
||||
this.messageBus.subscribe(channel, (data) => {
|
||||
if (this.newIncoming.indexOf(data.topic_id) === -1) {
|
||||
this.newIncoming.push(data.topic_id);
|
||||
this.incrementProperty("incomingCount");
|
||||
}
|
||||
});
|
||||
subscribe() {
|
||||
this.pmTopicTrackingState?.trackIncoming(
|
||||
this.inbox,
|
||||
this.filter,
|
||||
this.group
|
||||
);
|
||||
},
|
||||
|
||||
unsubscribe() {
|
||||
const channel = this.channel;
|
||||
if (channel) {
|
||||
this.messageBus.unsubscribe(channel);
|
||||
}
|
||||
this._resetTracking();
|
||||
this.set("channel", null);
|
||||
},
|
||||
|
||||
_resetTracking() {
|
||||
this.setProperties({
|
||||
newIncoming: [],
|
||||
incomingCount: 0,
|
||||
});
|
||||
this.pmTopicTrackingState?.resetTracking();
|
||||
},
|
||||
|
||||
@action
|
||||
|
@ -105,8 +80,8 @@ export default Controller.extend(BulkTopicSelection, {
|
|||
|
||||
@action
|
||||
showInserted() {
|
||||
this.model.loadBefore(this.newIncoming);
|
||||
this._resetTracking();
|
||||
this.model.loadBefore(this.pmTopicTrackingState.newIncoming);
|
||||
this.pmTopicTrackingState.resetTracking();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
import EmberObject from "@ember/object";
|
||||
import {
|
||||
ARCHIVE_FILTER,
|
||||
INBOX_FILTER,
|
||||
NEW_FILTER,
|
||||
UNREAD_FILTER,
|
||||
} from "discourse/routes/build-private-messages-route";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
|
||||
// See private_message_topic_tracking_state.rb for documentation
|
||||
const PrivateMessageTopicTrackingState = EmberObject.extend({
|
||||
CHANNEL_PREFIX: "/private-message-topic-tracking-state",
|
||||
|
||||
inbox: null,
|
||||
filter: null,
|
||||
activeGroup: null,
|
||||
|
||||
startTracking(data) {
|
||||
this.states = new Map();
|
||||
this.newIncoming = [];
|
||||
this._loadStates(data);
|
||||
this.establishChannels();
|
||||
},
|
||||
|
||||
establishChannels() {
|
||||
this.messageBus.subscribe(
|
||||
this._userChannel(this.user.id),
|
||||
this._processMessage.bind(this)
|
||||
);
|
||||
|
||||
this.user.groupsWithMessages?.forEach((group) => {
|
||||
this.messageBus.subscribe(
|
||||
this._groupChannel(group.id),
|
||||
this._processMessage.bind(this)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
stopTracking() {
|
||||
this.messageBus.unsubscribe(this._userChannel(this.user.id));
|
||||
|
||||
this.user.groupsWithMessages?.forEach((group) => {
|
||||
this.messageBus.unsubscribe(this._groupChannel(group.id));
|
||||
});
|
||||
},
|
||||
|
||||
lookupCount(type) {
|
||||
const typeFilterFn = type === "new" ? this._isNew : this._isUnread;
|
||||
let filterFn;
|
||||
|
||||
if (this.inbox === "user") {
|
||||
filterFn = this._isPersonal.bind(this);
|
||||
} else if (this.inbox === "group") {
|
||||
filterFn = this._isGroup.bind(this);
|
||||
}
|
||||
|
||||
return Array.from(this.states.values()).filter((topic) => {
|
||||
return typeFilterFn(topic) && (!filterFn || filterFn(topic));
|
||||
}).length;
|
||||
},
|
||||
|
||||
trackIncoming(inbox, filter, group) {
|
||||
this.setProperties({ inbox, filter, activeGroup: group });
|
||||
},
|
||||
|
||||
resetTracking() {
|
||||
if (this.inbox) {
|
||||
this.set("newIncoming", []);
|
||||
}
|
||||
},
|
||||
|
||||
_userChannel(userId) {
|
||||
return `${this.CHANNEL_PREFIX}/user/${userId}`;
|
||||
},
|
||||
|
||||
_groupChannel(groupId) {
|
||||
return `${this.CHANNEL_PREFIX}/group/${groupId}`;
|
||||
},
|
||||
|
||||
_isNew(topic) {
|
||||
return (
|
||||
!topic.last_read_post_number &&
|
||||
((topic.notification_level !== 0 && !topic.notification_level) ||
|
||||
topic.notification_level >= NotificationLevels.TRACKING) &&
|
||||
!topic.is_seen
|
||||
);
|
||||
},
|
||||
|
||||
_isUnread(topic) {
|
||||
return (
|
||||
topic.last_read_post_number &&
|
||||
topic.last_read_post_number < topic.highest_post_number &&
|
||||
topic.notification_level >= NotificationLevels.TRACKING
|
||||
);
|
||||
},
|
||||
|
||||
_isPersonal(topic) {
|
||||
const groups = this.user.groups;
|
||||
|
||||
if (groups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !groups.some((group) => {
|
||||
return topic.group_ids?.includes(group.id);
|
||||
});
|
||||
},
|
||||
|
||||
_isGroup(topic) {
|
||||
return this.user.groups.some((group) => {
|
||||
return (
|
||||
group.name === this.activeGroup.name &&
|
||||
topic.group_ids?.includes(group.id)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_processMessage(message) {
|
||||
switch (message.message_type) {
|
||||
case "new_topic":
|
||||
this._modifyState(message.topic_id, message.payload);
|
||||
|
||||
if (
|
||||
[NEW_FILTER, INBOX_FILTER].includes(this.filter) &&
|
||||
this._shouldDisplayMessageForInbox(message)
|
||||
) {
|
||||
this._notifyIncoming(message.topic_id);
|
||||
}
|
||||
|
||||
break;
|
||||
case "unread":
|
||||
this._modifyState(message.topic_id, message.payload);
|
||||
|
||||
if (
|
||||
[UNREAD_FILTER, INBOX_FILTER].includes(this.filter) &&
|
||||
this._shouldDisplayMessageForInbox(message)
|
||||
) {
|
||||
this._notifyIncoming(message.topic_id);
|
||||
}
|
||||
|
||||
break;
|
||||
case "archive":
|
||||
if (
|
||||
[INBOX_FILTER, ARCHIVE_FILTER].includes(this.filter) &&
|
||||
["user", "all"].includes(this.inbox)
|
||||
) {
|
||||
this._notifyIncoming(message.topic_id);
|
||||
}
|
||||
break;
|
||||
case "group_archive":
|
||||
if (
|
||||
[INBOX_FILTER, ARCHIVE_FILTER].includes(this.filter) &&
|
||||
(this.inbox === "all" || this._displayMessageForGroupInbox(message))
|
||||
) {
|
||||
this._notifyIncoming(message.topic_id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_displayMessageForGroupInbox(message) {
|
||||
return (
|
||||
this.inbox === "group" &&
|
||||
message.payload.group_ids.includes(this.activeGroup.id)
|
||||
);
|
||||
},
|
||||
|
||||
_shouldDisplayMessageForInbox(message) {
|
||||
return (
|
||||
this.inbox === "all" ||
|
||||
this._displayMessageForGroupInbox(message) ||
|
||||
(this.inbox === "user" &&
|
||||
(message.payload.group_ids.length === 0 ||
|
||||
this.currentUser.groups.filter((group) => {
|
||||
return message.payload.group_ids.includes(group.id);
|
||||
}).length === 0))
|
||||
);
|
||||
},
|
||||
|
||||
_notifyIncoming(topicId) {
|
||||
if (this.newIncoming.indexOf(topicId) === -1) {
|
||||
this.newIncoming.pushObject(topicId);
|
||||
}
|
||||
},
|
||||
|
||||
_loadStates(data) {
|
||||
(data || []).forEach((topic) => {
|
||||
this._modifyState(topic.topic_id, topic);
|
||||
});
|
||||
},
|
||||
|
||||
_modifyState(topicId, data) {
|
||||
this.states.set(topicId, data);
|
||||
},
|
||||
});
|
||||
|
||||
export default PrivateMessageTopicTrackingState;
|
|
@ -1,7 +1,7 @@
|
|||
import EmberObject, { computed, get, getProperties } from "@ember/object";
|
||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
||||
import { defaultHomepage, escapeExpression } from "discourse/lib/utilities";
|
||||
import { equal, gt, or } from "@ember/object/computed";
|
||||
import { equal, filterBy, gt, or } from "@ember/object/computed";
|
||||
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url";
|
||||
import { A } from "@ember/array";
|
||||
import Badge from "discourse/models/badge";
|
||||
|
@ -562,6 +562,8 @@ const User = RestModel.extend({
|
|||
});
|
||||
},
|
||||
|
||||
groupsWithMessages: filterBy("groups", "has_messages", true),
|
||||
|
||||
@discourseComputed("filteredGroups", "numGroupsToDisplay")
|
||||
displayGroups(filteredGroups, numGroupsToDisplay) {
|
||||
const groups = filteredGroups.slice(0, numGroupsToDisplay);
|
||||
|
|
|
@ -57,16 +57,16 @@ export default (inboxType, filter) => {
|
|||
|
||||
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 (filter) {
|
||||
this.controllerFor("user-topics-list").subscribe(
|
||||
`/private-messages/group/${this.get(
|
||||
"groupName"
|
||||
).toLowerCase()}/${filter}`
|
||||
const userTopicsListController = this.controllerFor("user-topics-list");
|
||||
userTopicsListController.set("group", this.group);
|
||||
|
||||
userTopicsListController.set(
|
||||
"pmTopicTrackingState.activeGroup",
|
||||
this.group
|
||||
);
|
||||
}
|
||||
|
||||
this.controllerFor("user-private-messages").set("group", this.group);
|
||||
},
|
||||
|
||||
dismissReadOptions() {
|
||||
|
|
|
@ -6,6 +6,8 @@ import { action } from "@ember/object";
|
|||
|
||||
export const NEW_FILTER = "new";
|
||||
export const UNREAD_FILTER = "unread";
|
||||
export const INBOX_FILTER = "inbox";
|
||||
export const ARCHIVE_FILTER = "archive";
|
||||
|
||||
// A helper to build a user topic list route
|
||||
export default (inboxType, path, filter) => {
|
||||
|
@ -42,13 +44,13 @@ export default (inboxType, path, filter) => {
|
|||
setupController() {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (filter) {
|
||||
this.controllerFor("user-topics-list").subscribe(
|
||||
`/private-messages/${filter}`
|
||||
const userPrivateMessagesController = this.controllerFor(
|
||||
"user-private-messages"
|
||||
);
|
||||
}
|
||||
|
||||
this.controllerFor("user-topics-list").setProperties({
|
||||
const userTopicsListController = this.controllerFor("user-topics-list");
|
||||
|
||||
userTopicsListController.setProperties({
|
||||
hideCategory: true,
|
||||
showPosters: true,
|
||||
tagsForUser: this.modelFor("user").get("username_lower"),
|
||||
|
@ -57,9 +59,13 @@ export default (inboxType, path, filter) => {
|
|||
filter: filter,
|
||||
group: null,
|
||||
inbox: inboxType,
|
||||
pmTopicTrackingState:
|
||||
userPrivateMessagesController.pmTopicTrackingState,
|
||||
});
|
||||
|
||||
this.controllerFor("user-private-messages").setProperties({
|
||||
userTopicsListController.subscribe();
|
||||
|
||||
userPrivateMessagesController.setProperties({
|
||||
archive: false,
|
||||
pmView: inboxType,
|
||||
group: null,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
import createPMRoute, {
|
||||
ARCHIVE_FILTER,
|
||||
} from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute(
|
||||
"archive",
|
||||
"all",
|
||||
"private-messages-all-archive",
|
||||
"archive"
|
||||
ARCHIVE_FILTER
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import createPMRoute from "discourse/routes/build-private-messages-group-route";
|
||||
import { ARCHIVE_FILTER } from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute("group", "archive");
|
||||
export default createPMRoute("group", ARCHIVE_FILTER);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import createPMRoute from "discourse/routes/build-private-messages-group-route";
|
||||
import { INBOX_FILTER } from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute("group", "inbox");
|
||||
export default createPMRoute("group", INBOX_FILTER);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
import createPMRoute, {
|
||||
INBOX_FILTER,
|
||||
} from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute("all", "private-messages-all", "inbox");
|
||||
export default createPMRoute("all", "private-messages-all", INBOX_FILTER);
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
import createPMRoute, {
|
||||
ARCHIVE_FILTER,
|
||||
} from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute("user", "private-messages-archive", "archive");
|
||||
export default createPMRoute(
|
||||
"user",
|
||||
"private-messages-archive",
|
||||
ARCHIVE_FILTER
|
||||
);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
import createPMRoute, {
|
||||
INBOX_FILTER,
|
||||
} from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute("user", "private-messages", "inbox");
|
||||
export default createPMRoute("user", "private-messages", INBOX_FILTER);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import Composer from "discourse/models/composer";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import Draft from "discourse/models/draft";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import PrivateMessageTopicTrackingState from "discourse/models/private-message-topic-tracking-state";
|
||||
|
||||
export default DiscourseRoute.extend({
|
||||
renderTemplate() {
|
||||
|
@ -8,11 +11,36 @@ export default DiscourseRoute.extend({
|
|||
},
|
||||
|
||||
model() {
|
||||
return this.modelFor("user");
|
||||
const user = this.modelFor("user");
|
||||
return ajax(`/u/${user.username}/private-message-topic-tracking-state`)
|
||||
.then((pmTopicTrackingStateData) => {
|
||||
return {
|
||||
user,
|
||||
pmTopicTrackingStateData,
|
||||
};
|
||||
})
|
||||
.catch((e) => {
|
||||
popupAjaxError(e);
|
||||
return { user };
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, user) {
|
||||
controller.set("model", user);
|
||||
setupController(controller, model) {
|
||||
const user = model.user;
|
||||
|
||||
const pmTopicTrackingState = PrivateMessageTopicTrackingState.create({
|
||||
messageBus: controller.messageBus,
|
||||
user,
|
||||
});
|
||||
|
||||
pmTopicTrackingState.startTracking(model.pmTopicTrackingStateData);
|
||||
|
||||
controller.setProperties({
|
||||
model: user,
|
||||
pmTopicTrackingState,
|
||||
});
|
||||
|
||||
this.set("pmTopicTrackingState", pmTopicTrackingState);
|
||||
|
||||
if (this.currentUser) {
|
||||
const composerController = this.controllerFor("composer");
|
||||
|
@ -30,6 +58,10 @@ export default DiscourseRoute.extend({
|
|||
}
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this.pmTopicTrackingState.stopTracking();
|
||||
},
|
||||
|
||||
actions: {
|
||||
refresh() {
|
||||
this.refresh();
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
showDismissRead=showDismissRead
|
||||
resetNew=(action "resetNew")}}
|
||||
|
||||
{{#if hasIncoming}}
|
||||
{{#if (gt incomingCount 0)}}
|
||||
<div class="show-mores">
|
||||
<a tabindex="0" href {{action "showInserted"}} class="alert alert-info clickable">
|
||||
{{count-i18n key="topic_count_" suffix="latest" count=incomingCount}}
|
||||
|
|
|
@ -32,13 +32,13 @@
|
|||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to "userPrivateMessages.new" model}}
|
||||
{{i18n "user.messages.new"}}
|
||||
{{#link-to "userPrivateMessages.new" model class="new"}}
|
||||
{{newLinkText}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to "userPrivateMessages.unread" model}}
|
||||
{{i18n "user.messages.unread"}}
|
||||
{{#link-to "userPrivateMessages.unread" model class="unread"}}
|
||||
{{unreadLinkText}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
|
@ -55,13 +55,13 @@
|
|||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to "userPrivateMessages.groupNew" group.name}}
|
||||
{{i18n "user.messages.new"}}
|
||||
{{#link-to "userPrivateMessages.groupNew" group.name class="new"}}
|
||||
{{newLinkText}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to "userPrivateMessages.groupUnread" group.name}}
|
||||
{{i18n "user.messages.unread"}}
|
||||
{{#link-to "userPrivateMessages.groupUnread" group.name class="unread"}}
|
||||
{{unreadLinkText}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
|
@ -83,13 +83,13 @@
|
|||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to "userPrivateMessages.personalNew" model}}
|
||||
{{i18n "user.messages.new"}}
|
||||
{{#link-to "userPrivateMessages.personalNew" model class="new"}}
|
||||
{{newLinkText}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to "userPrivateMessages.personalUnread" model}}
|
||||
{{i18n "user.messages.unread"}}
|
||||
{{#link-to "userPrivateMessages.personalUnread" model class="unread"}}
|
||||
{{unreadLinkText}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
|
@ -101,7 +101,7 @@
|
|||
|
||||
{{#if displayGlobalFilters}}
|
||||
{{#if pmTaggingEnabled}}
|
||||
<li class="noGlyph tags">
|
||||
<li class="tags">
|
||||
{{#link-to "userPrivateMessages.tags" model}}
|
||||
{{i18n "user.messages.tags"}}
|
||||
{{/link-to}}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import I18n from "I18n";
|
||||
import {
|
||||
acceptance,
|
||||
count,
|
||||
exists,
|
||||
publishToMessageBus,
|
||||
query,
|
||||
} 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";
|
||||
|
||||
|
@ -72,10 +75,13 @@ acceptance(
|
|||
[
|
||||
"/topics/private-messages-all-new/:username.json",
|
||||
"/topics/private-messages-all-unread/:username.json",
|
||||
"/topics/private-messages-all-archive/:username.json",
|
||||
"/topics/private-messages-new/:username.json",
|
||||
"/topics/private-messages-unread/:username.json",
|
||||
"/topics/private-messages-archive/:username.json",
|
||||
"/topics/private-messages-group/:username/:group_name/new.json",
|
||||
"/topics/private-messages-group/:username/:group_name/unread.json",
|
||||
"/topics/private-messages-group/:username/:group_name/archive.json",
|
||||
].forEach((url) => {
|
||||
server.get(url, () => {
|
||||
let topics;
|
||||
|
@ -153,6 +159,227 @@ acceptance(
|
|||
});
|
||||
});
|
||||
|
||||
const publishUnreadToMessageBus = function (group_ids) {
|
||||
publishToMessageBus("/private-message-topic-tracking-state/user/5", {
|
||||
topic_id: Math.random(),
|
||||
message_type: "unread",
|
||||
payload: {
|
||||
last_read_post_number: 1,
|
||||
highest_post_number: 2,
|
||||
notification_level: 2,
|
||||
group_ids: group_ids || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const publishNewToMessageBus = function (group_ids) {
|
||||
publishToMessageBus("/private-message-topic-tracking-state/user/5", {
|
||||
topic_id: Math.random(),
|
||||
message_type: "new_topic",
|
||||
payload: {
|
||||
last_read_post_number: null,
|
||||
highest_post_number: 1,
|
||||
group_ids: group_ids || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const publishArchiveToMessageBus = function () {
|
||||
publishToMessageBus("/private-message-topic-tracking-state/user/5", {
|
||||
topic_id: Math.random(),
|
||||
message_type: "archive",
|
||||
});
|
||||
};
|
||||
|
||||
const publishGroupArchiveToMessageBus = function (group_ids) {
|
||||
publishToMessageBus(
|
||||
`/private-message-topic-tracking-state/group/${group_ids[0]}`,
|
||||
{
|
||||
topic_id: Math.random(),
|
||||
message_type: "group_archive",
|
||||
payload: {
|
||||
group_ids: group_ids,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
test("incoming archive message on all and archive filter", async function (assert) {
|
||||
for (const url of [
|
||||
"/u/charlie/messages",
|
||||
"/u/charlie/messages/archive",
|
||||
"/u/charlie/messages/personal",
|
||||
"/u/charlie/messages/personal/archive",
|
||||
]) {
|
||||
await visit(url);
|
||||
|
||||
publishArchiveToMessageBus();
|
||||
|
||||
await visit(url); // wait for re-render
|
||||
|
||||
assert.ok(
|
||||
exists(".show-mores"),
|
||||
`${url} displays the topic incoming info`
|
||||
);
|
||||
}
|
||||
|
||||
for (const url of [
|
||||
"/u/charlie/messages/group/awesome_group/archive",
|
||||
"/u/charlie/messages/group/awesome_group",
|
||||
]) {
|
||||
await visit(url);
|
||||
|
||||
publishArchiveToMessageBus();
|
||||
|
||||
await visit(url); // wait for re-render
|
||||
|
||||
assert.ok(
|
||||
!exists(".show-mores"),
|
||||
`${url} does not display the topic incoming info`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("incoming group archive message on all and archive filter", async function (assert) {
|
||||
for (const url of [
|
||||
"/u/charlie/messages",
|
||||
"/u/charlie/messages/archive",
|
||||
"/u/charlie/messages/group/awesome_group",
|
||||
"/u/charlie/messages/group/awesome_group/archive",
|
||||
]) {
|
||||
await visit(url);
|
||||
|
||||
publishGroupArchiveToMessageBus([14]);
|
||||
|
||||
await visit(url); // wait for re-render
|
||||
|
||||
assert.ok(
|
||||
exists(".show-mores"),
|
||||
`${url} displays the topic incoming info`
|
||||
);
|
||||
}
|
||||
|
||||
for (const url of [
|
||||
"/u/charlie/messages/personal",
|
||||
"/u/charlie/messages/personal/archive",
|
||||
]) {
|
||||
await visit(url);
|
||||
|
||||
publishGroupArchiveToMessageBus([14]);
|
||||
|
||||
await visit(url); // wait for re-render
|
||||
|
||||
assert.ok(
|
||||
!exists(".show-mores"),
|
||||
`${url} does not display the topic incoming info`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("incoming unread and new messages on all filter", async function (assert) {
|
||||
await visit("/u/charlie/messages");
|
||||
|
||||
publishUnreadToMessageBus();
|
||||
publishNewToMessageBus();
|
||||
|
||||
await visit("/u/charlie/messages"); // wait for re-render
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.new").innerText.trim(),
|
||||
I18n.t("user.messages.new_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.unread").innerText.trim(),
|
||||
I18n.t("user.messages.unread_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
});
|
||||
|
||||
test("incoming new messages while viewing new", async function (assert) {
|
||||
await visit("/u/charlie/messages/new");
|
||||
|
||||
publishNewToMessageBus();
|
||||
|
||||
await visit("/u/charlie/messages/new"); // wait for re-render
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.new").innerText.trim(),
|
||||
I18n.t("user.messages.new_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.ok(exists(".show-mores"), "displays the topic incoming info");
|
||||
});
|
||||
|
||||
test("incoming unread messages while viewing unread", async function (assert) {
|
||||
await visit("/u/charlie/messages/unread");
|
||||
|
||||
publishUnreadToMessageBus();
|
||||
|
||||
await visit("/u/charlie/messages/unread"); // wait for re-render
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.unread").innerText.trim(),
|
||||
I18n.t("user.messages.unread_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.ok(exists(".show-mores"), "displays the topic incoming info");
|
||||
});
|
||||
|
||||
test("incoming unread messages while viewing group unread", async function (assert) {
|
||||
await visit("/u/charlie/messages/group/awesome_group/unread");
|
||||
|
||||
publishUnreadToMessageBus([14]);
|
||||
publishNewToMessageBus([14]);
|
||||
|
||||
await visit("/u/charlie/messages/group/awesome_group/unread"); // wait for re-render
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.unread").innerText.trim(),
|
||||
I18n.t("user.messages.unread_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.new").innerText.trim(),
|
||||
I18n.t("user.messages.new_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.ok(exists(".show-mores"), "displays the topic incoming info");
|
||||
|
||||
await visit("/u/charlie/messages/unread");
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.unread").innerText.trim(),
|
||||
I18n.t("user.messages.unread_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.new").innerText.trim(),
|
||||
I18n.t("user.messages.new_with_count", { count: 1 }),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
await visit("/u/charlie/messages/personal/unread");
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.unread").innerText.trim(),
|
||||
I18n.t("user.messages.unread"),
|
||||
"displays the right count"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query(".messages-nav li a.new").innerText.trim(),
|
||||
I18n.t("user.messages.new"),
|
||||
"displays the right count"
|
||||
);
|
||||
});
|
||||
|
||||
test("dismissing all unread messages", async function (assert) {
|
||||
await visit("/u/charlie/messages/unread");
|
||||
|
||||
|
|
|
@ -217,6 +217,10 @@ export function applyDefaultHandlers(pretender) {
|
|||
});
|
||||
});
|
||||
|
||||
pretender.get("/u/:username/private-message-topic-tracking-state", () => {
|
||||
return response([]);
|
||||
});
|
||||
|
||||
pretender.get("/topics/feature_stats.json", () => {
|
||||
return response({
|
||||
pinned_in_category_count: 0,
|
||||
|
|
|
@ -316,6 +316,21 @@ class UsersController < ApplicationController
|
|||
render json: MultiJson.dump(serializer)
|
||||
end
|
||||
|
||||
def private_message_topic_tracking_state
|
||||
user = fetch_user_from_params
|
||||
guardian.ensure_can_edit!(user)
|
||||
|
||||
report = PrivateMessageTopicTrackingState.report(user)
|
||||
|
||||
serializer = ActiveModel::ArraySerializer.new(
|
||||
report,
|
||||
each_serializer: PrivateMessageTopicTrackingStateSerializer,
|
||||
scope: guardian
|
||||
)
|
||||
|
||||
render json: MultiJson.dump(serializer)
|
||||
end
|
||||
|
||||
def badge_title
|
||||
params.require(:user_badge_id)
|
||||
|
||||
|
|
|
@ -5,8 +5,21 @@ module Jobs
|
|||
|
||||
def execute(args)
|
||||
post = Post.find_by(id: args[:post_id])
|
||||
return if !post&.topic
|
||||
|
||||
if post && post.topic
|
||||
topic = post.topic
|
||||
|
||||
if topic.private_message?
|
||||
if post.post_number > 1
|
||||
PrivateMessageTopicTrackingState.publish_unread(post)
|
||||
end
|
||||
|
||||
TopicGroup.new_message_update(
|
||||
topic.last_poster,
|
||||
topic.id,
|
||||
post.post_number
|
||||
)
|
||||
else
|
||||
TopicTrackingState.publish_unmuted(post.topic)
|
||||
if post.post_number > 1
|
||||
TopicTrackingState.publish_muted(post.topic)
|
||||
|
|
|
@ -9,7 +9,7 @@ class GroupArchivedMessage < ActiveRecord::Base
|
|||
destroyed = GroupArchivedMessage.where(group_id: group_id, topic_id: topic_id).destroy_all
|
||||
trigger(:move_to_inbox, group_id, topic_id)
|
||||
MessageBus.publish("/topic/#{topic_id}", { type: "move_to_inbox" }, group_ids: [group_id])
|
||||
publish_topic_tracking_state(topic)
|
||||
publish_topic_tracking_state(topic, group_id)
|
||||
set_imap_sync(topic_id) if !opts[:skip_imap_sync] && destroyed.present?
|
||||
end
|
||||
|
||||
|
@ -19,7 +19,7 @@ class GroupArchivedMessage < ActiveRecord::Base
|
|||
GroupArchivedMessage.create!(group_id: group_id, topic_id: topic_id)
|
||||
trigger(:archive_message, group_id, topic_id)
|
||||
MessageBus.publish("/topic/#{topic_id}", { type: "archived" }, group_ids: [group_id])
|
||||
publish_topic_tracking_state(topic)
|
||||
publish_topic_tracking_state(topic, group_id)
|
||||
set_imap_sync(topic_id) if !opts[:skip_imap_sync] && destroyed.blank?
|
||||
end
|
||||
|
||||
|
@ -31,20 +31,18 @@ class GroupArchivedMessage < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.publish_topic_tracking_state(topic)
|
||||
TopicTrackingState.publish_private_message(
|
||||
topic, group_archive: true
|
||||
)
|
||||
end
|
||||
|
||||
def self.set_imap_sync(topic_id)
|
||||
IncomingEmail.joins(:post)
|
||||
.where.not(imap_uid: nil)
|
||||
.where(topic_id: topic_id, posts: { post_number: 1 })
|
||||
.update_all(imap_sync: true)
|
||||
end
|
||||
private_class_method :set_imap_sync
|
||||
|
||||
def self.publish_topic_tracking_state(topic, group_id)
|
||||
PrivateMessageTopicTrackingState.publish_group_archived(topic, group_id)
|
||||
end
|
||||
private_class_method :publish_topic_tracking_state
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This class is used to mirror unread and new status for private messages between
|
||||
# server and client.
|
||||
#
|
||||
# On the server side, this class has two main responsibilities. The first is to
|
||||
# query the database for the initial state of a user's unread and new private
|
||||
# messages. The second is to publish message_bus messages to notify the client
|
||||
# of various topic events.
|
||||
#
|
||||
# On the client side, we have a `PrivateMessageTopicTrackingState` class as well
|
||||
# which will load the initial state into memory and subscribes to the relevant
|
||||
# message_bus messages. When a message is received, it modifies the in-memory
|
||||
# state based on the message type. The filtering for new and unread topics is
|
||||
# done on the client side based on the in-memory state in order to derive the
|
||||
# count of new and unread topics efficiently.
|
||||
class PrivateMessageTopicTrackingState
|
||||
CHANNEL_PREFIX = "/private-message-topic-tracking-state"
|
||||
NEW_MESSAGE_TYPE = "new_topic"
|
||||
UNREAD_MESSAGE_TYPE = "unread"
|
||||
ARCHIVE_MESSAGE_TYPE = "archive"
|
||||
GROUP_ARCHIVE_MESSAGE_TYPE = "group_archive"
|
||||
|
||||
def self.report(user)
|
||||
sql = new_and_unread_sql(user)
|
||||
|
||||
DB.query(
|
||||
sql + "\n\n LIMIT :max_topics",
|
||||
{
|
||||
max_topics: TopicTrackingState::MAX_TOPICS,
|
||||
min_new_topic_date: Time.at(SiteSetting.min_new_topics_time).to_datetime
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def self.new_and_unread_sql(user)
|
||||
sql = report_raw_sql(user, skip_unread: true)
|
||||
sql << "\nUNION ALL\n\n"
|
||||
sql << report_raw_sql(user, skip_new: true)
|
||||
end
|
||||
|
||||
def self.report_raw_sql(user, skip_unread: false,
|
||||
skip_new: false)
|
||||
|
||||
unread =
|
||||
if skip_unread
|
||||
"1=0"
|
||||
else
|
||||
TopicTrackingState.unread_filter_sql(staff: user.staff?)
|
||||
end
|
||||
|
||||
new =
|
||||
if skip_new
|
||||
"1=0"
|
||||
else
|
||||
TopicTrackingState.new_filter_sql
|
||||
end
|
||||
|
||||
sql = +<<~SQL
|
||||
SELECT
|
||||
DISTINCT topics.id AS topic_id,
|
||||
u.id AS user_id,
|
||||
last_read_post_number,
|
||||
tu.notification_level,
|
||||
#{TopicTrackingState.highest_post_number_column_select(user.staff?)},
|
||||
ARRAY(SELECT group_id FROM topic_allowed_groups WHERE topic_allowed_groups.topic_id = topics.id) AS group_ids
|
||||
FROM topics
|
||||
JOIN users u on u.id = #{user.id.to_i}
|
||||
JOIN user_stats AS us ON us.user_id = u.id
|
||||
JOIN user_options AS uo ON uo.user_id = u.id
|
||||
LEFT JOIN group_users gu ON gu.user_id = u.id
|
||||
LEFT JOIN topic_allowed_groups tag ON tag.topic_id = topics.id AND tag.group_id = gu.group_id
|
||||
LEFT JOIN topic_users tu ON tu.topic_id = topics.id AND tu.user_id = u.id
|
||||
LEFT JOIN topic_allowed_users tau ON tau.topic_id = topics.id AND tau.user_id = u.id
|
||||
#{skip_new ? "" : "LEFT JOIN dismissed_topic_users ON dismissed_topic_users.topic_id = topics.id AND dismissed_topic_users.user_id = #{user.id.to_i}"}
|
||||
WHERE (tau.topic_id IS NOT NULL OR tag.topic_id IS NOT NULL) AND
|
||||
#{skip_unread ? "" : "topics.updated_at >= LEAST(us.first_unread_pm_at, gu.first_unread_pm_at) AND"}
|
||||
topics.archetype = 'private_message' AND
|
||||
((#{unread}) OR (#{new})) AND
|
||||
topics.deleted_at IS NULL
|
||||
SQL
|
||||
end
|
||||
|
||||
def self.publish_unread(post)
|
||||
return unless post.topic.private_message?
|
||||
|
||||
scope = TopicUser
|
||||
.tracking(post.topic_id)
|
||||
.includes(user: :user_stat)
|
||||
|
||||
allowed_group_ids = post.topic.allowed_groups.pluck(:id)
|
||||
|
||||
group_ids =
|
||||
if post.post_type == Post.types[:whisper]
|
||||
[Group::AUTO_GROUPS[:staff]]
|
||||
else
|
||||
allowed_group_ids
|
||||
end
|
||||
|
||||
if group_ids.present?
|
||||
scope = scope
|
||||
.joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id")
|
||||
.where("gu.group_id IN (?)", group_ids)
|
||||
end
|
||||
|
||||
scope
|
||||
.select([:user_id, :last_read_post_number, :notification_level])
|
||||
.each do |tu|
|
||||
|
||||
message = {
|
||||
topic_id: post.topic_id,
|
||||
message_type: UNREAD_MESSAGE_TYPE,
|
||||
payload: {
|
||||
last_read_post_number: tu.last_read_post_number,
|
||||
highest_post_number: post.post_number,
|
||||
notification_level: tu.notification_level,
|
||||
group_ids: allowed_group_ids
|
||||
}
|
||||
}
|
||||
|
||||
MessageBus.publish(self.user_channel(tu.user_id), message.as_json,
|
||||
user_ids: [tu.user_id]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.publish_new(topic)
|
||||
return unless topic.private_message?
|
||||
|
||||
message = {
|
||||
message_type: NEW_MESSAGE_TYPE,
|
||||
topic_id: topic.id,
|
||||
payload: {
|
||||
last_read_post_number: nil,
|
||||
highest_post_number: 1,
|
||||
group_ids: topic.allowed_groups.pluck(:id)
|
||||
}
|
||||
}.as_json
|
||||
|
||||
topic.allowed_users.pluck(:id).each do |user_id|
|
||||
MessageBus.publish(self.user_channel(user_id), message, user_ids: [user_id])
|
||||
end
|
||||
|
||||
topic.allowed_groups.pluck(:id).each do |group_id|
|
||||
MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id])
|
||||
end
|
||||
end
|
||||
|
||||
def self.publish_group_archived(topic, group_id)
|
||||
return unless topic.private_message?
|
||||
|
||||
message = {
|
||||
message_type: GROUP_ARCHIVE_MESSAGE_TYPE,
|
||||
topic_id: topic.id,
|
||||
payload: {
|
||||
group_ids: [group_id]
|
||||
}
|
||||
}.as_json
|
||||
|
||||
MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id])
|
||||
end
|
||||
|
||||
def self.publish_user_archived(topic, user_id)
|
||||
return unless topic.private_message?
|
||||
|
||||
message = {
|
||||
message_type: ARCHIVE_MESSAGE_TYPE,
|
||||
topic_id: topic.id,
|
||||
}.as_json
|
||||
|
||||
MessageBus.publish(self.user_channel(user_id), message, user_ids: [user_id])
|
||||
end
|
||||
|
||||
def self.user_channel(user_id)
|
||||
"#{CHANNEL_PREFIX}/user/#{user_id}"
|
||||
end
|
||||
|
||||
def self.group_channel(group_id)
|
||||
"#{CHANNEL_PREFIX}/group/#{group_id}"
|
||||
end
|
||||
end
|
|
@ -16,7 +16,6 @@
|
|||
#
|
||||
# See discourse/app/models/topic-tracking-state.js
|
||||
class TopicTrackingState
|
||||
|
||||
include ActiveModel::SerializerSupport
|
||||
|
||||
UNREAD_MESSAGE_TYPE = "unread"
|
||||
|
@ -510,60 +509,6 @@ class TopicTrackingState
|
|||
"#{staff ? "topics.highest_staff_post_number AS highest_post_number" : "topics.highest_post_number"}"
|
||||
end
|
||||
|
||||
def self.publish_private_message(topic, archive_user_id: nil,
|
||||
post: nil,
|
||||
group_archive: false)
|
||||
|
||||
return unless topic.private_message?
|
||||
channels = {}
|
||||
|
||||
allowed_user_ids = topic.allowed_users.pluck(:id)
|
||||
|
||||
if post && allowed_user_ids.include?(post.user_id)
|
||||
channels["/private-messages/sent"] = [post.user_id]
|
||||
end
|
||||
|
||||
if archive_user_id
|
||||
user_ids = [archive_user_id]
|
||||
|
||||
[
|
||||
"/private-messages/archive",
|
||||
"/private-messages/inbox",
|
||||
"/private-messages/sent",
|
||||
].each do |channel|
|
||||
channels[channel] = user_ids
|
||||
end
|
||||
end
|
||||
|
||||
if channels.except("/private-messages/sent").blank?
|
||||
channels["/private-messages/inbox"] = allowed_user_ids
|
||||
end
|
||||
|
||||
topic.allowed_groups.each do |group|
|
||||
group_user_ids = group.users.pluck(:id)
|
||||
next if group_user_ids.blank?
|
||||
group_channels = []
|
||||
channel_prefix = "/private-messages/group/#{group.name.downcase}"
|
||||
group_channels << "#{channel_prefix}/inbox"
|
||||
group_channels << "#{channel_prefix}/archive" if group_archive
|
||||
group_channels.each { |channel| channels[channel] = group_user_ids }
|
||||
end
|
||||
|
||||
message = {
|
||||
topic_id: topic.id
|
||||
}
|
||||
|
||||
channels.each do |channel, ids|
|
||||
if ids.present?
|
||||
MessageBus.publish(
|
||||
channel,
|
||||
message.as_json,
|
||||
user_ids: ids
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.publish_read_indicator_on_write(topic_id, last_read_post_number, user_id)
|
||||
topic = Topic.includes(:allowed_groups).select(:highest_post_number, :archetype, :id).find_by(id: topic_id)
|
||||
|
||||
|
|
|
@ -36,13 +36,10 @@ class UserArchivedMessage < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.publish_topic_tracking_state(topic, user_id)
|
||||
TopicTrackingState.publish_private_message(
|
||||
topic, archive_user_id: user_id
|
||||
)
|
||||
PrivateMessageTopicTrackingState.publish_user_archived(topic, user_id)
|
||||
end
|
||||
private_class_method :publish_topic_tracking_state
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrivateMessageTopicTrackingStateSerializer < ApplicationSerializer
|
||||
attributes :topic_id,
|
||||
:highest_post_number,
|
||||
:last_read_post_number,
|
||||
:notification_level,
|
||||
:group_ids
|
||||
end
|
|
@ -1184,7 +1184,13 @@ en:
|
|||
latest: "Latest"
|
||||
sent: "Sent"
|
||||
unread: "Unread"
|
||||
unread_with_count:
|
||||
one: "Unread (%{count})"
|
||||
other: "Unread (%{count})"
|
||||
new: "New"
|
||||
new_with_count:
|
||||
one: "New (%{count})"
|
||||
other: "New (%{count})"
|
||||
archive: "Archive"
|
||||
groups: "My Groups"
|
||||
move_to_inbox: "Move to Inbox"
|
||||
|
|
|
@ -509,6 +509,7 @@ Discourse::Application.routes.draw do
|
|||
get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/private-message-topic-tracking-state" => "users#private_message_topic_tracking_state", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/profile-hidden" => "users#profile_hidden"
|
||||
put "#{root_path}/:username/feature-topic" => "users#feature_topic", constraints: { username: RouteFormat.username }
|
||||
put "#{root_path}/:username/clear-featured-topic" => "users#clear_featured_topic", constraints: { username: RouteFormat.username }
|
||||
|
|
|
@ -20,11 +20,6 @@ class PostJobsEnqueuer
|
|||
after_topic_create
|
||||
make_visible
|
||||
end
|
||||
|
||||
if @topic.private_message?
|
||||
TopicTrackingState.publish_private_message(@topic, post: @post)
|
||||
TopicGroup.new_message_update(@topic.last_poster, @topic.id, @post.post_number)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -46,6 +41,7 @@ class PostJobsEnqueuer
|
|||
end
|
||||
|
||||
def make_visible
|
||||
return if @topic.private_message?
|
||||
return unless SiteSetting.embed_unlisted?
|
||||
return unless @post.post_number > 1
|
||||
return if @topic.visible?
|
||||
|
@ -73,12 +69,18 @@ class PostJobsEnqueuer
|
|||
@topic.posters = @topic.posters_summary
|
||||
@topic.posts_count = 1
|
||||
|
||||
TopicTrackingState.publish_new(@topic)
|
||||
klass =
|
||||
if @topic.private_message?
|
||||
PrivateMessageTopicTrackingState
|
||||
else
|
||||
TopicTrackingState
|
||||
end
|
||||
|
||||
klass.publish_new(@topic)
|
||||
end
|
||||
|
||||
def skip_after_create?
|
||||
@opts[:import_mode] ||
|
||||
@topic.private_message? ||
|
||||
@post.post_type == Post.types[:moderator_action] ||
|
||||
@post.post_type == Post.types[:small_action]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe GroupArchivedMessage 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
|
||||
|
||||
describe '.move_to_inbox!' do
|
||||
it 'should unarchive the topic correctly' do
|
||||
described_class.archive!(group.id, group_message)
|
||||
|
||||
messages = MessageBus.track_publish(PrivateMessageTopicTrackingState.group_channel(group.id)) do
|
||||
described_class.move_to_inbox!(group.id, group_message)
|
||||
end
|
||||
|
||||
expect(messages.present?).to eq(true)
|
||||
|
||||
expect(GroupArchivedMessage.exists?(
|
||||
topic: group_message,
|
||||
group: group
|
||||
)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.archive!' do
|
||||
it 'should archive the topic correctly' do
|
||||
messages = MessageBus.track_publish(PrivateMessageTopicTrackingState.group_channel(group.id)) do
|
||||
described_class.archive!(group.id, group_message)
|
||||
end
|
||||
|
||||
expect(GroupArchivedMessage.exists?(
|
||||
topic: group_message,
|
||||
group: group
|
||||
)).to eq(true)
|
||||
|
||||
expect(messages.present?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,206 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe PrivateMessageTopicTrackingState 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
|
||||
|
||||
fab!(:private_message_2) do
|
||||
create_post(
|
||||
user: user,
|
||||
target_usernames: [Fabricate(:user).username],
|
||||
archetype: Archetype.private_message
|
||||
).topic
|
||||
end
|
||||
|
||||
describe '.report' do
|
||||
it 'returns the right tracking state' do
|
||||
TopicUser.find_by(user: user_2, topic: group_message).update!(
|
||||
last_read_post_number: 1
|
||||
)
|
||||
|
||||
expect(described_class.report(user_2).map(&:topic_id))
|
||||
.to contain_exactly(private_message.id)
|
||||
|
||||
create_post(user: user, topic: group_message)
|
||||
|
||||
report = described_class.report(user_2)
|
||||
|
||||
expect(report.map(&:topic_id)).to contain_exactly(
|
||||
group_message.id,
|
||||
private_message.id
|
||||
)
|
||||
|
||||
state = report.first
|
||||
|
||||
expect(state.topic_id).to eq(private_message.id)
|
||||
expect(state.user_id).to eq(user_2.id)
|
||||
expect(state.last_read_post_number).to eq(nil)
|
||||
expect(state.notification_level).to eq(NotificationLevels.all[:watching])
|
||||
expect(state.highest_post_number).to eq(1)
|
||||
expect(state.group_ids).to eq([])
|
||||
|
||||
expect(report.last.group_ids).to contain_exactly(group.id)
|
||||
end
|
||||
|
||||
it 'returns the right tracking state when topics contain whispers' do
|
||||
TopicUser.find_by(user: user_2, topic: private_message).update!(
|
||||
last_read_post_number: 1
|
||||
)
|
||||
|
||||
create_post(
|
||||
raw: "this is a test post",
|
||||
topic: private_message,
|
||||
post_type: Post.types[:whisper],
|
||||
user: Fabricate(:admin)
|
||||
)
|
||||
|
||||
expect(described_class.report(user_2).map(&:topic_id))
|
||||
.to contain_exactly(group_message.id)
|
||||
|
||||
user_2.grant_admin!
|
||||
|
||||
tracking_state = described_class.report(user_2)
|
||||
|
||||
expect(tracking_state.map { |topic| [topic.topic_id, topic.highest_post_number] })
|
||||
.to contain_exactly(
|
||||
[group_message.id, 1],
|
||||
[private_message.id, 2]
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the right tracking state when topics have been dismissed' do
|
||||
DismissedTopicUser.create!(
|
||||
user_id: user_2.id,
|
||||
topic_id: group_message.id
|
||||
)
|
||||
|
||||
expect(described_class.report(user_2).map(&:topic_id))
|
||||
.to contain_exactly(private_message.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.publish_new' do
|
||||
it 'should publish the right message_bus message' do
|
||||
messages = MessageBus.track_publish do
|
||||
described_class.publish_new(private_message)
|
||||
end
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(
|
||||
described_class.user_channel(user.id),
|
||||
described_class.user_channel(user_2.id)
|
||||
)
|
||||
|
||||
data = messages.find do |message|
|
||||
message.channel == described_class.user_channel(user.id)
|
||||
end.data
|
||||
|
||||
expect(data['message_type']).to eq(described_class::NEW_MESSAGE_TYPE)
|
||||
end
|
||||
|
||||
it 'should publish the right message_bus message for a group message' do
|
||||
messages = MessageBus.track_publish do
|
||||
described_class.publish_new(group_message)
|
||||
end
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(
|
||||
described_class.group_channel(group.id),
|
||||
described_class.user_channel(user.id)
|
||||
)
|
||||
|
||||
data = messages.find do |message|
|
||||
message.channel == described_class.group_channel(group.id)
|
||||
end.data
|
||||
|
||||
expect(data['message_type']).to eq(described_class::NEW_MESSAGE_TYPE)
|
||||
expect(data['topic_id']).to eq(group_message.id)
|
||||
expect(data['payload']['last_read_post_number']).to eq(nil)
|
||||
expect(data['payload']['highest_post_number']).to eq(1)
|
||||
expect(data['payload']['group_ids']).to eq([group.id])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.publish_unread' do
|
||||
it 'should publish the right message_bus message' do
|
||||
messages = MessageBus.track_publish do
|
||||
described_class.publish_unread(private_message.first_post)
|
||||
end
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(
|
||||
described_class.user_channel(user.id),
|
||||
described_class.user_channel(user_2.id)
|
||||
)
|
||||
|
||||
data = messages.find do |message|
|
||||
message.channel == described_class.user_channel(user.id)
|
||||
end.data
|
||||
|
||||
expect(data['message_type']).to eq(described_class::UNREAD_MESSAGE_TYPE)
|
||||
expect(data['topic_id']).to eq(private_message.id)
|
||||
expect(data['payload']['last_read_post_number']).to eq(1)
|
||||
expect(data['payload']['highest_post_number']).to eq(1)
|
||||
expect(data['payload']['notification_level'])
|
||||
.to eq(NotificationLevels.all[:watching])
|
||||
expect(data['payload']['group_ids']).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.publish_user_archived' do
|
||||
it 'should publish the right message_bus message' do
|
||||
message = MessageBus.track_publish(described_class.user_channel(user.id)) do
|
||||
described_class.publish_user_archived(private_message, user.id)
|
||||
end.first
|
||||
|
||||
data = message.data
|
||||
|
||||
expect(data['topic_id']).to eq(private_message.id)
|
||||
expect(data['message_type']).to eq(described_class::ARCHIVE_MESSAGE_TYPE)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.publish_group_archived' do
|
||||
it 'should publish the right message_bus message' do
|
||||
user_3 = Fabricate(:user)
|
||||
group.add(user_3)
|
||||
|
||||
messages = MessageBus.track_publish do
|
||||
described_class.publish_group_archived(group_message, group.id)
|
||||
end
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(
|
||||
described_class.group_channel(group.id)
|
||||
)
|
||||
|
||||
data = messages.find do |message|
|
||||
message.channel == described_class.group_channel(group.id)
|
||||
end.data
|
||||
|
||||
expect(data['message_type']).to eq(described_class::GROUP_ARCHIVE_MESSAGE_TYPE)
|
||||
expect(data['topic_id']).to eq(group_message.id)
|
||||
expect(data['payload']['group_ids']).to contain_exactly(group.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -208,186 +208,6 @@ describe TopicTrackingState do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#publish_private_message' do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
|
||||
describe 'normal topic' do
|
||||
it 'should publish the right message' do
|
||||
allowed_users = private_message_topic.allowed_users
|
||||
|
||||
messages = MessageBus.track_publish do
|
||||
TopicTrackingState.publish_private_message(private_message_topic)
|
||||
end
|
||||
|
||||
expect(messages.count).to eq(1)
|
||||
|
||||
message = messages.first
|
||||
|
||||
expect(message.channel).to eq('/private-messages/inbox')
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to contain_exactly(*allowed_users.map(&:id))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'topic with groups' do
|
||||
fab!(:group1) { Fabricate(:group, users: [Fabricate(:user)]) }
|
||||
fab!(:group2) { Fabricate(:group, users: [Fabricate(:user), Fabricate(:user)]) }
|
||||
|
||||
before do
|
||||
[group1, group2].each do |group|
|
||||
private_message_topic.allowed_groups << group
|
||||
end
|
||||
end
|
||||
|
||||
it "should publish the right message" do
|
||||
messages = MessageBus.track_publish do
|
||||
TopicTrackingState.publish_private_message(
|
||||
private_message_topic
|
||||
)
|
||||
end
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(
|
||||
'/private-messages/inbox',
|
||||
"/private-messages/group/#{group1.name}/inbox",
|
||||
"/private-messages/group/#{group2.name}/inbox"
|
||||
)
|
||||
|
||||
message = messages.find do |m|
|
||||
m.channel == '/private-messages/inbox'
|
||||
end
|
||||
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to eq(private_message_topic.allowed_users.map(&:id))
|
||||
|
||||
[group1, group2].each do |group|
|
||||
message = messages.find do |m|
|
||||
m.channel == "/private-messages/group/#{group.name}/inbox"
|
||||
end
|
||||
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to eq(group.users.map(&:id))
|
||||
end
|
||||
end
|
||||
|
||||
describe "archiving topic" do
|
||||
it "should publish the right message" do
|
||||
messages = MessageBus.track_publish do
|
||||
TopicTrackingState.publish_private_message(
|
||||
private_message_topic,
|
||||
group_archive: true
|
||||
)
|
||||
end
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(
|
||||
'/private-messages/inbox',
|
||||
"/private-messages/group/#{group1.name}/inbox",
|
||||
"/private-messages/group/#{group1.name}/archive",
|
||||
"/private-messages/group/#{group2.name}/inbox",
|
||||
"/private-messages/group/#{group2.name}/archive",
|
||||
)
|
||||
|
||||
message = messages.find { |m| m.channel == '/private-messages/inbox' }
|
||||
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to eq(private_message_topic.allowed_users.map(&:id))
|
||||
|
||||
[group1, group2].each do |group|
|
||||
[
|
||||
"/private-messages/group/#{group.name}/inbox",
|
||||
"/private-messages/group/#{group.name}/archive"
|
||||
].each do |channel|
|
||||
message = messages.find { |m| m.channel == channel }
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to eq(group.users.map(&:id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'topic with new post' do
|
||||
let(:user) { private_message_topic.allowed_users.last }
|
||||
|
||||
let!(:post) do
|
||||
Fabricate(:post,
|
||||
topic: private_message_topic,
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
let!(:group) do
|
||||
group = Fabricate(:group, users: [Fabricate(:user)])
|
||||
private_message_topic.allowed_groups << group
|
||||
group
|
||||
end
|
||||
|
||||
it 'should publish the right message' do
|
||||
messages = MessageBus.track_publish do
|
||||
TopicTrackingState.publish_private_message(
|
||||
private_message_topic,
|
||||
post: post
|
||||
)
|
||||
end
|
||||
|
||||
expected_channels = [
|
||||
'/private-messages/inbox',
|
||||
'/private-messages/sent',
|
||||
"/private-messages/group/#{group.name}/inbox"
|
||||
]
|
||||
|
||||
expect(messages.map(&:channel)).to contain_exactly(*expected_channels)
|
||||
|
||||
expected_channels.zip([
|
||||
private_message_topic.allowed_users.map(&:id),
|
||||
[user.id],
|
||||
[group.users.first.id]
|
||||
]).each do |channel, user_ids|
|
||||
message = messages.find { |m| m.channel == channel }
|
||||
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to eq(user_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'archived topic' do
|
||||
it 'should publish the right message' do
|
||||
messages = MessageBus.track_publish do
|
||||
TopicTrackingState.publish_private_message(
|
||||
private_message_topic,
|
||||
archive_user_id: private_message_post.user_id,
|
||||
)
|
||||
end
|
||||
|
||||
expected_channels = [
|
||||
"/private-messages/archive",
|
||||
"/private-messages/inbox",
|
||||
"/private-messages/sent",
|
||||
]
|
||||
|
||||
expect(messages.map(&:channel)).to eq(expected_channels)
|
||||
|
||||
expected_channels.each do |channel|
|
||||
message = messages.find { |m| m.channel = channel }
|
||||
expect(message.data["topic_id"]).to eq(private_message_topic.id)
|
||||
expect(message.user_ids).to eq([private_message_post.user_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for a regular topic' do
|
||||
it 'should not publish any message' do
|
||||
topic.allowed_users << Fabricate(:user)
|
||||
|
||||
messages = MessageBus.track_publish do
|
||||
TopicTrackingState.publish_private_message(topic)
|
||||
end
|
||||
|
||||
expect(messages).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#publish_read_private_message' do
|
||||
fab!(:group) { Fabricate(:group) }
|
||||
let(:read_topic_key) { "/private-messages/unread-indicator/#{group_message.id}" }
|
||||
|
|
|
@ -3,20 +3,51 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe UserArchivedMessage do
|
||||
it 'Does not move archived muted messages back to inbox' do
|
||||
user = Fabricate(:admin)
|
||||
user2 = Fabricate(:admin)
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
|
||||
topic = create_post(user: user,
|
||||
fab!(:private_message) do
|
||||
create_post(
|
||||
user: user,
|
||||
skip_validations: true,
|
||||
target_usernames: [user2.username, user.username].join(","),
|
||||
archetype: Archetype.private_message).topic
|
||||
target_usernames: [user_2.username, user.username].join(","),
|
||||
archetype: Archetype.private_message
|
||||
).topic
|
||||
end
|
||||
|
||||
UserArchivedMessage.archive!(user.id, topic)
|
||||
expect(topic.message_archived?(user)).to eq(true)
|
||||
describe '.move_to_inbox!' do
|
||||
it 'moves topic back to inbox correctly' do
|
||||
UserArchivedMessage.archive!(user.id, private_message)
|
||||
|
||||
TopicUser.change(user.id, topic.id, notification_level: TopicUser.notification_levels[:muted])
|
||||
UserArchivedMessage.move_to_inbox!(user.id, topic)
|
||||
expect(topic.message_archived?(user)).to eq(true)
|
||||
expect do
|
||||
messages = MessageBus.track_publish(PrivateMessageTopicTrackingState.user_channel(user.id)) do
|
||||
UserArchivedMessage.move_to_inbox!(user.id, private_message)
|
||||
end
|
||||
|
||||
expect(messages.present?).to eq(true)
|
||||
end.to change { private_message.message_archived?(user) }.from(true).to(false)
|
||||
end
|
||||
|
||||
it 'does not move archived muted messages back to inbox' do
|
||||
UserArchivedMessage.archive!(user.id, private_message)
|
||||
|
||||
expect(private_message.message_archived?(user)).to eq(true)
|
||||
|
||||
TopicUser.change(user.id, private_message.id, notification_level: TopicUser.notification_levels[:muted])
|
||||
UserArchivedMessage.move_to_inbox!(user.id, private_message)
|
||||
|
||||
expect(private_message.message_archived?(user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.archive' do
|
||||
it 'archives message correctly' do
|
||||
messages = MessageBus.track_publish(PrivateMessageTopicTrackingState.user_channel(user.id)) do
|
||||
UserArchivedMessage.archive!(user.id, private_message)
|
||||
end
|
||||
|
||||
expect(messages.present?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -5064,6 +5064,43 @@ describe UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#private_message_topic_tracking_state" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:user_2) { Fabricate(:user) }
|
||||
|
||||
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 'does not allow an unauthorized user to access the state of another user' do
|
||||
get "/u/#{user.username}/private-message-topic-tracking-state.json"
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it 'returns the right response' do
|
||||
get "/u/#{user_2.username}/private-message-topic-tracking-state.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_state = response.parsed_body.first
|
||||
|
||||
expect(topic_state["topic_id"]).to eq(private_message.id)
|
||||
expect(topic_state["highest_post_number"]).to eq(1)
|
||||
expect(topic_state["last_read_post_number"]).to eq(nil)
|
||||
expect(topic_state["notification_level"]).to eq(NotificationLevels.all[:watching])
|
||||
expect(topic_state["group_ids"]).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
def create_second_factor_security_key
|
||||
sign_in(user)
|
||||
stub_secure_session_confirmed
|
||||
|
|
Loading…
Reference in New Issue