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 000000000..10c572a23
--- /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 7b7b9a6c4..3cd887ec4 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 066b000a2..f4790b5c0 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 000000000..73000fb97
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/small-action.hbs
@@ -0,0 +1,10 @@
+{{fa-icon icon}}
+
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 000000000..ceb3dfe4a
--- /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 18014662b..bcdc0f7f8 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 af93ef94a..a5c24925d 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 99794d40b..09fa1cc4d 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 30f2da1b4..ec6146162 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 7a0a98dca..035fc3c0e 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 20c0d6200..e15e025de 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 0d8e1e664..94c5b1a15 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 4f23d7c3d..2f8c9f990 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 38a31992d..2dd29df5c 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 daf5d6faf..4111fbbde 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 65b63de99..f6dcf2b9f 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 59250eae3..64853e915 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 e1b971722..21f622367 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 000000000..df27d1ec8
--- /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 9feb47958..d9164d894 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 8629d1e87..1f1753861 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 3d17c8c66..d4e1354aa 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 e8082ceff..54483efd5 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 8a3d30225..b7326e946 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 39dad5bb3..787c56aa9 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