UX: Use smaller messages for moderator actions.

This commit is contained in:
Robin Ward
2015-07-24 16:39:03 -04:00
parent 326b2812e4
commit bb93a345eb
25 changed files with 171 additions and 60 deletions

View File

@@ -0,0 +1,36 @@
const icons = {
'closed.enabled': 'lock',
'closed.disabled': 'unlock-alt',
'archived.enabled': 'folder',
'archived.disabled': 'folder-open',
'pinned.enabled': 'thumb-tack',
'pinned.disabled': 'thumb-tack',
'visible.enabled': 'eye',
'visible.disabled': 'eye-slash'
};
export default Ember.Component.extend({
layoutName: 'components/small-action', // needed because `time-gap` inherits from this
classNames: ['small-action'],
description: function() {
const actionCode = this.get('actionCode');
if (actionCode) {
const dt = new Date(this.get('post.created_at'));
const when = Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'});
const result = I18n.t(`action_codes.${actionCode}`, {when});
return result + (this.get('post.cooked') || '');
}
}.property('actionCode', 'post.created_at', 'post.cooked'),
icon: function() {
return icons[this.get('actionCode')] || 'exclamation';
}.property('actionCode'),
actions: {
edit: function() {
this.sendAction('editPost', this.get('post'));
}
}
});

View File

