FEATURE: Display new/unread count in browse more messages for PMs. (#14188)

In order to include the new/unread count in the browse more message
under suggested topics, a couple of technical changes have to be made.

1. `PrivateMessageTopicTrackingState` is now auto-injected which is
   similar to how it is done for `TopicTrackingState`. This is done so
we don't have to attempt to pass the `PrivateMessageTopicTrackingState`
object multiple levels down into the suggested-topics component. While
the object is auto-injected, we only fetch the initial state and start
tracking when the relevant private messages routes has been hit and only
when a private message's suggested topics is loaded. This is
done as we do not want to add the extra overhead of fetching the inital
state to all page loads but instead wait till the private messages
routes are hit.

2. Previously, we would stop tracking once the `user-private-messages`
   route has been deactivated. However, that is not ideal since
navigating out of the route and back means we send an API call to the
server each time. Since `PrivateMessageTopicTrackingState` is kept in
sync cheaply via messageBus, we can just continue to track the state
even if the user has navigated away from the relevant stages.
This commit is contained in:
Alan Guo Xiang Tan 2021-09-07 12:30:40 +08:00 committed by GitHub
parent cb63c297b3
commit fc1fd1b416
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 423 additions and 134 deletions

View File

@ -5,6 +5,7 @@ import Site from "discourse/models/site";
import { categoryBadgeHTML } from "discourse/helpers/category-link"; import { categoryBadgeHTML } from "discourse/helpers/category-link";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({ export default Component.extend({
tagName: "", tagName: "",
@ -18,13 +19,68 @@ export default Component.extend({
} }
}), }),
@discourseComputed("topic", "topicTrackingState.messageCount") @discourseComputed(
"topic",
"pmTopicTrackingState.isTracking",
"pmTopicTrackingState.statesModificationCounter",
"topicTrackingState.messageCount"
)
browseMoreMessage(topic) { browseMoreMessage(topic) {
// TODO decide what to show for pms return topic.isPrivateMessage
if (topic.get("isPrivateMessage")) { ? this._privateMessageBrowseMoreMessage(topic)
return; : this._topicBrowseMoreMessage(topic);
} },
_privateMessageBrowseMoreMessage(topic) {
const username = this.currentUser.username;
const suggestedGroupName = topic.suggested_group_name;
const inboxFilter = suggestedGroupName ? "group" : "user";
const unreadCount = this.pmTopicTrackingState.lookupCount("unread", {
inboxFilter: inboxFilter,
groupName: suggestedGroupName,
});
const newCount = this.pmTopicTrackingState.lookupCount("new", {
inboxFilter: inboxFilter,
groupName: suggestedGroupName,
});
if (unreadCount + newCount > 0) {
const hasBoth = unreadCount > 0 && newCount > 0;
if (suggestedGroupName) {
return I18n.messageFormat("user.messages.read_more_group_pm_MF", {
BOTH: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username: username,
groupName: suggestedGroupName,
groupLink: this._groupLink(username, suggestedGroupName),
basePath: getURL(""),
});
} else {
return I18n.messageFormat("user.messages.read_more_personal_pm_MF", {
BOTH: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username,
basePath: getURL(""),
});
}
} else if (suggestedGroupName) {
return I18n.t("user.messages.read_more_in_group", {
groupLink: this._groupLink(username, suggestedGroupName),
});
} else {
return I18n.t("user.messages.read_more", {
basePath: getURL(""),
username,
});
}
},
_topicBrowseMoreMessage(topic) {
const opts = { const opts = {
latestLink: `<a href="${getURL("/latest")}">${I18n.t( latestLink: `<a href="${getURL("/latest")}">${I18n.t(
"topic.view_latest_topics" "topic.view_latest_topics"
@ -50,8 +106,13 @@ export default Component.extend({
"</a>"; "</a>";
} }
const unreadTopics = this.topicTrackingState.countUnread(); let unreadTopics = 0;
const newTopics = this.currentUser ? this.topicTrackingState.countNew() : 0; let newTopics = 0;
if (this.currentUser) {
unreadTopics = this.topicTrackingState.countUnread();
newTopics = this.topicTrackingState.countNew();
}
if (newTopics + unreadTopics > 0) { if (newTopics + unreadTopics > 0) {
const hasBoth = unreadTopics > 0 && newTopics > 0; const hasBoth = unreadTopics > 0 && newTopics > 0;
@ -71,4 +132,10 @@ export default Component.extend({
return I18n.t("topic.read_more", opts); return I18n.t("topic.read_more", opts);
} }
}, },
_groupLink(username, groupName) {
return `<a class="group-link" href="${getURL(
`/u/${username}/messages/group/${groupName}`
)}">${iconHTML("users")} ${groupName}</a>`;
},
}); });

View File

@ -18,7 +18,6 @@ export default Controller.extend(BulkTopicSelection, {
showPosters: false, showPosters: false,
channel: null, channel: null,
tagsForUser: null, tagsForUser: null,
pmTopicTrackingState: null,
incomingCount: reads("pmTopicTrackingState.newIncoming.length"), incomingCount: reads("pmTopicTrackingState.newIncoming.length"),
@discourseComputed("emptyState", "model.topics.length", "incomingCount") @discourseComputed("emptyState", "model.topics.length", "incomingCount")
@ -46,15 +45,11 @@ export default Controller.extend(BulkTopicSelection, {
}, },
subscribe() { subscribe() {
this.pmTopicTrackingState?.trackIncoming( this.pmTopicTrackingState.trackIncoming(this.inbox, this.filter);
this.inbox,
this.filter,
this.group
);
}, },
unsubscribe() { unsubscribe() {
this.pmTopicTrackingState?.resetTracking(); this.pmTopicTrackingState.resetIncomingTracking();
}, },
@action @action
@ -85,7 +80,7 @@ export default Controller.extend(BulkTopicSelection, {
@action @action
showInserted() { showInserted() {
this.model.loadBefore(this.pmTopicTrackingState.newIncoming); this.model.loadBefore(this.pmTopicTrackingState.newIncoming);
this.pmTopicTrackingState.resetTracking(); this.pmTopicTrackingState.resetIncomingTracking();
return false; return false;
}, },
}); });

View File

@ -1076,9 +1076,7 @@ export default RestModel.extend({
const store = this.store; const store = this.store;
return ajax(url, { data }).then((result) => { return ajax(url, { data }).then((result) => {
if (result.suggested_topics) { this._setSuggestedTopics(result);
this.set("topic.suggested_topics", result.suggested_topics);
}
const posts = get(result, "post_stream.posts"); const posts = get(result, "post_stream.posts");
@ -1124,9 +1122,7 @@ export default RestModel.extend({
data, data,
headers, headers,
}).then((result) => { }).then((result) => {
if (result.suggested_topics) { this._setSuggestedTopics(result);
this.set("topic.suggested_topics", result.suggested_topics);
}
const posts = get(result, "post_stream.posts"); const posts = get(result, "post_stream.posts");
@ -1245,4 +1241,17 @@ export default RestModel.extend({
} }
} }
}, },
_setSuggestedTopics(result) {
if (!result.suggested_topics) {
return;
}
this.topic.setProperties({
suggested_topics: result.suggested_topics,
suggested_group_name: result.suggested_group_name,
});
this.pmTopicTrackingState.startTracking();
},
}); });

View File

@ -1,4 +1,7 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { on } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { import {
ARCHIVE_FILTER, ARCHIVE_FILTER,
INBOX_FILTER, INBOX_FILTER,
@ -15,20 +18,33 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
filter: null, filter: null,
activeGroup: null, activeGroup: null,
startTracking(data) { @on("init")
_setup() {
this.states = new Map(); this.states = new Map();
this.statesModificationCounter = 0;
this.isTracking = false;
this.newIncoming = []; this.newIncoming = [];
this._loadStates(data);
this.establishChannels();
}, },
establishChannels() { startTracking() {
if (this.isTracking) {
return;
}
this._establishChannels();
this._loadInitialState().finally(() => {
this.set("isTracking", true);
});
},
_establishChannels() {
this.messageBus.subscribe( this.messageBus.subscribe(
this._userChannel(this.user.id), this._userChannel(),
this._processMessage.bind(this) this._processMessage.bind(this)
); );
this.user.groupsWithMessages?.forEach((group) => { this.currentUser.groupsWithMessages?.forEach((group) => {
this.messageBus.subscribe( this.messageBus.subscribe(
this._groupChannel(group.id), this._groupChannel(group.id),
this._processMessage.bind(this) this._processMessage.bind(this)
@ -36,26 +52,21 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
}); });
}, },
stopTracking() { lookupCount(type, opts = {}) {
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; const typeFilterFn = type === "new" ? this._isNew : this._isUnread;
const inbox = opts.inboxFilter || this.inbox;
let filterFn; let filterFn;
if (this.inbox === "user") { if (inbox === "user") {
filterFn = this._isPersonal.bind(this); filterFn = this._isPersonal.bind(this);
} else if (this.inbox === "group") { } else if (inbox === "group") {
filterFn = this._isGroup.bind(this); filterFn = this._isGroup.bind(this);
} }
return Array.from(this.states.values()).filter((topic) => { return Array.from(this.states.values()).filter((topic) => {
return typeFilterFn(topic) && (!filterFn || filterFn(topic)); return (
typeFilterFn(topic) && (!filterFn || filterFn(topic, opts.groupName))
);
}).length; }).length;
}, },
@ -63,14 +74,14 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
this.setProperties({ inbox, filter, activeGroup: group }); this.setProperties({ inbox, filter, activeGroup: group });
}, },
resetTracking() { resetIncomingTracking() {
if (this.inbox) { if (this.inbox) {
this.set("newIncoming", []); this.set("newIncoming", []);
} }
}, },
_userChannel(userId) { _userChannel() {
return `${this.CHANNEL_PREFIX}/user/${userId}`; return `${this.CHANNEL_PREFIX}/user/${this.currentUser.id}`;
}, },
_groupChannel(groupId) { _groupChannel(groupId) {
@ -95,9 +106,9 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
}, },
_isPersonal(topic) { _isPersonal(topic) {
const groups = this.user.groups; const groups = this.currentUser?.groups;
if (groups.length === 0) { if (!groups || groups.length === 0) {
return true; return true;
} }
@ -106,10 +117,10 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
}); });
}, },
_isGroup(topic) { _isGroup(topic, activeGroupName) {
return this.user.groups.some((group) => { return this.currentUser.groups.some((group) => {
return ( return (
group.name === this.activeGroup.name && group.name === (activeGroupName || this.activeGroup.name) &&
topic.group_ids?.includes(group.id) topic.group_ids?.includes(group.id)
); );
}); });
@ -182,14 +193,24 @@ const PrivateMessageTopicTrackingState = EmberObject.extend({
} }
}, },
_loadStates(data) { _loadInitialState() {
(data || []).forEach((topic) => { return ajax(
this._modifyState(topic.topic_id, topic); `/u/${this.currentUser.username}/private-message-topic-tracking-state`
}); )
.then((pmTopicTrackingStateData) => {
pmTopicTrackingStateData.forEach((topic) => {
this._modifyState(topic.topic_id, topic, { skipIncrement: true });
});
})
.catch(popupAjaxError);
}, },
_modifyState(topicId, data) { _modifyState(topicId, data, opts = {}) {
this.states.set(topicId, data); this.states.set(topicId, data);
if (!opts.skipIncrement) {
this.incrementProperty("statesModificationCounter");
}
}, },
}); });

