Merge pull request #5651 from tgxworld/live_update_group_messages

Live update group messages
This commit is contained in:
Guo Xiang Tan 2018-03-06 18:55:13 +08:00 committed by GitHub
commit 7068b90c01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 375 additions and 61 deletions

View File

@ -61,5 +61,11 @@ export default Ember.Component.extend({
}
return false;
}
}
},
actions: {
showInserted() {
this.sendAction('showInserted');
},
},
});

View File

@ -42,7 +42,7 @@ const controllerOpts = {
const tracker = this.topicTrackingState;
// Move inserted into topics
this.get('content').loadBefore(tracker.get('newIncoming'));
this.get('content').loadBefore(tracker.get('newIncoming'), true);
tracker.resetTracking();
return false;
},

View File

@ -33,7 +33,6 @@ export default Ember.Controller.extend({
return hasSelection && pmView !== "archive" && !archive;
},
bulkOperation(operation) {
const selected = this.get('selected');
var params = {type: operation};

View File

@ -1,18 +1,58 @@
import computed from 'ember-addons/ember-computed-decorators';
// Lists of topics on a user's page.
export default Ember.Controller.extend({
application: Ember.inject.controller(),
hideCategory: false,
showPosters: false,
newIncoming: [],
incomingCount: 0,
channel: null,
_showFooter: function() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
}.observes("model.canLoadMore"),
@computed('incomingCount')
hasIncoming(incomingCount) {
return incomingCount > 0;
},
subscribe(channel) {
this.set('channel', channel);
this.messageBus.subscribe(channel, data => {
if (this.get('newIncoming').indexOf(data.topic_id) === -1) {
this.get('newIncoming').push(data.topic_id);
this.incrementProperty('incomingCount');
}
});
},
unsubscribe() {
this.messageBus.unsubscribe(this.get('channel'));
this._resetTracking();
},
_resetTracking() {
this.setProperties({
"newIncoming": [],
"incomingCount": 0,
"channel": null,
});
},
actions: {
loadMore: function() {
this.get('model').loadMore();
}
},
showInserted() {
this.get('model').loadBefore(this.get('newIncoming'));
this._resetTracking();
return false;
},
},
});

View File

