From bb93a345eb3e33c51852ea92510293a19598fa61 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 24 Jul 2015 16:39:03 -0400 Subject: [PATCH] UX: Use smaller messages for moderator actions. --- .../discourse/components/small-action.js.es6 | 36 ++++++++++++ .../discourse/components/time-gap.js.es6 | 23 ++++---- .../discourse/controllers/topic.js.es6 | 1 + .../templates/components/small-action.hbs | 10 ++++ .../discourse/templates/post-small-action.hbs | 3 + .../javascripts/discourse/templates/post.hbs | 2 +- .../javascripts/discourse/views/post.js.es6 | 5 +- .../stylesheets/desktop/topic-post.scss | 55 +++++++++++++------ app/models/directory_item.rb | 5 +- app/models/post.rb | 10 ++-- app/models/post_action.rb | 2 +- app/models/topic.rb | 9 ++- app/models/topic_status_update.rb | 12 ++-- app/serializers/admin_post_serializer.rb | 2 +- app/serializers/post_serializer.rb | 7 ++- app/serializers/user_action_serializer.rb | 2 +- app/services/post_alerter.rb | 2 +- config/locales/client.en.yml | 14 +++++ .../20150724182342_add_action_code_to_post.rb | 5 ++ .../tilt/es6_module_transpiler_template.rb | 2 +- lib/post_creator.rb | 5 +- lib/post_jobs_enqueuer.rb | 5 +- lib/topic_view.rb | 2 +- script/import_scripts/base.rb | 2 +- spec/models/topic_status_update_spec.rb | 10 +++- 25 files changed, 171 insertions(+), 60 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/small-action.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/small-action.hbs create mode 100644 app/assets/javascripts/discourse/templates/post-small-action.hbs create mode 100644 db/migrate/20150724182342_add_action_code_to_post.rb diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6 new file mode 100644 index 00000000000..10c572a23b7 --- /dev/null +++ b/app/assets/javascripts/discourse/components/small-action.js.es6 @@ -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')); + } + } + +}); diff --git a/app/assets/javascripts/discourse/components/time-gap.js.es6 b/app/assets/javascripts/discourse/components/time-gap.js.es6 index 7b7b9a6c496..3cd887ec43a 100644 --- a/app/assets/javascripts/discourse/components/time-gap.js.es6 +++ b/app/assets/javascripts/discourse/components/time-gap.js.es6 @@ -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("
"); - - 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("
" + timeGapWords + "
"); - } + }.property(), }); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 066b000a269..f4790b5c0fd 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -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) { diff --git a/app/assets/javascripts/discourse/templates/components/small-action.hbs b/app/assets/javascripts/discourse/templates/components/small-action.hbs new file mode 100644 index 00000000000..73000fb97bf --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/small-action.hbs @@ -0,0 +1,10 @@ +
{{fa-icon icon}}
+
+ {{#if post}} + + + {{avatar post imageSize="small"}} + + {{/if}} +

{{{description}}}

+
diff --git a/app/assets/javascripts/discourse/templates/post-small-action.hbs b/app/assets/javascripts/discourse/templates/post-small-action.hbs new file mode 100644 index 00000000000..ceb3dfe4a76 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/post-small-action.hbs @@ -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"}} diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index 18014662b40..bcdc0f7f825 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -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}}
diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index af93ef94ad6..a5c24925d24 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -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; } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 99794d40b30..09fa1cc4d05 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -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; diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index 30f2da1b4b6..ec6146162b0 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -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 diff --git a/app/models/post.rb b/app/models/post.rb index 7a0a98dca6f..035fc3c0ec1 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -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 diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 20c0d62007c..e15e025de1e 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -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) diff --git a/app/models/topic.rb b/app/models/topic.rb index 0d8e1e664a2..94c5b1a159b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -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 diff --git a/app/models/topic_status_update.rb b/app/models/topic_status_update.rb index 4f23d7c3de6..2f8c9f990e6 100644 --- a/app/models/topic_status_update.rb +++ b/app/models/topic_status_update.rb @@ -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? diff --git a/app/serializers/admin_post_serializer.rb b/app/serializers/admin_post_serializer.rb index 38a31992dcc..2dd29df5c03 100644 --- a/app/serializers/admin_post_serializer.rb +++ b/app/serializers/admin_post_serializer.rb @@ -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 diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index daf5d6fafe5..4111fbbde2d 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -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 diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 65b63de99de..f6dcf2b9fb3 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -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? diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 59250eae3f7..64853e91551 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e1b9717222c..21f6223678a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -119,6 +119,20 @@ en: google+: 'share this link on Google+' email: 'send this link in an email' + action_codes: + closed: + enabled: 'closed this topic %{when}' + disabled: 'opened this topic %{when}' + archived: + enabled: 'archived this topic %{when}' + disabled: 'unarchived this topic %{when}' + pinned: + enabled: 'pinned this topic %{when}' + disabled: 'unpinned this topic %{when}' + visible: + enabled: 'unlisted this topic %{when}' + disabled: 'listed this topic %{when}' + topic_admin_menu: "topic admin actions" emails_are_disabled: "All outgoing email has been globally disabled by an administrator. No email notifications of any kind will be sent." diff --git a/db/migrate/20150724182342_add_action_code_to_post.rb b/db/migrate/20150724182342_add_action_code_to_post.rb new file mode 100644 index 00000000000..df27d1ec826 --- /dev/null +++ b/db/migrate/20150724182342_add_action_code_to_post.rb @@ -0,0 +1,5 @@ +class AddActionCodeToPost < ActiveRecord::Migration + def change + add_column :posts, :action_code, :string, null: true + end +end diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index 9feb4795838..d9164d894fb 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -139,7 +139,7 @@ module Tilt def generate_source(scope) js_source = ::JSON.generate(data, quirks_mode: true) - js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring']})['code']" + js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring', 'es6.templateLiterals']})['code']" "new module.exports.Compiler(#{js_source}, '#{module_name(scope.root_path, scope.logical_path)}', #{compiler_options}).#{compiler_method}()" end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 8629d1e873d..1f175386184 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -31,6 +31,7 @@ class PostCreator # :raw_email - Imported from an email # via_email - Mark this post as arriving via email # raw_email - Full text of arriving email (to store) + # action_code - Describes a small_action post (optional) # # When replying to a topic: # topic_id - topic we're replying to @@ -255,7 +256,7 @@ class PostCreator end def setup_post - @opts[:raw] = TextCleaner.normalize_whitespaces(@opts[:raw]).gsub(/\s+\z/, "") + @opts[:raw] = TextCleaner.normalize_whitespaces(@opts[:raw] || '').gsub(/\s+\z/, "") post = Post.new(raw: @opts[:raw], topic_id: @topic.try(:id), @@ -263,7 +264,7 @@ class PostCreator reply_to_post_number: @opts[:reply_to_post_number]) # Attributes we pass through to the post instance if present - [:post_type, :no_bump, :cooking_options, :image_sizes, :acting_user, :invalidate_oneboxes, :cook_method, :via_email, :raw_email].each do |a| + [:post_type, :no_bump, :cooking_options, :image_sizes, :acting_user, :invalidate_oneboxes, :cook_method, :via_email, :raw_email, :action_code].each do |a| post.send("#{a}=", @opts[a]) if @opts[a].present? end diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb index 3d17c8c66c1..d4e1354aaae 100644 --- a/lib/post_jobs_enqueuer.rb +++ b/lib/post_jobs_enqueuer.rb @@ -56,6 +56,9 @@ class PostJobsEnqueuer end def skip_after_create? - @opts[:import_mode] || @topic.private_message? || @post.post_type == Post.types[:moderator_action] + @opts[:import_mode] || + @topic.private_message? || + @post.post_type == Post.types[:moderator_action] || + @post.post_type == Post.types[:small_action] end end diff --git a/lib/topic_view.rb b/lib/topic_view.rb index e8082ceff9e..54483efd5df 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -379,7 +379,7 @@ class TopicView end if @best.present? - @filtered_posts = @filtered_posts.where('posts.post_type <> ?', Post.types[:moderator_action]) + @filtered_posts = @filtered_posts.where('posts.post_type = ?', Post.types[:regular]) @contains_gaps = true end diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 8a3d30225eb..b7326e946c5 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -504,7 +504,7 @@ class ImportScripts::Base def update_bumped_at puts "", "updating bumped_at on topics" - Post.exec_sql("update topics t set bumped_at = COALESCE((select max(created_at) from posts where topic_id = t.id and post_type != #{Post.types[:moderator_action]}), bumped_at)") + Post.exec_sql("update topics t set bumped_at = COALESCE((select max(created_at) from posts where topic_id = t.id and post_type = #{Post.types[:regular]}), bumped_at)") end def update_last_posted_at diff --git a/spec/models/topic_status_update_spec.rb b/spec/models/topic_status_update_spec.rb index 39dad5bb34f..787c56aa906 100644 --- a/spec/models/topic_status_update_spec.rb +++ b/spec/models/topic_status_update_spec.rb @@ -30,7 +30,10 @@ describe TopicStatusUpdate do TopicStatusUpdate.new(topic, admin).update!("autoclosed", true) - expect(topic.posts.last.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_minutes", count: 0)) + last_post = topic.posts.last + expect(last_post.post_type).to eq(Post.types[:small_action]) + expect(last_post.action_code).to eq('autoclosed.enabled') + expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_minutes", count: 0)) end it "adds an autoclosed message based on last post" do @@ -39,7 +42,10 @@ describe TopicStatusUpdate do TopicStatusUpdate.new(topic, admin).update!("autoclosed", true) - expect(topic.posts.last.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_lastpost_minutes", count: 0)) + last_post = topic.posts.last + expect(last_post.post_type).to eq(Post.types[:small_action]) + expect(last_post.action_code).to eq('autoclosed.enabled') + expect(last_post.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_lastpost_minutes", count: 0)) end end