@@ -1,22 +1,19 @@
export default Ember.Component.extend({
classNameBindings: [':time-gap'],
import SmallActionComponent from 'discourse/components/small-action';
render(buffer) {
const gapDays = this.get('gapDays');
export default SmallActionComponent.extend({
classNames: ['time-gap'],
icon: 'clock-o',
buffer.push("<div class='topic-avatar'><i class='fa fa-clock-o'></i></div>");
let timeGapWords;
description: function() {
const gapDays = this.get('daysAgo');
if (gapDays < 30) {
timeGapWords = I18n.t('dates.later.x_days', {count: gapDays});
return I18n.t('dates.later.x_days', {count: gapDays});
} else if (gapDays < 365) {
const gapMonths = Math.floor(gapDays / 30);
timeGapWords = I18n.t('dates.later.x_months', {count: gapMonths});
return I18n.t('dates.later.x_months', {count: gapMonths});
} else {
const gapYears = Math.floor(gapDays / 365);
timeGapWords = I18n.t('dates.later.x_years', {count: gapYears});
return I18n.t('dates.later.x_years', {count: gapYears});
}
buffer.push("<div class='time-gap-words'>" + timeGapWords + "</div>");
}
}.property(),
});

View File

@@ -592,6 +592,7 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, {
const self = this;
this.messageBus.subscribe("/topic/" + this.get('model.id'), function(data) {
console.log(data);
const topic = self.get('model');
if (data.notification_level_change) {

View File

@@ -0,0 +1,10 @@
<div class='topic-avatar'>{{fa-icon icon}}</div>
<div class='small-action-desc'>
{{#if post}}
<button {{action "edit"}} title="{{i18n "post.controls.edit"}}">{{fa-icon "pencil"}}</button>
<a href={{post.usernameUrl}} data-user-card={{post.username}}>
{{avatar post imageSize="small"}}
</a>
{{/if}}
<p>{{{description}}}</p>
</div>

View File

@@ -0,0 +1,3 @@
{{post-gap post=this postStream=controller.model.postStream before="true"}}
{{small-action actionCode=action_code post=this daysAgo=view.daysAgo editPost="editPost"}}
{{post-gap post=this postStream=controller.model.postStream before="false"}}

View File

@@ -1,7 +1,7 @@
{{post-gap post=this postStream=controller.model.postStream before="true"}}
{{#if hasTimeGap}}
{{time-gap gapDays=daysSincePrevious}}
{{time-gap daysAgo=daysSincePrevious}}
{{/if}}
<div class='row'>

View File

@@ -2,7 +2,6 @@ const DAY = 60 * 50 * 1000;
const PostView = Discourse.GroupedView.extend(Ember.Evented, {
classNames: ['topic-post', 'clearfix'],
templateName: 'post',
classNameBindings: ['needsModeratorClass:moderator:regular',
'selected',
'post.hidden:post-hidden',
@@ -13,6 +12,10 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
post: Ember.computed.alias('content'),
templateName: function() {
return (this.get('post.post_type') === 3) ? 'post-small-action' : 'post';
}.property('post.post_type'),
historyHeat: function() {
const updatedAt = this.get('post.updated_at');
if (!updatedAt) { return; }

View File

@@ -726,35 +726,56 @@ $topic-avatar-width: 45px;
width: calc(#{$topic-avatar-width} + #{$topic-body-width} + 2 * #{$topic-body-width-padding});
}
.time-gap {
.small-action {
width: 755px;
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
.topic-avatar {
padding: 5px 0;
border-top: none;
float: left;
i {
font-size: 35px;
width: 45px;
text-align: center;
color: lighten($primary, 75%);
}
}
}
.time-gap .topic-avatar i {
font-size: 35px;
width: 45px;
text-align: center;
color: lighten($primary, 75%);
}
.time-gap-words {
display: inline-block;
padding: 0.5em 1em;
margin-top: 9px;
text-transform: uppercase;
font-weight: bold;
font-size: 0.9em;
color: lighten($primary, 60%);
.small-action-desc {
display: inline-block;
padding: 0.5em 1em;
margin-top: 5px;
text-transform: uppercase;
font-weight: bold;
font-size: 0.9em;
color: lighten($primary, 60%);
width: 680px;
.avatar {
margin-right: 0.8em;
float: left;
}
p {
margin: 0;
padding-top: 4px;
}
}
button {
background: transparent;
border: 0;
float: right;
}
clear: both;
}
.posts-wrapper {
position: relative;
-webkit-font-smoothing: subpixel-antialiased;
}
}
.dropdown {
position: relative;

View File

@@ -59,7 +59,7 @@ class DirectoryItem < ActiveRecord::Base
AND COALESCE(t.visible, true)
AND p.deleted_at IS NULL
AND (NOT (COALESCE(p.hidden, false)))
AND COALESCE(p.post_type, :regular_post_type) != :moderator_action
AND COALESCE(p.post_type, :regular_post_type) = :regular_post_type
AND u.id > 0
GROUP BY u.id",
period_type: period_types[period_type],
@@ -68,8 +68,7 @@ class DirectoryItem < ActiveRecord::Base
was_liked_type: UserAction::WAS_LIKED,
new_topic_type: UserAction::NEW_TOPIC,
reply_type: UserAction::REPLY,
regular_post_type: Post.types[:regular],
moderator_action: Post.types[:moderator_action]
regular_post_type: Post.types[:regular]
end
end
end

View File

@@ -73,7 +73,7 @@ class Post < ActiveRecord::Base
end
def self.types
@types ||= Enum.new(:regular, :moderator_action)
@types ||= Enum.new(:regular, :moderator_action, :small_action)
end
def self.cook_methods
@@ -99,10 +99,10 @@ class Post < ActiveRecord::Base
# 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
id: id,
post_number: post_number,
updated_at: Time.now,
type: type
}, group_ids: topic.secure_group_ids) if topic
end

View File

@@ -205,7 +205,7 @@ SQL
end
def staff_already_replied?(topic)
topic.posts.where("user_id IN (SELECT id FROM users WHERE moderator OR admin) OR post_type = :post_type", post_type: Post.types[:moderator_action]).exists?
topic.posts.where("user_id IN (SELECT id FROM users WHERE moderator OR admin) OR (post_type != :regular_post_type)", regular_post_type: Post.types[:regular]).exists?
end
def self.create_message_for_post_action(user, post, post_action_type_id, opts)

View File

@@ -489,15 +489,18 @@ class Topic < ActiveRecord::Base
true
end
def add_moderator_post(user, text, opts={})
def add_moderator_post(user, text, opts=nil)
opts ||= {}
new_post = nil
Topic.transaction do
creator = PostCreator.new(user,
raw: text,
post_type: Post.types[:moderator_action],
post_type: opts[:post_type] || Post.types[:moderator_action],
action_code: opts[:action_code],
no_bump: opts[:bump].blank?,
skip_notifications: opts[:skip_notifications],
topic_id: self.id)
topic_id: self.id,
skip_validations: true)
new_post = creator.create
increment!(:moderator_posts_count)
end

View File

@@ -49,8 +49,6 @@ TopicStatusUpdate = Struct.new(:topic, :user) do
locale_key = status.locale_key
locale_key << "_lastpost" if topic.auto_close_based_on_last_post
message_for_autoclosed(locale_key)
else
I18n.t(status.locale_key)
end
end
@@ -69,7 +67,9 @@ TopicStatusUpdate = Struct.new(:topic, :user) do
end
def options_for(status)
{ bump: status.reopening_topic? }
{ bump: status.reopening_topic?,
post_type: Post.types[:small_action],
action_code: status.action_code }
end
Status = Struct.new(:name, :enabled) do
@@ -85,8 +85,12 @@ TopicStatusUpdate = Struct.new(:topic, :user) do
!enabled?
end
def action_code
"#{name}.#{enabled? ? 'enabled' : 'disabled'}"
end
def locale_key
"topic_statuses.#{name}_#{enabled? ? 'enabled' : 'disabled'}"
"topic_statuses.#{action_code.gsub('.', '_')}"
end
def reopening_topic?

View File

@@ -46,7 +46,7 @@ class AdminPostSerializer < ApplicationSerializer
end
def moderator_action
object.post_type == Post.types[:moderator_action]
object.post_type == Post.types[:moderator_action] || object.post_type == Post.types[:small_action]
end
def deleted_by

View File

@@ -57,7 +57,8 @@ class PostSerializer < BasicPostSerializer
:wiki,
:user_custom_fields,
:static_doc,
:via_email
:via_email,
:action_code
def initialize(object, opts)
super(object, opts)
@@ -281,6 +282,10 @@ class PostSerializer < BasicPostSerializer
scope.is_staff? ? object.version : object.public_version
end
def include_action_code?
object.action_code.present?
end
private
def post_actions

View File

@@ -68,7 +68,7 @@ class UserActionSerializer < ApplicationSerializer
end
def moderator_action
object.post_type == Post.types[:moderator_action]
object.post_type == Post.types[:moderator_action] || object.post_type == Post.types[:small_action]
end
def include_reply_to_post_number?

View File

@@ -24,7 +24,7 @@ class PostAlerter
end
create_notification(user, Notification.types[:private_message], post)
end
elsif post.post_type != Post.types[:moderator_action]
elsif post.post_type == Post.types[:regular]
# If it's not a private message and it's not an automatic post caused by a moderator action, notify the users
notify_post_users(post)
end