@ -70,14 +70,14 @@ const TopicList = RestModel.extend({
// loads topics with these ids "before" the current topics
loadBefore(topic_ids) {
loadBefore(topic_ids, storeInSession) {
const topicList = this,
topics = this.get('topics');
// refresh dupes
topics.removeObjects(topics.filter(topic => topic_ids.indexOf(topic.get('id')) >= 0));
const url = `${Discourse.getURL("/")}${this.get('filter')}?topic_ids=${topic_ids.join(",")}`;
const url = `${Discourse.getURL("/")}${this.get('filter')}.json?topic_ids=${topic_ids.join(",")}`;
const store = this.store;
return ajax({ url }).then(result => {
@ -88,7 +88,7 @@ const TopicList = RestModel.extend({
topics.insertAt(i,t);
i++;
});
Discourse.Session.currentProp('topicList', topicList);
if (storeInSession) Discourse.Session.currentProp('topicList', topicList);
});
}
});

View File

@ -1,6 +1,5 @@
import { NotificationLevels } from 'discourse/lib/notification-levels';
import computed from "ember-addons/ember-computed-decorators";
import { on } from "ember-addons/ember-computed-decorators";
import { default as computed, on } from "ember-addons/ember-computed-decorators";
import { defaultHomepage } from 'discourse/lib/utilities';
import PreloadStore from 'preload-store';
@ -35,7 +34,7 @@ const TopicTrackingState = Discourse.Model.extend({
tracker.incrementMessageCount();
}
if (data.message_type === "new_topic" || data.message_type === "latest") {
if (["new_topic", "latest"].includes(data.message_type)) {
const muted_category_ids = Discourse.User.currentProp("muted_category_ids");
if (_.include(muted_category_ids, data.payload.category_id)) {
return;
@ -55,7 +54,7 @@ const TopicTrackingState = Discourse.Model.extend({
tracker.notify(data);
}
if (data.message_type === "new_topic" || data.message_type === "unread" || data.message_type === "read") {
if (["new_topic", "unread", "read"].includes(data.message_type)) {
tracker.notify(data);
const old = tracker.states["t" + data.topic_id];
@ -123,11 +122,11 @@ const TopicTrackingState = Discourse.Model.extend({
}
}
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") {
if (["all", "latest", "new"].includes(filter) && data.message_type === "new_topic") {
this.addIncoming(data.topic_id);
}
if ((filter === "all" || filter === "unread") && data.message_type === "unread") {
if (["all", "unread"].includes(filter) && data.message_type === "unread") {
const old = this.states["t" + data.topic_id];
if(!old || old.highest_post_number === old.last_read_post_number) {
this.addIncoming(data.topic_id);

View File

@ -1,7 +1,7 @@
import UserTopicListRoute from "discourse/routes/user-topic-list";
// A helper to build a user topic list route
export default (viewName, path) => {
export default (viewName, path, channel) => {
return UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.messages_received,
@ -19,6 +19,10 @@ export default (viewName, path) => {
setupController() {
this._super.apply(this, arguments);
if (channel) {
this.controllerFor("user-topics-list").subscribe(`/private-messages/${channel}`);
}
this.controllerFor("user-topics-list").setProperties({
hideCategory: true,
showPosters: true,
@ -32,6 +36,8 @@ export default (viewName, path) => {
},
deactivate() {
this.controllerFor('user-topics-list').unsubscribe();
this.searchService.set(
'searchContext',
this.controllerFor("user").get("model.searchContext")

View File

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

View File

@ -22,5 +22,6 @@ export default createPMRoute('groups', 'private-messages-groups').extend({
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

@ -20,5 +20,6 @@ export default createPMRoute('groups', 'private-messages-groups').extend({
const group = _.last(model.get("filter").split('/'));
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";
export default createPMRoute('index', 'private-messages');
export default createPMRoute('index', 'private-messages', 'inbox');

View File

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

View File

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

View File

@ -4,7 +4,10 @@
showParticipants=showParticipants
showPosters=showPosters
bulkSelectEnabled=bulkSelectEnabled
selected=selected}}
selected=selected
hasIncoming=hasIncoming
incomingCount=incomingCount
showInserted="showInserted"}}
{{conditional-loading-spinner condition=model.loadingMore}}
{{/load-more}}

View File

@ -7,6 +7,15 @@
margin-right: 10px;
}
}
.paginated-topics-list {
position: relative;
}
.show-mores {
position: absolute;
width: 100%;
}
}
.user-main {

View File

@ -386,19 +386,19 @@ class TopicsController < ApplicationController
.where('topic_allowed_groups.group_id IN (?)', group_ids).pluck(:id)
allowed_groups.each do |id|
if archive
GroupArchivedMessage.archive!(id, topic.id)
GroupArchivedMessage.archive!(id, topic)
group_id = id
else
GroupArchivedMessage.move_to_inbox!(id, topic.id)
GroupArchivedMessage.move_to_inbox!(id, topic)
end
end
end
if topic.allowed_users.include?(current_user)
if archive
UserArchivedMessage.archive!(current_user.id, topic.id)
UserArchivedMessage.archive!(current_user.id, topic)
else
UserArchivedMessage.move_to_inbox!(current_user.id, topic.id)
UserArchivedMessage.move_to_inbox!(current_user.id, topic)
end
end

View File

@ -2,17 +2,21 @@ class GroupArchivedMessage < ActiveRecord::Base
belongs_to :user
belongs_to :topic
def self.move_to_inbox!(group_id, topic_id)
def self.move_to_inbox!(group_id, topic)
topic_id = topic.id
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, false)
end
def self.archive!(group_id, topic_id)
def self.archive!(group_id, topic)
topic_id = topic.id
GroupArchivedMessage.where(group_id: group_id, topic_id: topic_id).destroy_all
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, true)
end
def self.trigger(event, group_id, topic_id)
@ -23,6 +27,13 @@ class GroupArchivedMessage < ActiveRecord::Base
end
end
private
def self.publish_topic_tracking_state(topic, archived)
TopicTrackingState.publish_private_message(
topic, group_archived: archived
)
end
end
# == Schema Information

View File

@ -99,7 +99,7 @@ class Topic < ActiveRecord::Base
if: Proc.new { |t|
(t.new_record? || t.category_id_changed?) &&
!SiteSetting.allow_uncategorized_topics &&
(t.archetype.nil? || t.archetype == Archetype.default) &&
(t.archetype.nil? || t.regular?) &&
(!t.user_id || !t.user.staff?)
}
@ -273,7 +273,7 @@ class Topic < ActiveRecord::Base
end
def ensure_topic_has_a_category
if category_id.nil? && (archetype.nil? || archetype == Archetype.default)
if category_id.nil? && (archetype.nil? || self.regular?)
self.category_id = SiteSetting.uncategorized_category_id
end
end
@ -463,6 +463,10 @@ class Topic < ActiveRecord::Base
archetype == Archetype.private_message
end
def regular?
self.archetype == Archetype.default
end
MAX_SIMILAR_BODY_LENGTH ||= 200
def self.similar_to(title, raw, user = nil)

View File

@ -10,6 +10,8 @@ class TopicTrackingState
include ActiveModel::SerializerSupport
CHANNEL = "/user-tracking"
UNREAD_MESSAGE_TYPE = "unread".freeze
LATEST_MESSAGE_TYPE = "latest".freeze
attr_accessor :user_id,
:topic_id,
@ -20,6 +22,7 @@ class TopicTrackingState
:notification_level
def self.publish_new(topic)
return unless topic.regular?
message = {
topic_id: topic.id,
@ -41,14 +44,13 @@ class TopicTrackingState
end
def self.publish_latest(topic, staff_only = false)
return unless topic.archetype == "regular"
return unless topic.regular?
message = {
topic_id: topic.id,
message_type: "latest",
message_type: LATEST_MESSAGE_TYPE,
payload: {
bumped_at: topic.bumped_at,
topic_id: topic.id,
category_id: topic.category_id,
archetype: topic.archetype
}
@ -63,7 +65,12 @@ class TopicTrackingState
MessageBus.publish("/latest", message.as_json, group_ids: group_ids)
end
def self.unread_channel_key(user_id)
"/unread/#{user_id}"
end
def self.publish_unread(post)
return unless post.topic.regular?
# TODO at high scale we are going to have to defer this,
# perhaps cut down to users that are around in the last 7 days as well
@ -81,19 +88,18 @@ class TopicTrackingState
message = {
topic_id: post.topic_id,
message_type: "unread",
message_type: UNREAD_MESSAGE_TYPE,
payload: {
last_read_post_number: tu.last_read_post_number,
highest_post_number: post.post_number,
created_at: post.created_at,
topic_id: post.topic_id,
category_id: post.topic.category_id,
notification_level: tu.notification_level,
archetype: post.topic.archetype
}
}
MessageBus.publish("/unread/#{tu.user_id}", message.as_json, group_ids: group_ids)
MessageBus.publish(self.unread_channel_key(tu.user_id), message.as_json, group_ids: group_ids)
end
end
@ -103,10 +109,7 @@ class TopicTrackingState
message = {
topic_id: topic.id,
message_type: "recover",
payload: {
topic_id: topic.id,
}
message_type: "recover"
}
MessageBus.publish("/recover", message.as_json, group_ids: group_ids)
@ -118,17 +121,13 @@ class TopicTrackingState
message = {
topic_id: topic.id,
message_type: "delete",
payload: {
topic_id: topic.id,
}
message_type: "delete"
}
MessageBus.publish("/delete", message.as_json, group_ids: group_ids)
end
def self.publish_read(topic_id, last_read_post_number, user_id, notification_level = nil)
highest_post_number = Topic.where(id: topic_id).pluck(:highest_post_number).first
message = {
@ -142,8 +141,7 @@ class TopicTrackingState
}
}
MessageBus.publish("/unread/#{user_id}", message.as_json, user_ids: [user_id])
MessageBus.publish(self.unread_channel_key(user_id), message.as_json, user_ids: [user_id])
end
def self.treat_as_new_topic_clause
@ -248,4 +246,42 @@ SQL
sql
end
def self.publish_private_message(topic, archived: false, post: nil, group_archived: false)
return unless topic.private_message?
channels = {}
allowed_user_ids = topic.allowed_users.pluck(:id)
if allowed_user_ids.include?(post&.user_id)
channels["/private-messages/sent"] = [post.user_id]
end
if archived
channels["/private-messages/archive"] = allowed_user_ids
else
topic.allowed_groups.each do |group|
channel = "/private-messages/group/#{group.name.downcase}"
channel = "#{channel}/archive" if group_archived
channels[channel] = group.users.pluck(:id)
end
end
if channels.except("/private-messages/sent").blank?
channels["/private-messages/inbox"] = allowed_user_ids
end
message = {
topic_id: topic.id
}
admin_ids = User.admins.human_users.pluck(:id)
channels.each do |channel, user_ids|
MessageBus.publish(
channel,
message.as_json,
user_ids: user_ids | admin_ids
)
end
end
end

View File

@ -2,7 +2,9 @@ class UserArchivedMessage < ActiveRecord::Base
belongs_to :user
belongs_to :topic
def self.move_to_inbox!(user_id, topic_id)
def self.move_to_inbox!(user_id, topic)
topic_id = topic.id
return if (TopicUser.where(
user_id: user_id,
topic_id: topic_id,
@ -12,13 +14,16 @@ class UserArchivedMessage < ActiveRecord::Base
UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all
trigger(:move_to_inbox, user_id, topic_id)
MessageBus.publish("/topic/#{topic_id}", { type: "move_to_inbox" }, user_ids: [user_id])
publish_topic_tracking_state(topic, false)
end
def self.archive!(user_id, topic_id)
def self.archive!(user_id, topic)
topic_id = topic.id
UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all
UserArchivedMessage.create!(user_id: user_id, topic_id: topic_id)
trigger(:archive_message, user_id, topic_id)
MessageBus.publish("/topic/#{topic_id}", { type: "archived" }, user_ids: [user_id])
publish_topic_tracking_state(topic, true)
end
def self.trigger(event, user_id, topic_id)
@ -28,6 +33,14 @@ class UserArchivedMessage < ActiveRecord::Base
DiscourseEvent.trigger(event, user: user, topic: topic)
end
end
private
def self.publish_topic_tracking_state(topic, archived)
TopicTrackingState.publish_private_message(
topic, archived: archived
)
end
end
# == Schema Information

View File

@ -365,11 +365,11 @@ class PostCreator
return unless @topic.private_message? && @topic.id
UserArchivedMessage.where(topic_id: @topic.id).pluck(:user_id).each do |user_id|
UserArchivedMessage.move_to_inbox!(user_id, @topic.id)
UserArchivedMessage.move_to_inbox!(user_id, @topic)
end
GroupArchivedMessage.where(topic_id: @topic.id).pluck(:group_id).each do |group_id|
GroupArchivedMessage.move_to_inbox!(group_id, @topic.id)
GroupArchivedMessage.move_to_inbox!(group_id, @topic)
end
end

View File

@ -17,6 +17,10 @@ class PostJobsEnqueuer
after_post_create
after_topic_create
end
if @topic.private_message?
TopicTrackingState.publish_private_message(@topic, post: @post)
end
end
private
@ -35,7 +39,7 @@ class PostJobsEnqueuer
def after_post_create
TopicTrackingState.publish_unread(@post) if @post.post_number > 1
TopicTrackingState.publish_latest(@topic, @post.post_type == Post.types[:whisper])
TopicTrackingState.publish_latest(@topic, @post.whisper?)
Jobs.enqueue_in(
SiteSetting.email_time_window_mins.minutes,

View File

@ -43,9 +43,9 @@ class TopicsBulkAction
topics.each do |t|
if guardian.can_see?(t) && t.private_message?
if group
GroupArchivedMessage.move_to_inbox!(group.id, t.id)
GroupArchivedMessage.move_to_inbox!(group.id, t)
else
UserArchivedMessage.move_to_inbox!(@user.id, t.id)
UserArchivedMessage.move_to_inbox!(@user.id, t)
end
end
end
@ -56,9 +56,9 @@ class TopicsBulkAction
topics.each do |t|
if guardian.can_see?(t) && t.private_message?
if group
GroupArchivedMessage.archive!(group.id, t.id)
GroupArchivedMessage.archive!(group.id, t)
else
UserArchivedMessage.archive!(@user.id, t.id)
UserArchivedMessage.archive!(@user.id, t)
end
end
end

View File

@ -10,9 +10,182 @@ describe TopicTrackingState do
create_post
end
it "can correctly publish unread" do
# TODO setup stuff and look at messages
TopicTrackingState.publish_unread(post)
let(:topic) { post.topic }
let(:private_message_post) { Fabricate(:private_message_post) }
let(:private_message_topic) { private_message_post.topic }
describe '#publish_latest' do
it 'can correctly publish latest' do
message = MessageBus.track_publish("/latest") do
described_class.publish_latest(topic)
end.first
data = message.data
expect(data["topic_id"]).to eq(topic.id)
expect(data["message_type"]).to eq(described_class::LATEST_MESSAGE_TYPE)
expect(data["payload"]["archetype"]).to eq(Archetype.default)
end
describe 'private message' do
it 'should not publish any message' do
messages = MessageBus.track_publish do
described_class.publish_latest(private_message_topic)
end
expect(messages).to eq([])
end
end
end
describe '#publish_unread' do
it "can correctly publish unread" do
message = MessageBus.track_publish(described_class.unread_channel_key(post.user.id)) do
TopicTrackingState.publish_unread(post)
end.first
data = message.data
expect(data["topic_id"]).to eq(topic.id)
expect(data["message_type"]).to eq(described_class::UNREAD_MESSAGE_TYPE)
expect(data["payload"]["archetype"]).to eq(Archetype.default)
end
describe 'for a private message' do
before do
TopicUser.change(
private_message_topic.allowed_users.first.id,
private_message_topic.id,
notification_level: TopicUser.notification_levels[:tracking]
)
end
it 'should not publish any message' do
messages = MessageBus.track_publish do
TopicTrackingState.publish_unread(private_message_post)
end
expect(messages).to eq([])
end
end
end
describe '#publish_private_message' do
let!(: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 eq(allowed_users.map(&:id) << admin.id)
end
end
describe 'topic with groups' do
let(:group1) { Fabricate(:group, users: [Fabricate(:user)]) }
let(: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
[group1, group2].each do |group|
message = messages.find do |message|
message.channel == "/private-messages/group/#{group.name}"
end
expect(message.data["topic_id"]).to eq(private_message_topic.id)
expect(message.user_ids).to eq(group.users.map(&:id) << admin.id)
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
it 'should publish the right message' do
messages = MessageBus.track_publish do
TopicTrackingState.publish_private_message(
private_message_topic,
post: post
)
end
expect(messages.count).to eq(2)
[
['/private-messages/inbox', private_message_topic.allowed_users.map(&:id)],
['/private-messages/sent', [user.id]]
].each do |channel, user_ids|
message = messages.find do |message|
message.channel == channel
end
expect(message.data["topic_id"]).to eq(private_message_topic.id)
expect(message.user_ids).to eq(user_ids << admin.id)
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,
archived: true
)
end
expect(messages.count).to eq(1)
message = messages.first
expect(message.channel).to eq('/private-messages/archive')
expect(message.data["topic_id"]).to eq(private_message_topic.id)
expect(message.user_ids).to eq(
private_message_topic.allowed_users.map(&:id) << admin.id
)
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
it "correctly handles muted categories" do

View File

@ -10,11 +10,11 @@ describe UserArchivedMessage do
target_usernames: [user2.username, user.username].join(","),
archetype: Archetype.private_message).topic
UserArchivedMessage.archive!(user.id, topic.id)
UserArchivedMessage.archive!(user.id, topic)
expect(topic.message_archived?(user)).to eq(true)
TopicUser.change(user.id, topic.id, notification_level: TopicUser.notification_levels[:muted])
UserArchivedMessage.move_to_inbox!(user.id, topic.id)
UserArchivedMessage.move_to_inbox!(user.id, topic)
expect(topic.message_archived?(user)).to eq(true)
end
end

View File

@ -762,10 +762,10 @@ describe UserMerger do
Fabricate.build(:topic_allowed_user, user: source_user)
])
UserArchivedMessage.archive!(source_user.id, pm_topic1.id)
UserArchivedMessage.archive!(target_user.id, pm_topic1.id)
UserArchivedMessage.archive!(source_user.id, pm_topic2.id)
UserArchivedMessage.archive!(walter.id, pm_topic2.id)
UserArchivedMessage.archive!(source_user.id, pm_topic1)
UserArchivedMessage.archive!(target_user.id, pm_topic1)
UserArchivedMessage.archive!(source_user.id, pm_topic2)
UserArchivedMessage.archive!(walter.id, pm_topic2)
merge_users!