View File

@ -1,6 +1,7 @@
import TopicTrackingState, { import TopicTrackingState, {
startTracking, startTracking,
} from "discourse/models/topic-tracking-state"; } from "discourse/models/topic-tracking-state";
import PrivateMessageTopicTrackingState from "discourse/models/private-message-topic-tracking-state";
import DiscourseLocation from "discourse/lib/discourse-location"; import DiscourseLocation from "discourse/lib/discourse-location";
import KeyValueStore from "discourse/lib/key-value-store"; import KeyValueStore from "discourse/lib/key-value-store";
import MessageBus from "message-bus-client"; import MessageBus from "message-bus-client";
@ -50,19 +51,30 @@ export default {
app.register("current-user:main", currentUser, { instantiate: false }); app.register("current-user:main", currentUser, { instantiate: false });
app.currentUser = currentUser; app.currentUser = currentUser;
ALL_TARGETS.forEach((t) => ALL_TARGETS.forEach((t) => {
app.inject(t, "topicTrackingState", "topic-tracking-state:main") app.inject(t, "topicTrackingState", "topic-tracking-state:main");
); app.inject(t, "pmTopicTrackingState", "pm-topic-tracking-state:main");
});
const topicTrackingState = TopicTrackingState.create({ const topicTrackingState = TopicTrackingState.create({
messageBus: MessageBus, messageBus: MessageBus,
siteSettings, siteSettings,
currentUser, currentUser,
}); });
app.register("topic-tracking-state:main", topicTrackingState, { app.register("topic-tracking-state:main", topicTrackingState, {
instantiate: false, instantiate: false,
}); });
const pmTopicTrackingState = PrivateMessageTopicTrackingState.create({
messageBus: MessageBus,
currentUser,
});
app.register("pm-topic-tracking-state:main", pmTopicTrackingState, {
instantiate: false,
});
const site = Site.current(); const site = Site.current();
app.register("site:main", site, { instantiate: false }); app.register("site:main", site, { instantiate: false });

View File

@ -61,8 +61,6 @@ export default (inboxType, path, filter) => {
filter: filter, filter: filter,
group: null, group: null,
inbox: inboxType, inbox: inboxType,
pmTopicTrackingState:
userPrivateMessagesController.pmTopicTrackingState,
emptyState: this.emptyState(), emptyState: this.emptyState(),
}); });

