mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
FEATURE: Whisper posts
This commit is contained in:
parent
62cc029886
commit
5af0f5f80e
|
@ -3,6 +3,7 @@ import DiscourseURL from 'discourse/lib/url';
|
|||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
function loadDraft(store, opts) {
|
||||
opts = opts || {};
|
||||
|
@ -64,6 +65,11 @@ export default Ember.Controller.extend({
|
|||
this.set('similarTopics', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed('model.action')
|
||||
canWhisper(action) {
|
||||
return this.siteSettings.enable_whispers && action === Composer.REPLY;
|
||||
},
|
||||
|
||||
showWarning: function() {
|
||||
if (!Discourse.User.currentProp('staff')) { return false; }
|
||||
|
||||
|
@ -132,7 +138,6 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
hitEsc() {
|
||||
|
||||
const messages = this.get('controllers.composer-messages.model');
|
||||
if (messages.length) {
|
||||
messages.popObject();
|
||||
|
|
|
@ -24,6 +24,7 @@ const CLOSED = 'closed',
|
|||
category: 'categoryId',
|
||||
topic_id: 'topic.id',
|
||||
is_warning: 'isWarning',
|
||||
whisper: 'whisper',
|
||||
archetype: 'archetypeId',
|
||||
target_usernames: 'targetUsernames',
|
||||
typing_duration_msecs: 'typingTime',
|
||||
|
@ -557,6 +558,9 @@ const Composer = RestModel.extend({
|
|||
|
||||
let addedToStream = false;
|
||||
|
||||
const postTypes = this.site.get('post_types');
|
||||
const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular;
|
||||
|
||||
// Build the post object
|
||||
const createdPost = this.store.createRecord('post', {
|
||||
imageSizes: opts.imageSizes,
|
||||
|
@ -569,7 +573,7 @@ const Composer = RestModel.extend({
|
|||
user_title: user.get('title'),
|
||||
avatar_template: user.get('avatar_template'),
|
||||
user_custom_fields: user.get('custom_fields'),
|
||||
post_type: this.site.get('post_types.regular'),
|
||||
post_type: postType,
|
||||
actions_summary: [],
|
||||
moderator: user.get('moderator'),
|
||||
admin: user.get('admin'),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import RestModel from 'discourse/models/rest';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import ActionSummary from 'discourse/models/action-summary';
|
||||
import { url, fmt, propertyEqual } from 'discourse/lib/computed';
|
||||
import { url, propertyEqual } from 'discourse/lib/computed';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
|
@ -77,7 +77,6 @@ const Post = RestModel.extend({
|
|||
|
||||
topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'),
|
||||
hasHistory: Em.computed.gt('version', 1),
|
||||
postElementId: fmt('post_number', 'post_%@'),
|
||||
|
||||
canViewRawEmail: function() {
|
||||
return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff');
|
||||
|
|
|
@ -60,6 +60,16 @@
|
|||
{{/unless}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if canWhisper}}
|
||||
<div class='form-element clearfix'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.whisper tabindex="3"}}
|
||||
{{i18n "composer.add_whisper"}}
|
||||
</label>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "composer-fields"}}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{{view 'reply-history' content=replyHistory}}
|
||||
</div>
|
||||
|
||||
<article {{bind-attr class=":boxed via_email" id="postElementId" data-post-id="id" data-user-id="user_id"}}>
|
||||
<article class="boxed {{if via_email 'via-email'}}" id={{postElementId}} data-post-id={{id}} data-user-id={{user_id}}>
|
||||
<div class='row'>
|
||||
|
||||
<div class='topic-avatar'>
|
||||
|
@ -45,15 +45,20 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
{{#if wiki}}
|
||||
<div class="post-info wiki" title="{{i18n 'post.wiki.about'}}" {{action "editPost" this}}>{{fa-icon "pencil-square-o"}}</div>
|
||||
<div class="post-info wiki" title={{i18n 'post.wiki.about'}} {{action "editPost" this}}>{{fa-icon "pencil-square-o"}}</div>
|
||||
{{/if}}
|
||||
{{#if via_email}}
|
||||
{{#if canViewRawEmail}}
|
||||
<div class="post-info via-email raw-email" title="{{i18n 'post.via_email'}}" {{action "showRawEmail" this}}>{{fa-icon "envelope-o"}}</div>
|
||||
<div class="post-info via-email raw-email" title={{i18n 'post.via_email'}} {{action "showRawEmail" this}}>{{fa-icon "envelope-o"}}</div>
|
||||
{{else}}
|
||||
<div class="post-info via-email" title="{{i18n 'post.via_email'}}">{{fa-icon "envelope-o"}}</div>
|
||||
<div class="post-info via-email" title={{i18n 'post.via_email'}}>{{fa-icon "envelope-o"}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if view.whisper}}
|
||||
<div class="post-info whisper" title={{i18n 'post.whisper'}}>{{fa-icon "user-secret"}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showUserReplyTab}}
|
||||
<a href {{action "toggleReplyHistory" this target="view"}} class='reply-to-tab'>
|
||||
{{#if loadingReplyHistory}}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import ScreenTrack from 'discourse/lib/screen-track';
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { fmt } from 'discourse/lib/computed';
|
||||
|
||||
const DAY = 60 * 50 * 1000;
|
||||
|
||||
|
@ -12,10 +14,18 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
|||
'post.deleted:deleted',
|
||||
'post.topicOwner:topic-owner',
|
||||
'groupNameClass',
|
||||
'post.wiki:wiki'],
|
||||
'post.wiki:wiki',
|
||||
'whisper'],
|
||||
|
||||
post: Ember.computed.alias('content'),
|
||||
|
||||
postElementId: fmt('post.post_number', 'post_%@'),
|
||||
|
||||
@computed('post.post_type')
|
||||
whisper(postType) {
|
||||
return postType === this.site.get('post_types.whisper');
|
||||
},
|
||||
|
||||
templateName: function() {
|
||||
return (this.get('post.post_type') === this.site.get('post_types.small_action')) ? 'post-small-action' : 'post';
|
||||
}.property('post.post_type'),
|
||||
|
|
|
@ -147,7 +147,7 @@ aside.quote {
|
|||
}
|
||||
|
||||
.post-info {
|
||||
&.wiki, &.via-email {
|
||||
&.wiki, &.via-email, &.whisper {
|
||||
margin-right: 5px;
|
||||
i.fa {
|
||||
font-size: 1em;
|
||||
|
|
|
@ -582,6 +582,15 @@ a.mention {
|
|||
}
|
||||
}
|
||||
|
||||
.whisper {
|
||||
.topic-body {
|
||||
.cooked {
|
||||
font-style: italic;
|
||||
color: dark-light-diff($primary, $secondary, 55%, -40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#share-link {
|
||||
width: 365px;
|
||||
margin-left: -4px;
|
||||
|
|
|
@ -465,6 +465,10 @@ class PostsController < ApplicationController
|
|||
result[:is_warning] = false
|
||||
end
|
||||
|
||||
if SiteSetting.enable_whispers? && params[:whisper] == "true"
|
||||
result[:post_type] = Post.types[:whisper]
|
||||
end
|
||||
|
||||
PostRevisor.tracked_topic_fields.each_key do |f|
|
||||
params.permit(f => [])
|
||||
result[f] = params[f] if params.has_key?(f)
|
||||
|
|
|
@ -74,7 +74,7 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.types
|
||||
@types ||= Enum.new(:regular, :moderator_action, :small_action)
|
||||
@types ||= Enum.new(:regular, :moderator_action, :small_action, :whisper)
|
||||
end
|
||||
|
||||
def self.cook_methods
|
||||
|
@ -96,15 +96,24 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def publish_change_to_clients!(type)
|
||||
# special failsafe for posts missing topics
|
||||
# consistency checks should fix, but message
|
||||
|
||||
channel = "/topic/#{topic_id}"
|
||||
msg = { id: id,
|
||||
post_number: post_number,
|
||||
updated_at: Time.now,
|
||||
type: type }
|
||||
|
||||
# special failsafe for posts missing topics consistency checks should fix, but message
|
||||
# is safe to skip
|
||||
MessageBus.publish("/topic/#{topic_id}", {
|
||||
id: id,
|
||||
post_number: post_number,
|
||||
updated_at: Time.now,
|
||||
type: type
|
||||
}, group_ids: topic.secure_group_ids) if topic
|
||||
return unless topic
|
||||
|
||||
# Whispers should not be published to everyone
|
||||
if post_type == Post.types[:whisper]
|
||||
user_ids = User.where('admin or moderator or id = ?', user_id).pluck(:id)
|
||||
MessageBus.publish(channel, msg, user_ids: user_ids)
|
||||
else
|
||||
MessageBus.publish(channel, msg, group_ids: topic.secure_group_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def trash!(trashed_by=nil)
|
||||
|
|
|
@ -218,6 +218,13 @@ class Topic < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def visible_post_types(viewed_by=nil)
|
||||
types = Post.types
|
||||
result = [types[:regular], types[:moderator_action], types[:small_action]]
|
||||
result << types[:whisper] if viewed_by.try(:staff?)
|
||||
result
|
||||
end
|
||||
|
||||
def self.top_viewed(max = 10)
|
||||
Topic.listable_topics.visible.secured.order('views desc').limit(max)
|
||||
end
|
||||
|
|
|
@ -809,6 +809,7 @@ en:
|
|||
emoji: "Emoji :smile:"
|
||||
|
||||
add_warning: "This is an official warning."
|
||||
add_whisper: "This is a whisper only visible to moderators"
|
||||
posting_not_on_topic: "Which topic do you want to reply to?"
|
||||
saving_draft_tip: "saving..."
|
||||
saved_draft_tip: "saved"
|
||||
|
@ -1349,6 +1350,7 @@ en:
|
|||
yes_value: "Yes, abandon"
|
||||
|
||||
via_email: "this post arrived via email"
|
||||
whisper: "this post is a private whisper for moderators"
|
||||
|
||||
wiki:
|
||||
about: "this post is a wiki; basic users can edit it"
|
||||
|
|
|
@ -880,6 +880,7 @@ en:
|
|||
email_token_grace_period_hours: "Forgot password / activate account tokens are still valid for a grace period of (n) hours after being redeemed."
|
||||
|
||||
enable_badges: "Enable the badge system"
|
||||
enable_whispers: "Allow users to whisper to moderators"
|
||||
|
||||
allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines."
|
||||
email_domains_blacklist: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net"
|
||||
|
|
|
@ -182,6 +182,9 @@ basic:
|
|||
enable_badges:
|
||||
client: true
|
||||
default: true
|
||||
enable_whispers:
|
||||
client: true
|
||||
default: false
|
||||
|
||||
login:
|
||||
invite_only:
|
||||
|
|
|
@ -144,10 +144,13 @@ module PostGuardian
|
|||
end
|
||||
|
||||
def can_see_post?(post)
|
||||
post.present? &&
|
||||
(is_admin? ||
|
||||
((is_moderator? || !post.deleted_at.present?) &&
|
||||
can_see_topic?(post.topic)))
|
||||
return false if post.blank?
|
||||
return true if is_admin?
|
||||
return false unless can_see_topic?(post.topic)
|
||||
return false unless post.user == @user || post.topic.visible_post_types(@user).include?(post.post_type)
|
||||
return false if !is_moderator? && post.deleted_at.present?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def can_view_edit_history?(post)
|
||||
|
|
|
@ -191,11 +191,9 @@ class TopicView
|
|||
|
||||
# Find the sort order for a post in the topic
|
||||
def sort_order_for_post_number(post_number)
|
||||
Post.where(topic_id: @topic.id, post_number: post_number)
|
||||
.with_deleted
|
||||
.select(:sort_order)
|
||||
.first
|
||||
.try(:sort_order)
|
||||
posts = Post.where(topic_id: @topic.id, post_number: post_number).with_deleted
|
||||
posts = filter_post_types(posts)
|
||||
posts.select(:sort_order).first.try(:sort_order)
|
||||
end
|
||||
|
||||
# Filter to all posts near a particular post number
|
||||
|
@ -332,11 +330,22 @@ class TopicView
|
|||
|
||||
private
|
||||
|
||||
def filter_post_types(posts)
|
||||
visible_types = @topic.visible_post_types(@user)
|
||||
|
||||
if @user.present?
|
||||
posts.where("user_id = ? OR post_type IN (?)", @user.id, visible_types)
|
||||
else
|
||||
posts.where(post_type: visible_types)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_posts_by_ids(post_ids)
|
||||
# TODO: Sort might be off
|
||||
@posts = Post.where(id: post_ids, topic_id: @topic.id)
|
||||
.includes(:user, :reply_to_user)
|
||||
.order('sort_order')
|
||||
@posts = filter_post_types(@posts)
|
||||
@posts = @posts.with_deleted if @guardian.can_see_deleted_posts?
|
||||
@posts
|
||||
end
|
||||
|
@ -361,7 +370,7 @@ class TopicView
|
|||
end
|
||||
|
||||
def unfiltered_posts
|
||||
result = @topic.posts
|
||||
result = filter_post_types(@topic.posts)
|
||||
result = result.with_deleted if @guardian.can_see_deleted_posts?
|
||||
result = @topic.posts.where("user_id IS NOT NULL") if @exclude_deleted_users
|
||||
result
|
||||
|
|
|
@ -437,6 +437,32 @@ describe Guardian do
|
|||
expect(Guardian.new(user).can_see?(post)).to be_falsey
|
||||
expect(Guardian.new(admin).can_see?(post)).to be_truthy
|
||||
end
|
||||
|
||||
it 'respects whispers' do
|
||||
regular_post = Fabricate.build(:post)
|
||||
whisper_post = Fabricate.build(:post, post_type: Post.types[:whisper])
|
||||
|
||||
anon_guardian = Guardian.new
|
||||
expect(anon_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(anon_guardian.can_see?(whisper_post)).to eq(false)
|
||||
|
||||
regular_user = Fabricate.build(:user)
|
||||
regular_guardian = Guardian.new(regular_user)
|
||||
expect(regular_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(regular_guardian.can_see?(whisper_post)).to eq(false)
|
||||
|
||||
# can see your own whispers
|
||||
regular_whisper = Fabricate.build(:post, post_type: Post.types[:whisper], user: regular_user)
|
||||
expect(regular_guardian.can_see?(regular_whisper)).to eq(true)
|
||||
|
||||
mod_guardian = Guardian.new(Fabricate.build(:moderator))
|
||||
expect(mod_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(mod_guardian.can_see?(whisper_post)).to eq(true)
|
||||
|
||||
admin_guardian = Guardian.new(Fabricate.build(:admin))
|
||||
expect(admin_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(admin_guardian.can_see?(whisper_post)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a PostRevision' do
|
||||
|
|
|
@ -251,6 +251,23 @@ describe TopicView do
|
|||
|
||||
end
|
||||
|
||||
context 'whispers' do
|
||||
it "handles their visibility properly" do
|
||||
p1 = Fabricate(:post, topic: topic, user: coding_horror)
|
||||
p2 = Fabricate(:post, topic: topic, user: coding_horror, post_type: Post.types[:whisper])
|
||||
p3 = Fabricate(:post, topic: topic, user: coding_horror)
|
||||
|
||||
ch_posts = TopicView.new(topic.id, coding_horror).posts
|
||||
expect(ch_posts.map(&:id)).to eq([p1.id, p2.id, p3.id])
|
||||
|
||||
anon_posts = TopicView.new(topic.id).posts
|
||||
expect(anon_posts.map(&:id)).to eq([p1.id, p3.id])
|
||||
|
||||
admin_posts = TopicView.new(topic.id, Fabricate(:moderator)).posts
|
||||
expect(admin_posts.map(&:id)).to eq([p1.id, p2.id, p3.id])
|
||||
end
|
||||
end
|
||||
|
||||
context '.posts' do
|
||||
|
||||
# Create the posts in a different order than the sort_order
|
||||
|
|
|
@ -11,6 +11,40 @@ describe Topic do
|
|||
|
||||
it { is_expected.to rate_limit }
|
||||
|
||||
context '#visible_post_types' do
|
||||
let(:types) { Post.types }
|
||||
|
||||
it "returns the appropriate types for anonymous users" do
|
||||
topic = Fabricate.build(:topic)
|
||||
post_types = topic.visible_post_types
|
||||
|
||||
expect(post_types).to include(types[:regular])
|
||||
expect(post_types).to include(types[:moderator_action])
|
||||
expect(post_types).to include(types[:small_action])
|
||||
expect(post_types).to_not include(types[:whisper])
|
||||
end
|
||||
|
||||
it "returns the appropriate types for regular users" do
|
||||
topic = Fabricate.build(:topic)
|
||||
post_types = topic.visible_post_types(Fabricate.build(:user))
|
||||
|
||||
expect(post_types).to include(types[:regular])
|
||||
expect(post_types).to include(types[:moderator_action])
|
||||
expect(post_types).to include(types[:small_action])
|
||||
expect(post_types).to_not include(types[:whisper])
|
||||
end
|
||||
|
||||
it "returns the appropriate types for staff users" do
|
||||
topic = Fabricate.build(:topic)
|
||||
post_types = topic.visible_post_types(Fabricate.build(:moderator))
|
||||
|
||||
expect(post_types).to include(types[:regular])
|
||||
expect(post_types).to include(types[:moderator_action])
|
||||
expect(post_types).to include(types[:small_action])
|
||||
expect(post_types).to include(types[:whisper])
|
||||
end
|
||||
end
|
||||
|
||||
context 'slug' do
|
||||
let(:title) { "hello world topic" }
|
||||
let(:slug) { "hello-world-topic" }
|
||||
|
|
Loading…
Reference in New Issue
Block a user