View File

@ -34,6 +34,14 @@ export default DiscourseRoute.extend({
}); });
}, },
afterModel() {
const topic = this.modelFor("topic");
if (topic.isPrivateMessage && topic.suggested_topics) {
this.pmTopicTrackingState.startTracking();
}
},
deactivate() { deactivate() {
this._super(...arguments); this._super(...arguments);
this.controllerFor("topic").unsubscribe(); this.controllerFor("topic").unsubscribe();

View File

@ -1,9 +1,6 @@
import Composer from "discourse/models/composer"; import Composer from "discourse/models/composer";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import Draft from "discourse/models/draft"; 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({ export default DiscourseRoute.extend({
renderTemplate() { renderTemplate() {
@ -11,36 +8,15 @@ export default DiscourseRoute.extend({
}, },
model() { model() {
const user = this.modelFor("user"); return this.modelFor("user");
return ajax(`/u/${user.username}/private-message-topic-tracking-state`) },
.then((pmTopicTrackingStateData) => {
return { afterModel() {
user, return this.pmTopicTrackingState.startTracking();
pmTopicTrackingStateData,
};
})
.catch((e) => {
popupAjaxError(e);
return { user };
});
}, },
setupController(controller, model) { setupController(controller, model) {
const user = model.user; controller.set("model", model);
const pmTopicTrackingState = PrivateMessageTopicTrackingState.create({
messageBus: controller.messageBus,
user,
});
pmTopicTrackingState.startTracking(model.pmTopicTrackingStateData);
controller.setProperties({
model: user,
pmTopicTrackingState,
});
this.set("pmTopicTrackingState", pmTopicTrackingState);
if (this.currentUser) { if (this.currentUser) {
const composerController = this.controllerFor("composer"); const composerController = this.controllerFor("composer");
@ -58,10 +34,6 @@ export default DiscourseRoute.extend({
} }
}, },
deactivate() {
this.pmTopicTrackingState.stopTracking();
},
actions: { actions: {
refresh() { refresh() {
this.refresh(); this.refresh();

View File

@ -10,6 +10,7 @@ import {
} from "discourse/tests/helpers/qunit-helpers"; } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper"; import selectKit from "discourse/tests/helpers/select-kit-helper";
import { PERSONAL_INBOX } from "discourse/controllers/user-private-messages"; import { PERSONAL_INBOX } from "discourse/controllers/user-private-messages";
import { fixturesByUrl } from "discourse/tests/helpers/create-pretender";
acceptance( acceptance(
"User Private Messages - user with no group messages", "User Private Messages - user with no group messages",
@ -47,7 +48,11 @@ acceptance(
let fetchUserNew; let fetchUserNew;
let fetchedGroupNew; let fetchedGroupNew;
needs.user(); needs.user({
id: 5,
username: "charlie",
groups: [{ id: 14, name: "awesome_group", has_messages: true }],
});
needs.site({ needs.site({
can_tag_pms: true, can_tag_pms: true,
@ -60,6 +65,12 @@ acceptance(
}); });
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
server.get("/t/13.json", () => {
const response = { ...fixturesByUrl["/t/12/1.json"] };
response.suggested_group_name = "awesome_group";
return helper.response(response);
});
server.get("/topics/private-messages-all/:username.json", () => { server.get("/topics/private-messages-all/:username.json", () => {
return helper.response({ return helper.response({
topic_list: { topic_list: {
@ -159,46 +170,86 @@ acceptance(
}); });
}); });
const publishUnreadToMessageBus = function (group_ids) { const publishUnreadToMessageBus = function (opts = {}) {
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( publishToMessageBus(
`/private-message-topic-tracking-state/group/${group_ids[0]}`, `/private-message-topic-tracking-state/user/${opts.userId || 5}`,
{
topic_id: Math.random(),
message_type: "unread",
payload: {
last_read_post_number: 1,
highest_post_number: 2,
notification_level: 2,
group_ids: opts.groupIds || [],
},
}
);
};
const publishNewToMessageBus = function (opts = {}) {
publishToMessageBus(
`/private-message-topic-tracking-state/user/${opts.userId || 5}`,
{
topic_id: Math.random(),
message_type: "new_topic",
payload: {
last_read_post_number: null,
highest_post_number: 1,
group_ids: opts.groupIds || [],
},
}
);
};
const publishArchiveToMessageBus = function (userId) {
publishToMessageBus(
`/private-message-topic-tracking-state/user/${userId || 5}`,
{
topic_id: Math.random(),
message_type: "archive",
}
);
};
const publishGroupArchiveToMessageBus = function (groupIds) {
publishToMessageBus(
`/private-message-topic-tracking-state/group/${groupIds[0]}`,
{ {
topic_id: Math.random(), topic_id: Math.random(),
message_type: "group_archive", message_type: "group_archive",
payload: { payload: {
group_ids: group_ids, group_ids: groupIds,
},
}
);
};
const publishGroupUnreadToMessageBus = function (groupIds) {
publishToMessageBus(
`/private-message-topic-tracking-state/group/${groupIds[0]}`,
{
topic_id: Math.random(),
message_type: "unread",
payload: {
last_read_post_number: 1,
highest_post_number: 2,
notification_level: 2,
group_ids: groupIds || [],
},
}
);
};
const publishGroupNewToMessageBus = function (groupIds) {
publishToMessageBus(
`/private-message-topic-tracking-state/group/${groupIds[0]}`,
{
topic_id: Math.random(),
message_type: "new_topic",
payload: {
last_read_post_number: null,
highest_post_number: 1,
group_ids: groupIds || [],
}, },
} }
); );
@ -332,8 +383,8 @@ acceptance(
test("incoming unread messages while viewing group unread", async function (assert) { test("incoming unread messages while viewing group unread", async function (assert) {
await visit("/u/charlie/messages/group/awesome_group/unread"); await visit("/u/charlie/messages/group/awesome_group/unread");
publishUnreadToMessageBus([14]); publishUnreadToMessageBus({ groupIds: [14] });
publishNewToMessageBus([14]); publishNewToMessageBus({ groupIds: [14] });
await visit("/u/charlie/messages/group/awesome_group/unread"); // wait for re-render await visit("/u/charlie/messages/group/awesome_group/unread"); // wait for re-render
@ -544,6 +595,74 @@ acceptance(
"does not display the tags filter" "does not display the tags filter"
); );
}); });
test("suggested messages without new or unread", async function (assert) {
await visit("/t/12");
assert.equal(
query(".suggested-topics-message").innerText.trim(),
"Want to read more? Browse other messages in personal messages.",
"displays the right browse more message"
);
});
test("suggested messages with new and unread", async function (assert) {
await visit("/t/12");
publishNewToMessageBus({ userId: 5 });
await visit("/t/12"); // await re-render
assert.equal(
query(".suggested-topics-message").innerText.trim(),
"There is 1 new message remaining, or browse other personal messages",
"displays the right browse more message"
);
publishUnreadToMessageBus({ userId: 5 });
await visit("/t/12"); // await re-render
assert.equal(
query(".suggested-topics-message").innerText.trim(),
"There is 1 unread and 1 new message remaining, or browse other personal messages",
"displays the right browse more message"
);
});
test("suggested messages for group messages without new or unread", async function (assert) {
await visit("/t/13");
assert.equal(
query(".suggested-topics-message").innerText.trim(),
"Want to read more? Browse other messages in awesome_group.",
"displays the right browse more message"
);
});
test("suggested messages for group messages with new and unread", async function (assert) {
await visit("/t/13");
publishGroupNewToMessageBus([14]);
await visit("/t/13"); // await re-render
assert.equal(
query(".suggested-topics-message").innerText.trim(),
"There is 1 new message remaining, or browse other messages in awesome_group",
"displays the right browse more message"
);
publishGroupUnreadToMessageBus([14]);
await visit("/t/13"); // await re-render
assert.equal(
query(".suggested-topics-message").innerText.trim(),
"There is 1 unread and 1 new message remaining, or browse other messages in awesome_group",
"displays the right browse more message"
);
});
} }
); );

View File

@ -69,8 +69,8 @@ class CurrentUserSerializer < BasicUserSerializer
def groups def groups
owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set
object.visible_groups.pluck(:id, :name).map do |id, name| object.visible_groups.pluck(:id, :name, :has_messages).map do |id, name, has_messages|
group = { id: id, name: name } group = { id: id, name: name, has_messages: has_messages }
group[:owner] = true if owned_group_ids.include?(id) group[:owner] = true if owned_group_ids.include?(id)
group group
end end

View File

@ -4,6 +4,7 @@ module SuggestedTopicsMixin
def self.included(klass) def self.included(klass)
klass.attributes :related_messages klass.attributes :related_messages
klass.attributes :suggested_topics klass.attributes :suggested_topics
klass.attributes :suggested_group_name
end end
def include_related_messages? def include_related_messages?
@ -16,6 +17,24 @@ module SuggestedTopicsMixin
object.next_page.nil? && object.suggested_topics&.topics object.next_page.nil? && object.suggested_topics&.topics
end end
def include_suggested_group_name?
return false unless include_suggested_topics?
object.topic.private_message? && scope.user
end
def suggested_group_name
return if object.topic.topic_allowed_users.exists?(user_id: scope.user.id)
if object.topic_allowed_group_ids.present?
Group.joins(:group_users)
.where(
"group_users.group_id IN (?) AND group_users.user_id = ?",
object.topic_allowed_group_ids, scope.user.id
)
.pluck_first(:name)
end
end
def related_messages def related_messages
object.related_messages.topics.map do |t| object.related_messages.topics.map do |t|
SuggestedTopicSerializer.new(t, scope: scope, root: false) SuggestedTopicSerializer.new(t, scope: scope, root: false)

View File

@ -1204,6 +1204,38 @@ en:
failed_to_move: "Failed to move selected messages (perhaps your network is down)" failed_to_move: "Failed to move selected messages (perhaps your network is down)"
tags: "Tags" tags: "Tags"
warnings: "Official Warnings" warnings: "Official Warnings"
read_more_in_group: "Want to read more? Browse other messages in %{groupLink}."
read_more: "Want to read more? Browse other messages in <a href='%{basePath}/u/%{username}/messages/personal'>personal messages</a>."
read_more_group_pm_MF: "There {
UNREAD, plural,
=0 {}
one {
is <a href='{basePath}/u/{username}/messages/group/{groupName}/unread'># unread</a>
} other {
are <a href='{basePath}/u/{username}/messages/group/{groupName}/unread'># unread</a>
}
} {
NEW, plural,
=0 {}
one { {BOTH, select, true{and } false {is } other{}} <a href='{basePath}/u/{username}/messages/group/{groupName}/new'># new</a> message}
other { {BOTH, select, true{and } false {are } other{}} <a href='{basePath}/u/{username}/messages/group/{groupName}/new'># new</a> messages}
} remaining, or browse other messages in {groupLink}"
read_more_personal_pm_MF: "There {
UNREAD, plural,
=0 {}
one {
is <a href='{basePath}/u/{username}/messages/personal/unread'># unread</a>
} other {
are <a href='{basePath}/u/{username}/messages/personal/unread'># unread</a>
}
} {
NEW, plural,
=0 {}
one { {BOTH, select, true{and } false {is } other{}} <a href='{basePath}/u/{username}/messages/personal/new'># new</a> message}
other { {BOTH, select, true{and } false {are } other{}} <a href='{basePath}/u/{username}/messages/personal/new'># new</a> messages}
} remaining, or browse other <a href='{basePath}/u/{username}/messages/personal'>personal messages</a>"
preferences_nav: preferences_nav:
account: "Account" account: "Account"

View File

@ -464,11 +464,18 @@ class TopicView
end end
end end
def topic_allowed_group_ids
@topic_allowed_group_ids ||= begin
@topic.allowed_groups.map(&:id)
end
end
def group_allowed_user_ids def group_allowed_user_ids
return @group_allowed_user_ids unless @group_allowed_user_ids.nil? return @group_allowed_user_ids unless @group_allowed_user_ids.nil?
group_ids = @topic.allowed_groups.map(&:id) @group_allowed_user_ids = GroupUser
@group_allowed_user_ids = Set.new(GroupUser.where(group_id: group_ids).pluck('distinct user_id')) .where(group_id: topic_allowed_group_ids)
.pluck('distinct user_id')
end end
def category_group_moderator_user_ids def category_group_moderator_user_ids

View File

@ -135,7 +135,9 @@ RSpec.describe CurrentUserSerializer do
public_group.save! public_group.save!
payload = serializer.as_json payload = serializer.as_json
expect(payload[:groups]).to eq([{ id: public_group.id, name: public_group.name }]) expect(payload[:groups]).to contain_exactly(
{ id: public_group.id, name: public_group.name, has_messages: false }
)
end end
end end

View File

@ -151,6 +151,34 @@ describe TopicViewSerializer do
end end
end end
describe '#suggested_group_name' do
fab!(:pm) { Fabricate(:private_message_post).topic }
fab!(:group) { Fabricate(:group) }
it 'is nil for a regular topic' do
json = serialize_topic(topic, user)
expect(json[:suggested_group_name]).to eq(nil)
end
it 'is nil if user is an allowed user of the private message' do
pm.allowed_users << user
json = serialize_topic(pm, user)
expect(json[:suggested_group_name]).to eq(nil)
end
it 'returns the right group name if user is part of allowed group in the private message' do
pm.allowed_groups << group
group.add(user)
json = serialize_topic(pm, user)
expect(json[:suggested_group_name]).to eq(group.name)
end
end
describe 'when tags added to private message topics' do describe 'when tags added to private message topics' do
fab!(:moderator) { Fabricate(:moderator) } fab!(:moderator) { Fabricate(:moderator) }
fab!(:tag) { Fabricate(:tag) } fab!(:tag) { Fabricate(:tag) }