From 08e69b988c4f0a64f1e55fa81ac003f7b0d1f56a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 29 Jul 2015 01:02:40 +0800 Subject: [PATCH 01/83] FIX: Draft overlaps topic counts blurb in suggested topics. --- app/assets/javascripts/discourse/views/composer.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 354ed503dc..240028f84d 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -543,8 +543,11 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { this.$('.wmd-preview').off('click.preview'); + const self = this; + Em.run.next(() => { - $('#main-outlet').css('padding-bottom', 0); + const sizePx = self.get('composeState') === Discourse.Composer.CLOSED ? 0 : $('#reply-control').height(); + $('#main-outlet').css('padding-bottom', sizePx); // need to wait a bit for the "slide down" transition of the composer Em.run.later(() => { this.appEvents.trigger("composer:closed"); From ef0b75386f918afe97edbcb415a03cd5e406097c Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 30 Jul 2015 16:31:49 -0400 Subject: [PATCH 02/83] FIX: badge titles should always render under the badge image on user's badges page --- .../discourse/templates/components/badge-button.hbs | 2 +- app/assets/stylesheets/common/base/user-badges.scss | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/badge-button.hbs b/app/assets/javascripts/discourse/templates/components/badge-button.hbs index 7c0469791e..4886147389 100644 --- a/app/assets/javascripts/discourse/templates/components/badge-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/badge-button.hbs @@ -1,3 +1,3 @@ {{icon-or-image badge.icon}} -{{badge.displayName}} +{{badge.displayName}} {{yield}} diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 24691c940e..74b87c8265 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -51,7 +51,9 @@ } img { - display: inline-block; + display: block; + margin: auto; + margin-bottom: 4px; width: 55px; height: 55px; } From 8c62c8d7bf233c838287488189d9b8235b3d5ff8 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 30 Jul 2015 11:30:30 -0700 Subject: [PATCH 03/83] FEATURE: Add off button on preferences for popup notifications --- .../desktop-notification-config.js.es6 | 65 +++++++++++++++++++ .../lib/desktop-notifications.js.es6 | 1 + .../desktop-notification-config.hbs | 20 ++++++ .../discourse/templates/user/preferences.hbs | 6 ++ config/locales/client.en.yml | 11 ++++ 5 files changed, 103 insertions(+) create mode 100644 app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs diff --git a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 new file mode 100644 index 0000000000..5a362280c3 --- /dev/null +++ b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 @@ -0,0 +1,65 @@ +export default Ember.Component.extend({ + classNames: ['controls'], + + notificationsPermission: function() { + if (this.get('isNotSupported')) return ''; + + return Notification.permission; + }.property(), + + notificationsDisabled: function(_, value) { + if (arguments.length > 1) { + localStorage.setItem('notifications-disabled', value); + } + return localStorage.getItem('notifications-disabled'); + }.property(), + + + isNotSupported: function() { + return !window['Notification']; + }.property(), + + isDefaultPermission: function() { + if (this.get('isNotSupported')) return false; + + return Notification.permission === "default"; + }.property('isNotSupported', 'notificationsPermission'), + + isDeniedPermission: function() { + if (this.get('isNotSupported')) return false; + + return Notification.permission === "denied"; + }.property('isNotSupported', 'notificationsPermission'), + + isGrantedPermission: function() { + if (this.get('isNotSupported')) return false; + + return Notification.permission === "granted"; + }.property('isNotSupported', 'notificationsPermission'), + + isEnabled: function() { + if (!this.get('isGrantedPermission')) return false; + + return !this.get('notificationsDisabled'); + }.property('isGrantedPermission', 'notificationsDisabled'), + + actions: { + requestPermission() { + const self = this; + Notification.requestPermission(function() { + self.propertyDidChange('notificationsPermission'); + }); + }, + recheckPermission() { + this.propertyDidChange('notificationsPermission'); + }, + turnoff() { + this.set('notificationsDisabled', 'disabled'); + this.propertyDidChange('notificationsPermission'); + }, + turnon() { + this.set('notificationsDisabled', ''); + this.propertyDidChange('notificationsPermission'); + } + } +}); diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 index 5d7b18db0f..ce67ff9dfd 100644 --- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 +++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 @@ -94,6 +94,7 @@ function onNotification(data) { if (!liveEnabled) { return; } if (!primaryTab) { return; } if (!isIdle()) { return; } + if (localStorage.getItem('notifications-disabled')) { return; } const notificationTitle = I18n.t(i18nKey(data.notification_type), { site_title: Discourse.SiteSettings.title, diff --git a/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs b/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs new file mode 100644 index 0000000000..3bc0db1c51 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs @@ -0,0 +1,20 @@ + +{{#if isNotSupported}} + {{d-button icon="bell-slash" label="user.desktop_notifications.not_supported" disabled="true"}} +{{/if}} +{{#if isDefaultPermission}} + {{d-button icon="bell-slash" label="user.desktop_notifications.perm_default" action="requestPermission"}} +{{/if}} +{{#if isDeniedPermission}} + {{d-button icon="bell-slash" label="user.desktop_notifications.perm_denied_btn" action="recheckPermission"}} + {{i18n "user.desktop_notifications.perm_denied_expl"}} +{{/if}} +{{#if isGrantedPermission}} + {{#if isEnabled}} + {{d-button icon="bell-slash-o" label="user.desktop_notifications.disable" action="turnoff"}} + {{i18n "user.desktop_notifications.currently_enabled"}} + {{else}} + {{d-button icon="bell-o" label="user.desktop_notifications.enable" action="turnon"}} + {{i18n "user.desktop_notifications.currently_disabled"}} + {{/if}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 775f93bd9d..278cd554bd 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -189,6 +189,12 @@ +
+ + {{desktop-notification-config}} +
{{i18n 'user.desktop_notifications.each_browser_note'}}
+
+
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e1c4b3c9eb..f6034fac9a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -413,6 +413,17 @@ en: invited_by: "Invited By" trust_level: "Trust Level" notifications: "Notifications" + desktop_notifications: + label: "Desktop Notifications" + not_supported: "Notifications are not supported on this browser. Sorry." + perm_default: "Turn On Notifications" + perm_denied_btn: "Permission Denied" + perm_denied_expl: "You have denied permission for notifications. Use your browser to enable notifications, then click the button when done. (Desktop: The leftmost icon in the address bar. Mobile: 'Site Info'.)" + disable: "Disable Notifications" + currently_enabled: "(currently enabled)" + enable: "Enable Notifications" + currently_disabled: "(currently disabled)" + each_browser_note: "Note: You have to change this setting on every browser you use." dismiss_notifications: "Mark all as Read" dismiss_notifications_tooltip: "Mark all unread notifications as read" disable_jump_reply: "Don't jump to my post after I reply" From 6f9dc135bada244187f584c418d471cd3c0276e6 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Jul 2015 15:10:18 +1000 Subject: [PATCH 04/83] FEATURE: allow logging of raw body of all unprocessable email set log_mail_processing_failures to true to enable --- app/jobs/scheduled/poll_mailbox.rb | 3 +++ config/locales/server.en.yml | 1 + config/site_settings.yml | 1 + 3 files changed, 5 insertions(+) diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index 608559dff6..68a1f98632 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -31,6 +31,9 @@ module Jobs end def handle_failure(mail_string, e) + + Rails.logger.warn("Email can not be processed: #{e}\n\n#{mail_string}") if SiteSetting.log_mail_processing_failures + template_args = {} case e when Email::Receiver::UserNotSufficientTrustLevelError diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 9a76fd0bb9..57c817df29 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1093,6 +1093,7 @@ en: pop3_polling_host: "The host to poll for email via POP3." pop3_polling_username: "The username for the POP3 account to poll for email." pop3_polling_password: "The password for the POP3 account to poll for email." + log_mail_processing_failures: "Log all email processing failures to http://yoursitename.com/logs" email_in: "Allow users to post new topics via email (requires pop3 polling). Configure the addresses in the \"Settings\" tab of each category." email_in_min_trust: "The minimum trust level a user needs to have to be allowed to post new topics via email." email_prefix: "The [label] used in the subject of emails. It will default to 'title' if not set." diff --git a/config/site_settings.yml b/config/site_settings.yml index 0329f89bd1..204b00bddc 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -491,6 +491,7 @@ email: pop3_polling_port: 995 pop3_polling_username: '' pop3_polling_password: '' + log_mail_processing_failures: false email_in: default: false client: true From 568adc49c09c4964ef3168dfd1fa12c2fb05203b Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Jul 2015 17:53:20 +1000 Subject: [PATCH 05/83] FIX: fenced code blocks not hoisted correctly also fixes unhoisting logic --- .../javascripts/discourse/dialects/dialect.js | 17 +++++++++++++---- spec/components/pretty_text_spec.rb | 6 ++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index 62febca1a7..b54011180a 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -189,14 +189,15 @@ function hoistCodeBlocksAndSpans(text) { // /!\ the order is important /!\ //
...
code blocks - text = text.replace(/(^\n*|\n\n)
([\s\S]*?)<\/pre>/ig, function(_, before, content) {
+  text = text.replace(/(\s|^)
([\s\S]*?)<\/pre>/ig, function(_, before, content) {
     var hash = md5(content);
     hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
     return before + "
" + hash + "
"; }); + // fenced code blocks (AKA GitHub code blocks) - text = text.replace(/(^\n*|\n\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) { + text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) { var hash = md5(content); hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content))); return before + "```" + language + "\n" + hash + "\n```"; @@ -277,11 +278,19 @@ Discourse.Dialect = { // If we hoisted out anything, put it back var keys = Object.keys(hoisted); if (keys.length) { - keys.forEach(function(key) { + var found = true; + + var unhoist = function(key) { result = result.replace(new RegExp(key, "g"), function() { + found = true; return hoisted[key]; }); - }); + }; + + while(found) { + found = false; + keys.forEach(unhoist); + } } return result.trim(); diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 8cffddd192..f9c7333c61 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -322,6 +322,12 @@ describe PrettyText do expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("

cpp
") end + it 'indents code correctly' do + code = "X\n```\n\n #\n x\n```" + cooked = PrettyText.cook(code) + expect(cooked).to match_html("

X

\n\n

    #\n    x
") + end + it 'can substitute s3 cdn correctly' do SiteSetting.enable_s3_uploads = true SiteSetting.s3_access_key_id = "XXX" From d8d849ee84515b3f88806dacbd2b2b2a9e529b5d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Jul 2015 18:27:23 +1000 Subject: [PATCH 06/83] hoist pre blocks last --- .../javascripts/discourse/dialects/dialect.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index b54011180a..e80e5e9671 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -188,14 +188,6 @@ function hoistCodeBlocksAndSpans(text) { // /!\ the order is important /!\ - //
...
code blocks - text = text.replace(/(\s|^)
([\s\S]*?)<\/pre>/ig, function(_, before, content) {
-    var hash = md5(content);
-    hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
-    return before + "
" + hash + "
"; - }); - - // fenced code blocks (AKA GitHub code blocks) text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) { var hash = md5(content); @@ -219,6 +211,13 @@ function hoistCodeBlocksAndSpans(text) { return before + " " + hash + "\n"; }); + //
...
code blocks + text = text.replace(/(\s|^)
([\s\S]*?)<\/pre>/ig, function(_, before, content) {
+    var hash = md5(content);
+    hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
+    return before + "
" + hash + "
"; + }); + // code spans (double & single `) ["``", "`"].forEach(function(delimiter) { var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g"); From 89d6d91c73b8150a01a7967c79a2987cf62c5679 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Jul 2015 01:55:52 -0700 Subject: [PATCH 07/83] better copy for set password emails --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 57c817df29..18d91311ac 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1370,7 +1370,7 @@ en: Click this link to choose a password now: %{base_url}/users/password-reset/%{email_token} - If you don't remember your password, or don't have one yet, choose "I forgot my password" when logging in with your email address. + (If the link above has expired, choose "I forgot my password" when logging in with your email address.) test_mailer: subject_template: "[%{site_name}] Email Deliverability Test" From 3a6bb64d97bbc26dd19903189ec640cd0d6654a3 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Jul 2015 02:00:36 -0700 Subject: [PATCH 08/83] copyedit on password link expired --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 18d91311ac..685aa84473 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -454,7 +454,7 @@ en: other: "almost %{count} years ago" password_reset: - no_token: "Sorry, that password change link is too old. Select 'I forgot my password' to get a new link." + no_token: "Sorry, that password change link is too old. Select the Log In button and use 'I forgot my password' to get a new link." choose_new: "Please choose a new password" choose: "Please choose a password" update: 'Update Password' From d95ad05d768af36de9acd12d5958de9cae9e446b Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Jul 2015 03:34:39 -0700 Subject: [PATCH 09/83] left align version number table in admin --- app/assets/stylesheets/common/admin/admin_base.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 166829bb16..8f2a24a2dd 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -698,10 +698,13 @@ section.details { .version-check { + th { + text-align: left !important; + } + .version-number { font-size: 1.286em; font-weight: bold; - text-align: center; } .face { From 233cdc011d8f6f04e68581f7427149897e3c4b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 12:42:19 +0200 Subject: [PATCH 10/83] FIX: disable text selection in polls --- plugins/poll/assets/stylesheets/common/poll.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss index 8730ef44c5..b15a657400 100644 --- a/plugins/poll/assets/stylesheets/common/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -10,6 +10,8 @@ div.poll { border: 1px solid $border-color; + @include unselectable; + ul, ol { margin: 0; padding: 0; From 9b819d92452b9be0c400f594d466781a0361fbcc Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Jul 2015 03:43:02 -0700 Subject: [PATCH 11/83] make polls unselectable https://meta.discourse.org/t/disabling-text-selection-in-polls/31586 --- plugins/poll/assets/stylesheets/desktop/poll.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/poll/assets/stylesheets/desktop/poll.scss b/plugins/poll/assets/stylesheets/desktop/poll.scss index e875c29a03..1ac54f6f9f 100644 --- a/plugins/poll/assets/stylesheets/desktop/poll.scss +++ b/plugins/poll/assets/stylesheets/desktop/poll.scss @@ -1,4 +1,6 @@ div.poll { + @include unselectable; + display: table; width: 500px; max-width: 500px; From 969f6ad1d010f46fc1e18b21cb725a21839a068b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 12:46:30 +0200 Subject: [PATCH 12/83] Revert "make polls unselectable" This reverts commit 9b819d92452b9be0c400f594d466781a0361fbcc. --- plugins/poll/assets/stylesheets/desktop/poll.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/poll/assets/stylesheets/desktop/poll.scss b/plugins/poll/assets/stylesheets/desktop/poll.scss index 1ac54f6f9f..e875c29a03 100644 --- a/plugins/poll/assets/stylesheets/desktop/poll.scss +++ b/plugins/poll/assets/stylesheets/desktop/poll.scss @@ -1,6 +1,4 @@ div.poll { - @include unselectable; - display: table; width: 500px; max-width: 500px; From 8f435fcbf6f8dc7dda077fc34ba645552cb3f960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 15:03:35 +0200 Subject: [PATCH 13/83] FIX: wrong track view header --- app/assets/javascripts/discourse/mixins/ajax.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index a659ed57be..1b8e2b626a 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -47,7 +47,7 @@ Discourse.Ajax = Em.Mixin.create({ if (_trackView && (!args.type || args.type === "GET")) { _trackView = false; - args.headers['Discourse-Track-View'] = true; + args.headers['HTTP_DISCOURSE_TRACK_VIEW'] = true; } args.success = function(data, textStatus, xhr) { From d7aa4e81d681f2ebb909244ffcb910d6e3e36a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 15:22:30 +0200 Subject: [PATCH 14/83] revert 8f435fcbf6f8dc --- app/assets/javascripts/discourse/mixins/ajax.js | 3 ++- lib/middleware/request_tracker.rb | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index 1b8e2b626a..6817150d51 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -47,7 +47,8 @@ Discourse.Ajax = Em.Mixin.create({ if (_trackView && (!args.type || args.type === "GET")) { _trackView = false; - args.headers['HTTP_DISCOURSE_TRACK_VIEW'] = true; + // DON'T CHANGE: rack is prepending "HTTP_" in the header's name + args.headers['DISCOURSE_TRACK_VIEW'] = true; } args.success = function(data, textStatus, xhr) { diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index 6a0b4d3d0c..7c5ab1ab7b 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -47,7 +47,6 @@ class Middleware::RequestTracker TRACK_VIEW = "HTTP_DISCOURSE_TRACK_VIEW".freeze CONTENT_TYPE = "Content-Type".freeze def self.get_data(env,result) - status,headers = result status = status.to_i From fb65970530edda395231e5a0871154e4c55a132c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 16:53:18 +0200 Subject: [PATCH 15/83] FIX: footer should also be hidden when using back/forward buttons --- .../discourse/controllers/topic.js.es6 | 5 ++-- .../discourse/initializers/show-footer.js.es6 | 15 +++++++++++ .../discourse/lib/static-route-builder.js.es6 | 4 +-- .../discourse/mixins/show-footer.js.es6 | 15 ----------- .../javascripts/discourse/routes/about.js.es6 | 4 +-- .../discourse/routes/badges-index.js.es6 | 4 +-- .../discourse/routes/badges-show.js.es6 | 4 +-- .../build-admin-user-posts-route.js.es6 | 16 ++++++------ .../routes/build-user-topic-list-route.js.es6 | 25 +++++++++---------- .../routes/discovery-categories-route.js.es6 | 3 +-- .../discourse/routes/discovery.js.es6 | 19 +++++++------- .../discourse/routes/exception.js.es6 | 8 ++---- .../discourse/routes/group-index.js.es6 | 18 ++++++------- .../discourse/routes/group-members.js.es6 | 8 +++--- .../discourse/routes/preferences.js.es6 | 3 +-- .../javascripts/discourse/routes/topic.js.es6 | 3 +-- .../routes/user-activity-stream.js.es6 | 15 ++++++----- .../discourse/routes/user-badges.js.es6 | 15 ++++++----- .../discourse/routes/user-invited-show.js.es6 | 21 ++++++++-------- .../routes/user-notifications.js.es6 | 11 ++++---- 20 files changed, 95 insertions(+), 121 deletions(-) create mode 100644 app/assets/javascripts/discourse/initializers/show-footer.js.es6 delete mode 100644 app/assets/javascripts/discourse/mixins/show-footer.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index aace6f8e0f..7dbce6dbb4 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -725,7 +725,8 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, { }, _showFooter: function() { - this.set("controllers.application.showFooter", this.get("model.postStream.loadedAllPosts")); - }.observes("model.postStream.loadedAllPosts") + const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts"); + this.set("controllers.application.showFooter", showFooter); + }.observes("model.postStream.{loaded,loadedAllPosts}") }); diff --git a/app/assets/javascripts/discourse/initializers/show-footer.js.es6 b/app/assets/javascripts/discourse/initializers/show-footer.js.es6 new file mode 100644 index 0000000000..ce58457774 --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/show-footer.js.es6 @@ -0,0 +1,15 @@ +export default { + name: "show-footer", + + initialize(container) { + const router = container.lookup("router:main"); + const application = container.lookup("controller:application"); + + // only take care of hiding the footer here + // controllers MUST take care of displaying it + router.on("willTransition", () => { + application.set("showFooter", false); + return true; + }); + } +}; diff --git a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 index 79365f4655..a1140f89a3 100644 --- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 +++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 @@ -1,5 +1,3 @@ -import ShowFooter from "discourse/mixins/show-footer"; - var configs = { 'faq': 'faq_url', 'tos': 'tos_url', @@ -7,7 +5,7 @@ var configs = { }; export default function(page) { - return Discourse.Route.extend(ShowFooter, { + return Discourse.Route.extend({ renderTemplate: function() { this.render('static'); }, diff --git a/app/assets/javascripts/discourse/mixins/show-footer.js.es6 b/app/assets/javascripts/discourse/mixins/show-footer.js.es6 deleted file mode 100644 index b97014153a..0000000000 --- a/app/assets/javascripts/discourse/mixins/show-footer.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -export default Em.Mixin.create({ - actions: { - didTransition() { - Em.run.schedule("afterRender", () => { - this.controllerFor("application").set("showFooter", true); - }); - return true; - }, - - willTransition() { - this.controllerFor("application").set("showFooter", false); - return true; - } - } -}); diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6 index 5258ea7fbd..3021dd0835 100644 --- a/app/assets/javascripts/discourse/routes/about.js.es6 +++ b/app/assets/javascripts/discourse/routes/about.js.es6 @@ -1,6 +1,4 @@ -import ShowFooter from "discourse/mixins/show-footer"; - -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend({ model: function() { return Discourse.ajax("/about.json").then(function(result) { return result.about; diff --git a/app/assets/javascripts/discourse/routes/badges-index.js.es6 b/app/assets/javascripts/discourse/routes/badges-index.js.es6 index 67af4625f2..fb7e990565 100644 --- a/app/assets/javascripts/discourse/routes/badges-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-index.js.es6 @@ -1,6 +1,4 @@ -import ShowFooter from "discourse/mixins/show-footer"; - -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend({ model: function() { if (PreloadStore.get('badges')) { return PreloadStore.getAndRemove('badges').then(function(json) { diff --git a/app/assets/javascripts/discourse/routes/badges-show.js.es6 b/app/assets/javascripts/discourse/routes/badges-show.js.es6 index 7b897f03bf..7cbb37c017 100644 --- a/app/assets/javascripts/discourse/routes/badges-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-show.js.es6 @@ -1,6 +1,4 @@ -import ShowFooter from "discourse/mixins/show-footer"; - -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend({ actions: { didTransition: function() { this.controllerFor("badges/show")._showFooter(); diff --git a/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6 b/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6 index 6d79b3b487..1615cdaca8 100644 --- a/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-admin-user-posts-route.js.es6 @@ -1,31 +1,29 @@ -import ShowFooter from "discourse/mixins/show-footer"; - export default function (filter) { - return Discourse.Route.extend(ShowFooter, { + return Discourse.Route.extend({ actions: { - didTransition: function() { - this.controllerFor('user').set('indexStream', true); + didTransition() { + this.controllerFor("user").set("indexStream", true); this.controllerFor("user-posts")._showFooter(); return true; } }, - model: function () { + model() { return this.modelFor("user").get("postsStream"); }, - afterModel: function () { + afterModel() { return this.modelFor("user").get("postsStream").filterBy(filter); }, - setupController: function(controller, model) { + setupController(controller, model) { // initialize "canLoadMore" model.set("canLoadMore", model.get("itemsLoaded") === 60); this.controllerFor("user-posts").set("model", model); }, - renderTemplate: function() { + renderTemplate() { this.render("user/posts", { into: "user" }); } }); diff --git a/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 b/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 index b5a83808d1..c14174fae7 100644 --- a/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 @@ -1,36 +1,35 @@ import UserTopicListRoute from "discourse/routes/user-topic-list"; -import ShowFooter from "discourse/mixins/show-footer"; // A helper to build a user topic list route -export default function (viewName, path) { - return UserTopicListRoute.extend(ShowFooter, { +export default (viewName, path) => { + return UserTopicListRoute.extend({ userActionType: Discourse.UserAction.TYPES.messages_received, actions: { - didTransition: function() { + didTransition() { this.controllerFor("user-topics-list")._showFooter(); return true; } }, - model: function() { - return this.store.findFiltered('topicList', {filter: 'topics/' + path + '/' + this.modelFor('user').get('username_lower')}); + model() { + return this.store.findFiltered("topicList", { filter: "topics/" + path + "/" + this.modelFor("user").get("username_lower") }); }, - setupController: function() { + setupController() { this._super.apply(this, arguments); - this.controllerFor('user-topics-list').setProperties({ + this.controllerFor("user-topics-list").setProperties({ hideCategory: true, showParticipants: true }); - this.controllerFor('user').set('pmView', viewName); - this.controllerFor('search').set('contextType', 'private_messages'); + this.controllerFor("user").set("pmView", viewName); + this.controllerFor("search").set("contextType", "private_messages"); }, - deactivate: function(){ - this.controllerFor('search').set('contextType', 'user'); + deactivate() { + this.controllerFor("search").set("contextType", "user"); } }); -} +}; diff --git a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 index c46b9b8275..2ac037c484 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 @@ -1,8 +1,7 @@ -import ShowFooter from 'discourse/mixins/show-footer'; import showModal from 'discourse/lib/show-modal'; import OpenComposer from "discourse/mixins/open-composer"; -Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, ShowFooter, { +Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { renderTemplate() { this.render('navigation/categories', { outlet: 'navigation-bar' }); this.render('discovery/categories', { outlet: 'list-container' }); diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6 index e5be7da1e3..eb8abb275f 100644 --- a/app/assets/javascripts/discourse/routes/discovery.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery.js.es6 @@ -2,14 +2,15 @@ The parent route for all discovery routes. Handles the logic for showing the loading spinners. **/ -import ShowFooter from "discourse/mixins/show-footer"; import OpenComposer from "discourse/mixins/open-composer"; import { scrollTop } from 'discourse/mixins/scroll-top'; -const DiscoveryRoute = Discourse.Route.extend(OpenComposer, ShowFooter, { - redirect: function() { return this.redirectIfLoginRequired(); }, +const DiscoveryRoute = Discourse.Route.extend(OpenComposer, { + redirect() { + return this.redirectIfLoginRequired(); + }, - beforeModel: function(transition) { + beforeModel(transition) { if (transition.intent.url === "/" && transition.targetName.indexOf("discovery.top") === -1 && Discourse.User.currentProp("should_be_redirected_to_top")) { @@ -19,30 +20,30 @@ const DiscoveryRoute = Discourse.Route.extend(OpenComposer, ShowFooter, { }, actions: { - loading: function() { + loading() { this.controllerFor('discovery').set("loading", true); return true; }, - loadingComplete: function() { + loadingComplete() { this.controllerFor('discovery').set('loading', false); if (!this.session.get('topicListScrollPosition')) { scrollTop(); } }, - didTransition: function() { + didTransition() { this.controllerFor("discovery")._showFooter(); this.send('loadingComplete'); return true; }, // clear a pinned topic - clearPin: function(topic) { + clearPin(topic) { topic.clearPin(); }, - createTopic: function() { + createTopic() { this.openComposer(this.controllerFor('discovery/topics')); } } diff --git a/app/assets/javascripts/discourse/routes/exception.js.es6 b/app/assets/javascripts/discourse/routes/exception.js.es6 index a5c5fab878..d20795d028 100644 --- a/app/assets/javascripts/discourse/routes/exception.js.es6 +++ b/app/assets/javascripts/discourse/routes/exception.js.es6 @@ -1,7 +1,3 @@ -import ShowFooter from "discourse/mixins/show-footer"; - -export default Discourse.Route.extend(ShowFooter, { - serialize: function() { - return ""; - } +export default Discourse.Route.extend({ + serialize() { return ""; } }); diff --git a/app/assets/javascripts/discourse/routes/group-index.js.es6 b/app/assets/javascripts/discourse/routes/group-index.js.es6 index 63a1a23f76..89dec1edd2 100644 --- a/app/assets/javascripts/discourse/routes/group-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-index.js.es6 @@ -1,18 +1,14 @@ -import ShowFooter from "discourse/mixins/show-footer"; - -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend({ actions: { - didTransition: function() { - return true; - } + didTransition() { return true; } }, - model: function() { - return this.modelFor('group').findPosts(); + model() { + return this.modelFor("group").findPosts(); }, - setupController: function(controller, model) { - controller.set('model', model); - this.controllerFor('group').set('showing', 'index'); + setupController(controller, model) { + controller.set("model", model); + this.controllerFor("group").set("showing", "index"); } }); diff --git a/app/assets/javascripts/discourse/routes/group-members.js.es6 b/app/assets/javascripts/discourse/routes/group-members.js.es6 index 4713495d8f..22e328cacf 100644 --- a/app/assets/javascripts/discourse/routes/group-members.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-members.js.es6 @@ -1,12 +1,10 @@ -import ShowFooter from "discourse/mixins/show-footer"; - -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend({ model() { - return this.modelFor('group'); + return this.modelFor("group"); }, setupController(controller, model) { - this.controllerFor('group').set('showing', 'members'); + this.controllerFor("group").set("showing", "members"); controller.set("model", model); model.findMembers(); } diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 67fd1b923e..d748689f40 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -1,8 +1,7 @@ -import ShowFooter from "discourse/mixins/show-footer"; import RestrictedUserRoute from "discourse/routes/restricted-user"; import showModal from 'discourse/lib/show-modal'; -export default RestrictedUserRoute.extend(ShowFooter, { +export default RestrictedUserRoute.extend({ model() { return this.modelFor('user'); }, diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 63762c7b55..7d5f253341 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -4,10 +4,9 @@ let isTransitioning = false, const SCROLL_DELAY = 500; -import ShowFooter from "discourse/mixins/show-footer"; import showModal from 'discourse/lib/show-modal'; -const TopicRoute = Discourse.Route.extend(ShowFooter, { +const TopicRoute = Discourse.Route.extend({ redirect() { return this.redirectIfLoginRequired(); }, queryParams: { diff --git a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 index bec825f849..e77b58da10 100644 --- a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 @@ -1,32 +1,31 @@ -import ShowFooter from "discourse/mixins/show-footer"; import ViewingActionType from "discourse/mixins/viewing-action-type"; -export default Discourse.Route.extend(ShowFooter, ViewingActionType, { - model: function() { +export default Discourse.Route.extend(ViewingActionType, { + model() { return this.modelFor('user').get('stream'); }, - afterModel: function() { + afterModel() { return this.modelFor('user').get('stream').filterBy(this.get('userActionType')); }, - renderTemplate: function() { + renderTemplate() { this.render('user_stream'); }, - setupController: function(controller, model) { + setupController(controller, model) { controller.set('model', model); this.viewingActionType(this.get('userActionType')); }, actions: { - didTransition: function() { + didTransition() { this.controllerFor("user-activity")._showFooter(); return true; }, - removeBookmark: function(userAction) { + removeBookmark(userAction) { var user = this.modelFor('user'); Discourse.Post.updateBookmark(userAction.get('post_id'), false) .then(function() { diff --git a/app/assets/javascripts/discourse/routes/user-badges.js.es6 b/app/assets/javascripts/discourse/routes/user-badges.js.es6 index d3b39a3b93..9594cc73b3 100644 --- a/app/assets/javascripts/discourse/routes/user-badges.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-badges.js.es6 @@ -1,17 +1,16 @@ -import ShowFooter from "discourse/mixins/show-footer"; import ViewingActionType from "discourse/mixins/viewing-action-type"; -export default Discourse.Route.extend(ShowFooter, ViewingActionType, { - model: function() { - return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'), {grouped: true}); +export default Discourse.Route.extend(ViewingActionType, { + model() { + return Discourse.UserBadge.findByUsername(this.modelFor("user").get("username_lower"), { grouped: true }); }, - setupController: function(controller, model) { + setupController(controller, model) { this.viewingActionType(-1); - controller.set('model', model); + controller.set("model", model); }, - renderTemplate: function() { - this.render('user/badges', {into: 'user'}); + renderTemplate() { + this.render("user/badges", {into: "user"}); } }); diff --git a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 index 6c42105dd6..21317f3090 100644 --- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 @@ -1,33 +1,32 @@ -import ShowFooter from 'discourse/mixins/show-footer'; -import showModal from 'discourse/lib/show-modal'; +import showModal from "discourse/lib/show-modal"; -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend({ - model: function(params) { + model(params) { this.inviteFilter = params.filter; - return Discourse.Invite.findInvitedBy(this.modelFor('user'), params.filter); + return Discourse.Invite.findInvitedBy(this.modelFor("user"), params.filter); }, - afterModel: function(model) { + afterModel(model) { if (!model.can_see_invite_details) { - this.replaceWith('userInvited.show', 'redeemed'); + this.replaceWith("userInvited.show", "redeemed"); } }, setupController(controller, model) { controller.setProperties({ model: model, - user: this.controllerFor('user').get('model'), + user: this.controllerFor("user").get("model"), filter: this.inviteFilter, - searchTerm: '', + searchTerm: "", totalInvites: model.invites.length }); }, actions: { showInvite() { - showModal('invite', { model: this.currentUser }); - this.controllerFor('invite').reset(); + showModal("invite", { model: this.currentUser }); + this.controllerFor("invite").reset(); }, uploadSuccess(filename) { diff --git a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications.js.es6 index a90c131af2..2b497d538f 100644 --- a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-notifications.js.es6 @@ -1,7 +1,6 @@ -import ShowFooter from "discourse/mixins/show-footer"; import ViewingActionType from "discourse/mixins/viewing-action-type"; -export default Discourse.Route.extend(ShowFooter, ViewingActionType, { +export default Discourse.Route.extend(ViewingActionType, { actions: { didTransition() { this.controllerFor("user-notifications")._showFooter(); @@ -10,13 +9,13 @@ export default Discourse.Route.extend(ShowFooter, ViewingActionType, { }, model() { - var user = this.modelFor('user'); - return this.store.find('notification', {username: user.get('username')}); + var user = this.modelFor("user"); + return this.store.find("notification", { username: user.get("username") }); }, setupController(controller, model) { - controller.set('model', model); - controller.set('user', this.modelFor('user')); + controller.set("model", model); + controller.set("user", this.modelFor("user")); this.viewingActionType(-1); } }); From 1a5c3b4331dfa90f32aa46811d0b3aa8b8dc1176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 17:18:38 +0200 Subject: [PATCH 16/83] FIX: some pages were missing the footer --- .../discourse/controllers/static.js.es6 | 4 +-- .../discourse/lib/static-route-builder.js.es6 | 36 ++++++++++--------- .../javascripts/discourse/routes/about.js.es6 | 14 +++++--- .../discourse/routes/badges-index.js.es6 | 20 ++++++----- .../discourse/routes/badges-show.js.es6 | 36 +++++++++---------- .../discourse/routes/discourse.js.es6 | 14 ++++---- .../routes/discovery-categories-route.js.es6 | 36 ++++++++++--------- .../discourse/routes/user-badges.js.es6 | 6 ++++ 8 files changed, 94 insertions(+), 72 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index 2e9050b2d5..c22ce3c26d 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -1,8 +1,8 @@ export default Ember.Controller.extend({ - showLoginButton: Em.computed.equal('model.path', 'login'), + showLoginButton: Em.computed.equal("model.path", "login"), actions: { - markFaqRead: function() { + markFaqRead() { if (this.currentUser) { Discourse.ajax("/users/read-faq", { method: "POST" }); } diff --git a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 index a1140f89a3..fa58bf5182 100644 --- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 +++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 @@ -1,37 +1,41 @@ -var configs = { - 'faq': 'faq_url', - 'tos': 'tos_url', - 'privacy': 'privacy_policy_url' +const configs = { + "faq": "faq_url", + "tos": "tos_url", + "privacy": "privacy_policy_url" }; -export default function(page) { +export default (page) => { return Discourse.Route.extend({ - renderTemplate: function() { - this.render('static'); + renderTemplate() { + this.render("static"); }, - beforeModel: function(transition) { - var configKey = configs[page]; + beforeModel(transition) { + const configKey = configs[page]; if (configKey && Discourse.SiteSettings[configKey].length > 0) { transition.abort(); Discourse.URL.redirectTo(Discourse.SiteSettings[configKey]); } }, - activate: function() { + activate() { this._super(); - // Scroll to an element if exists Discourse.URL.scrollToId(document.location.hash); }, - model: function() { + model() { return Discourse.StaticPage.find(page); }, - setupController: function(controller, model) { - this.controllerFor('static').set('model', model); + setupController(controller, model) { + this.controllerFor("static").set("model", model); + }, + + actions: { + didTransition() { + this.controllerFor("application").set("showFooter", true); + } } }); -} - +}; diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6 index 3021dd0835..9141db775f 100644 --- a/app/assets/javascripts/discourse/routes/about.js.es6 +++ b/app/assets/javascripts/discourse/routes/about.js.es6 @@ -1,11 +1,15 @@ export default Discourse.Route.extend({ - model: function() { - return Discourse.ajax("/about.json").then(function(result) { - return result.about; - }); + model() { + return Discourse.ajax("/about.json").then(result => result.about); }, - titleToken: function() { + titleToken() { return I18n.t('about.simple_title'); + }, + + actions: { + didTransition() { + this.controllerFor("application").set("showFooter", true); + } } }); diff --git a/app/assets/javascripts/discourse/routes/badges-index.js.es6 b/app/assets/javascripts/discourse/routes/badges-index.js.es6 index fb7e990565..c1c13445c0 100644 --- a/app/assets/javascripts/discourse/routes/badges-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-index.js.es6 @@ -1,15 +1,19 @@ export default Discourse.Route.extend({ - model: function() { - if (PreloadStore.get('badges')) { - return PreloadStore.getAndRemove('badges').then(function(json) { - return Discourse.Badge.createFromJson(json); - }); + model() { + if (PreloadStore.get("badges")) { + return PreloadStore.getAndRemove("badges").then(json => Discourse.Badge.createFromJson(json)); } else { - return Discourse.Badge.findAll({onlyListable: true}); + return Discourse.Badge.findAll({ onlyListable: true }); } }, - titleToken: function() { - return I18n.t('badges.title'); + titleToken() { + return I18n.t("badges.title"); + }, + + actions: { + didTransition() { + this.controllerFor("application").set("showFooter", true); + } } }); diff --git a/app/assets/javascripts/discourse/routes/badges-show.js.es6 b/app/assets/javascripts/discourse/routes/badges-show.js.es6 index 7cbb37c017..cc4cf0eaa9 100644 --- a/app/assets/javascripts/discourse/routes/badges-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-show.js.es6 @@ -1,41 +1,41 @@ export default Discourse.Route.extend({ actions: { - didTransition: function() { + didTransition() { this.controllerFor("badges/show")._showFooter(); return true; } }, - serialize: function(model) { - return {id: model.get('id'), slug: model.get('name').replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase()}; + serialize(model) { + return { + id: model.get("id"), + slug: model.get("name").replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase() + }; }, - model: function(params) { - if (PreloadStore.get('badge')) { - return PreloadStore.getAndRemove('badge').then(function(json) { - return Discourse.Badge.createFromJson(json); - }); + model(params) { + if (PreloadStore.get("badge")) { + return PreloadStore.getAndRemove("badge").then(json => Discourse.Badge.createFromJson(json)); } else { return Discourse.Badge.findById(params.id); } }, - afterModel: function(model) { - var self = this; - return Discourse.UserBadge.findByBadgeId(model.get('id')).then(function(userBadges) { - self.userBadges = userBadges; + afterModel(model) { + return Discourse.UserBadge.findByBadgeId(model.get("id")).then(userBadges => { + this.userBadges = userBadges; }); }, - titleToken: function() { - var model = this.modelFor('badges.show'); + titleToken() { + const model = this.modelFor("badges.show"); if (model) { - return model.get('displayName'); + return model.get("displayName"); } }, - setupController: function(controller, model) { - controller.set('model', model); - controller.set('userBadges', this.userBadges); + setupController(controller, model) { + controller.set("model", model); + controller.set("userBadges", this.userBadges); } }); diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index 2a26642805..a6bba2cb6a 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -4,7 +4,7 @@ const DiscourseRoute = Ember.Route.extend({ // changes resfreshQueryWithoutTransition: false, - refresh: function() { + refresh() { if (!this.refreshQueryWithoutTransition) { return this._super(); } if (!this.router.router.activeTransition) { @@ -17,13 +17,13 @@ const DiscourseRoute = Ember.Route.extend({ } }, - _refreshTitleOnce: function() { + _refreshTitleOnce() { this.send('_collectTitleTokens', []); }, actions: { - _collectTitleTokens: function(tokens) { + _collectTitleTokens(tokens) { // If there's a title token method, call it and get the token if (this.titleToken) { const t = this.titleToken(); @@ -40,19 +40,19 @@ const DiscourseRoute = Ember.Route.extend({ return true; }, - refreshTitle: function() { + refreshTitle() { Ember.run.once(this, this._refreshTitleOnce); } }, - redirectIfLoginRequired: function() { + redirectIfLoginRequired() { const app = this.controllerFor('application'); if (app.get('loginRequired')) { this.replaceWith('login'); } }, - openTopicDraft: function(model){ + openTopicDraft(model){ // If there's a draft, open the create topic composer if (model.draft) { const composer = this.controllerFor('composer'); @@ -67,7 +67,7 @@ const DiscourseRoute = Ember.Route.extend({ } }, - isPoppedState: function(transition) { + isPoppedState(transition) { return (!transition._discourse_intercepted) && (!!transition.intent.url); } }); diff --git a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 index 2ac037c484..97540914c7 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 @@ -1,14 +1,14 @@ -import showModal from 'discourse/lib/show-modal'; +import showModal from "discourse/lib/show-modal"; import OpenComposer from "discourse/mixins/open-composer"; Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { renderTemplate() { - this.render('navigation/categories', { outlet: 'navigation-bar' }); - this.render('discovery/categories', { outlet: 'list-container' }); + this.render("navigation/categories", { outlet: "navigation-bar" }); + this.render("discovery/categories", { outlet: "list-container" }); }, beforeModel() { - this.controllerFor('navigation/categories').set('filterMode', 'categories'); + this.controllerFor("navigation/categories").set("filterMode", "categories"); }, model() { @@ -16,11 +16,11 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { // if default page is categories PreloadStore.remove("topic_list"); - return Discourse.CategoryList.list('categories').then(function(list) { + return Discourse.CategoryList.list("categories").then(function(list) { const tracking = Discourse.TopicTrackingState.current(); if (tracking) { - tracking.sync(list, 'categories'); - tracking.trackIncoming('categories'); + tracking.sync(list, "categories"); + tracking.trackIncoming("categories"); } return list; }); @@ -28,15 +28,15 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { titleToken() { if (Discourse.Utilities.defaultHomepage() === "categories") { return; } - return I18n.t('filters.categories.title'); + return I18n.t("filters.categories.title"); }, setupController(controller, model) { - controller.set('model', model); + controller.set("model", model); // Only show either the Create Category or Create Topic button - this.controllerFor('navigation/categories').set('canCreateCategory', model.get('can_create_category')); - this.controllerFor('navigation/categories').set('canCreateTopic', model.get('can_create_topic') && !model.get('can_create_category')); + this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category")); + this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic") && !model.get("can_create_category")); this.openTopicDraft(model); }, @@ -44,20 +44,24 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { actions: { createCategory() { const groups = this.site.groups, - everyoneName = groups.findBy('id', 0).name; + everyoneName = groups.findBy("id", 0).name; const model = Discourse.Category.create({ - color: 'AB9364', text_color: 'FFFFFF', group_permissions: [{group_name: everyoneName, permission_type: 1}], + color: "AB9364", text_color: "FFFFFF", group_permissions: [{group_name: everyoneName, permission_type: 1}], available_groups: groups.map(g => g.name), allow_badges: true }); - showModal('editCategory', { model }); - this.controllerFor('editCategory').set('selectedTab', 'general'); + showModal("editCategory", { model }); + this.controllerFor("editCategory").set("selectedTab", "general"); }, createTopic() { - this.openComposer(this.controllerFor('discovery/categories')); + this.openComposer(this.controllerFor("discovery/categories")); + }, + + didTransition() { + this.controllerFor("application").set("showFooter", true); } } }); diff --git a/app/assets/javascripts/discourse/routes/user-badges.js.es6 b/app/assets/javascripts/discourse/routes/user-badges.js.es6 index 9594cc73b3..4a89d50acf 100644 --- a/app/assets/javascripts/discourse/routes/user-badges.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-badges.js.es6 @@ -12,5 +12,11 @@ export default Discourse.Route.extend(ViewingActionType, { renderTemplate() { this.render("user/badges", {into: "user"}); + }, + + actions: { + didTransition() { + this.controllerFor("application").set("showFooter", true); + } } }); From d71301e4064551a0776fad5a70c89e6939366634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Jul 2015 20:16:37 +0200 Subject: [PATCH 17/83] FIX: always 'return true' when overriding a route action --- .../discourse/lib/static-route-builder.js.es6 | 1 + .../javascripts/discourse/routes/about.js.es6 | 1 + .../discourse/routes/badges-index.js.es6 | 1 + .../routes/discovery-categories-route.js.es6 | 1 + .../discourse/routes/discovery.js.es6 | 12 ++++++------ .../routes/user-activity-index.js.es6 | 4 ++-- .../routes/user-activity-stream.js.es6 | 18 +++++++++--------- .../discourse/routes/user-badges.js.es6 | 1 + .../discourse/routes/user-notifications.js.es6 | 3 +-- 9 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 index fa58bf5182..09ef6736c2 100644 --- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 +++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 @@ -35,6 +35,7 @@ export default (page) => { actions: { didTransition() { this.controllerFor("application").set("showFooter", true); + return true; } } }); diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6 index 9141db775f..f25d64387b 100644 --- a/app/assets/javascripts/discourse/routes/about.js.es6 +++ b/app/assets/javascripts/discourse/routes/about.js.es6 @@ -10,6 +10,7 @@ export default Discourse.Route.extend({ actions: { didTransition() { this.controllerFor("application").set("showFooter", true); + return true; } } }); diff --git a/app/assets/javascripts/discourse/routes/badges-index.js.es6 b/app/assets/javascripts/discourse/routes/badges-index.js.es6 index c1c13445c0..dfb207391f 100644 --- a/app/assets/javascripts/discourse/routes/badges-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-index.js.es6 @@ -14,6 +14,7 @@ export default Discourse.Route.extend({ actions: { didTransition() { this.controllerFor("application").set("showFooter", true); + return true; } } }); diff --git a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 index 97540914c7..61fd853d10 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 @@ -62,6 +62,7 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { didTransition() { this.controllerFor("application").set("showFooter", true); + return true; } } }); diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6 index eb8abb275f..98d182c42a 100644 --- a/app/assets/javascripts/discourse/routes/discovery.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery.js.es6 @@ -3,7 +3,7 @@ Handles the logic for showing the loading spinners. **/ import OpenComposer from "discourse/mixins/open-composer"; -import { scrollTop } from 'discourse/mixins/scroll-top'; +import { scrollTop } from "discourse/mixins/scroll-top"; const DiscoveryRoute = Discourse.Route.extend(OpenComposer, { redirect() { @@ -21,20 +21,20 @@ const DiscoveryRoute = Discourse.Route.extend(OpenComposer, { actions: { loading() { - this.controllerFor('discovery').set("loading", true); + this.controllerFor("discovery").set("loading", true); return true; }, loadingComplete() { - this.controllerFor('discovery').set('loading', false); - if (!this.session.get('topicListScrollPosition')) { + this.controllerFor("discovery").set("loading", false); + if (!this.session.get("topicListScrollPosition")) { scrollTop(); } }, didTransition() { this.controllerFor("discovery")._showFooter(); - this.send('loadingComplete'); + this.send("loadingComplete"); return true; }, @@ -44,7 +44,7 @@ const DiscoveryRoute = Discourse.Route.extend(OpenComposer, { }, createTopic() { - this.openComposer(this.controllerFor('discovery/topics')); + this.openComposer(this.controllerFor("discovery/topics")); } } diff --git a/app/assets/javascripts/discourse/routes/user-activity-index.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-index.js.es6 index 3f5ae1272a..926ba744e4 100644 --- a/app/assets/javascripts/discourse/routes/user-activity-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-activity-index.js.es6 @@ -4,9 +4,9 @@ export default UserActivityStreamRoute.extend({ userActionType: undefined, actions: { - didTransition: function() { + didTransition() { this._super(); - this.controllerFor('user').set('indexStream', true); + this.controllerFor("user").set("indexStream", true); return true; } } diff --git a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 index e77b58da10..7447d71217 100644 --- a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 @@ -2,20 +2,20 @@ import ViewingActionType from "discourse/mixins/viewing-action-type"; export default Discourse.Route.extend(ViewingActionType, { model() { - return this.modelFor('user').get('stream'); + return this.modelFor("user").get("stream"); }, afterModel() { - return this.modelFor('user').get('stream').filterBy(this.get('userActionType')); + return this.modelFor("user").get("stream").filterBy(this.get("userActionType")); }, renderTemplate() { - this.render('user_stream'); + this.render("user_stream"); }, setupController(controller, model) { - controller.set('model', model); - this.viewingActionType(this.get('userActionType')); + controller.set("model", model); + this.viewingActionType(this.get("userActionType")); }, actions: { @@ -26,13 +26,13 @@ export default Discourse.Route.extend(ViewingActionType, { }, removeBookmark(userAction) { - var user = this.modelFor('user'); - Discourse.Post.updateBookmark(userAction.get('post_id'), false) + var user = this.modelFor("user"); + Discourse.Post.updateBookmark(userAction.get("post_id"), false) .then(function() { // remove the user action from the stream - user.get('stream').remove(userAction); + user.get("stream").remove(userAction); // update the counts - user.get('stats').forEach(function (stat) { + user.get("stats").forEach(function (stat) { if (stat.get("action_type") === userAction.action_type) { stat.decrementProperty("count"); } diff --git a/app/assets/javascripts/discourse/routes/user-badges.js.es6 b/app/assets/javascripts/discourse/routes/user-badges.js.es6 index 4a89d50acf..fcd099d765 100644 --- a/app/assets/javascripts/discourse/routes/user-badges.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-badges.js.es6 @@ -17,6 +17,7 @@ export default Discourse.Route.extend(ViewingActionType, { actions: { didTransition() { this.controllerFor("application").set("showFooter", true); + return true; } } }); diff --git a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications.js.es6 index 2b497d538f..7c2384db01 100644 --- a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-notifications.js.es6 @@ -9,8 +9,7 @@ export default Discourse.Route.extend(ViewingActionType, { }, model() { - var user = this.modelFor("user"); - return this.store.find("notification", { username: user.get("username") }); + return this.store.find("notification", { username: this.modelFor("user").get("username") }); }, setupController(controller, model) { From cf91bca0cd79f4589f1787e2c0a72e37606e471e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 31 Jul 2015 14:22:28 -0400 Subject: [PATCH 18/83] FIX: Small actions should show descriptions on the user stream --- .../discourse/components/small-action.js.es6 | 29 +++++++--------- .../discourse/components/stream-item.js.es6 | 7 ++++ .../controllers/user-activity.js.es6 | 2 +- .../templates/components/small-action.hbs | 5 ++- .../templates/components/stream-item.hbs | 32 +++++++++++++++++ .../discourse/templates/user/stream.hbs | 30 ++-------------- .../views/user-activity-stream.js.es6 | 27 +++++++++++++++ .../discourse/views/user-stream.js.es6 | 6 ++-- app/controllers/user_actions_controller.rb | 2 +- app/models/user_action.rb | 1 + app/models/user_action_observer.rb | 34 +++++++++---------- app/serializers/user_action_serializer.rb | 9 ++--- 12 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/stream-item.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/stream-item.hbs create mode 100644 app/assets/javascripts/discourse/views/user-activity-stream.js.es6 diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6 index 50e42f19ab..977b408c80 100644 --- a/app/assets/javascripts/discourse/components/small-action.js.es6 +++ b/app/assets/javascripts/discourse/components/small-action.js.es6 @@ -13,27 +13,22 @@ const icons = { 'visible.disabled': 'eye-slash' }; +export function actionDescription(actionCode, createdAt) { + return function() { + const ac = this.get(actionCode); + if (actionCode) { + const dt = new Date(this.get(createdAt)); + const when = Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'}); + return I18n.t(`action_codes.${ac}`, {when}).htmlSafe(); + } + }.property(actionCode, createdAt); +} + 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'}); - var result = I18n.t(`action_codes.${actionCode}`, {when}); - var cooked = this.get('post.cooked'); - - result = "

" + result + "

"; - - if (!Em.isEmpty(cooked)) { - result += "
" + cooked + "
"; - } - - return result; - } - }.property('actionCode', 'post.created_at', 'post.cooked'), + description: actionDescription('actionCode', 'post.created_at'), icon: function() { return icons[this.get('actionCode')] || 'exclamation'; diff --git a/app/assets/javascripts/discourse/components/stream-item.js.es6 b/app/assets/javascripts/discourse/components/stream-item.js.es6 new file mode 100644 index 0000000000..ef7b03ad5c --- /dev/null +++ b/app/assets/javascripts/discourse/components/stream-item.js.es6 @@ -0,0 +1,7 @@ +import { actionDescription } from 'discourse/components/small-action'; + +export default Ember.Component.extend({ + classNameBindings: [':item', 'item.hidden', 'item.deleted', 'moderatorAction'], + moderatorAction: Discourse.computed.propertyEqual('item.post_type', 'site.post_types.moderator_action'), + actionDescription: actionDescription('item.action_code', 'item.created_at') +}); diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 index 6113cedff3..7e4ec2db92 100644 --- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 @@ -5,7 +5,7 @@ export default Ember.ObjectController.extend({ _showFooter: function() { var showFooter; if (this.get("userActionType")) { - var stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") }); + const stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") }); showFooter = stat && stat.count <= this.get("model.stream.itemsLoaded"); } else { showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded"); diff --git a/app/assets/javascripts/discourse/templates/components/small-action.hbs b/app/assets/javascripts/discourse/templates/components/small-action.hbs index b05c658088..091f0e07df 100644 --- a/app/assets/javascripts/discourse/templates/components/small-action.hbs +++ b/app/assets/javascripts/discourse/templates/components/small-action.hbs @@ -11,5 +11,8 @@ {{avatar post imageSize="small"}} {{/if}} - {{{description}}} +

{{description}}

+ {{#if post.cooked}} +
{{{post.cooked}}}
+ {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/stream-item.hbs b/app/assets/javascripts/discourse/templates/components/stream-item.hbs new file mode 100644 index 0000000000..8dea1ccb13 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/stream-item.hbs @@ -0,0 +1,32 @@ +
+
{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}
+ {{format-date item.created_at}} + {{topic-status topic=item disableActions=true}} + + {{{item.title}}} + +
{{category-link item.category}}
+
+ +{{#if actionDescription}} +

{{actionDescription}}

+{{/if}} + +

{{{item.excerpt}}}

+ + +{{#each item.children as |child|}} +
+ + {{#each child.items as |grandChild|}} + {{#if grandChild.removableBookmark}} + + {{else}} +
{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}
+ {{#if grandChild.edit_reason}} — {{grandChild.edit_reason}}{{/if}} + {{/if}} + {{/each}} +
+{{/each}} diff --git a/app/assets/javascripts/discourse/templates/user/stream.hbs b/app/assets/javascripts/discourse/templates/user/stream.hbs index 96e96c8376..4f03bef896 100644 --- a/app/assets/javascripts/discourse/templates/user/stream.hbs +++ b/app/assets/javascripts/discourse/templates/user/stream.hbs @@ -1,29 +1,3 @@ -{{#each item in model.content}} -
-
-
{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}
- {{format-date item.created_at}} - {{topic-status topic=item disableActions=true}} - - {{{unbound item.title}}} - -
{{category-link item.category}}
-
-

{{{unbound item.excerpt}}}

- {{#each child in item.children}} -
- - {{#each grandChild in child.items}} - {{#if grandChild.removableBookmark}} - - {{else}} -
{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}
- {{#if grandChild.edit_reason}} — {{unbound grandChild.edit_reason}}{{/if}} - {{/if}} - {{/each}} -
- {{/each}} -
+{{#each model.content as |item|}} + {{stream-item item=item}} {{/each}} diff --git a/app/assets/javascripts/discourse/views/user-activity-stream.js.es6 b/app/assets/javascripts/discourse/views/user-activity-stream.js.es6 new file mode 100644 index 0000000000..86fbaefd8f --- /dev/null +++ b/app/assets/javascripts/discourse/views/user-activity-stream.js.es6 @@ -0,0 +1,27 @@ +import LoadMore from "discourse/mixins/load-more"; + +export default Ember.View.extend(LoadMore, { + loading: false, + eyelineSelector: '.user-stream .item', + classNames: ['user-stream'], + + _scrollTopOnModelChange: function() { + Em.run.schedule('afterRender', function() { + $(document).scrollTop(0); + }); + }.observes('controller.model.user.id'), + + actions: { + loadMore() { + const self = this; + if (this.get('loading')) { return; } + + this.set('loading', true); + const stream = this.get('controller.model'); + stream.findItems().then(function() { + self.set('loading', false); + self.get('eyeline').flushRest(); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/views/user-stream.js.es6 b/app/assets/javascripts/discourse/views/user-stream.js.es6 index 367c089ed0..86fbaefd8f 100644 --- a/app/assets/javascripts/discourse/views/user-stream.js.es6 +++ b/app/assets/javascripts/discourse/views/user-stream.js.es6 @@ -12,12 +12,12 @@ export default Ember.View.extend(LoadMore, { }.observes('controller.model.user.id'), actions: { - loadMore: function() { - var self = this; + loadMore() { + const self = this; if (this.get('loading')) { return; } this.set('loading', true); - var stream = this.get('controller.model'); + const stream = this.get('controller.model'); stream.findItems().then(function() { self.set('loading', false); self.get('eyeline').flushRest(); diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb index e4d4079fec..c20f9c8d23 100644 --- a/app/controllers/user_actions_controller.rb +++ b/app/controllers/user_actions_controller.rb @@ -24,7 +24,7 @@ class UserActionsController < ApplicationController UserAction.stream(opts) end - render_serialized(stream, UserActionSerializer, root: "user_actions") + render_serialized(stream, UserActionSerializer, root: 'user_actions') end def show diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 0d4c661d10..9b72a755de 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -154,6 +154,7 @@ SQL CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted, p.hidden, p.post_type, + p.action_code, p.edit_reason, t.category_id FROM user_actions as a diff --git a/app/models/user_action_observer.rb b/app/models/user_action_observer.rb index 665a1efdfc..34e6fcabed 100644 --- a/app/models/user_action_observer.rb +++ b/app/models/user_action_observer.rb @@ -29,11 +29,11 @@ class UserActionObserver < ActiveRecord::Observer return unless action && post && user && post.id row = { - action_type: action, - user_id: user.id, - acting_user_id: acting_user_id || post.user_id, - target_topic_id: post.topic_id, - target_post_id: post.id + action_type: action, + user_id: user.id, + acting_user_id: acting_user_id || post.user_id, + target_topic_id: post.topic_id, + target_post_id: post.id } if post.deleted_at.nil? @@ -48,12 +48,12 @@ class UserActionObserver < ActiveRecord::Observer return if model.is_first_post? row = { - action_type: UserAction::REPLY, - user_id: model.user_id, - acting_user_id: model.user_id, - target_post_id: model.id, - target_topic_id: model.topic_id, - created_at: model.created_at + action_type: UserAction::REPLY, + user_id: model.user_id, + acting_user_id: model.user_id, + target_post_id: model.id, + target_topic_id: model.topic_id, + created_at: model.created_at } rows = [row] @@ -79,12 +79,12 @@ class UserActionObserver < ActiveRecord::Observer def log_topic(model) row = { - action_type: model.archetype == Archetype.private_message ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::NEW_TOPIC, - user_id: model.user_id, - acting_user_id: model.user_id, - target_topic_id: model.id, - target_post_id: -1, - created_at: model.created_at + action_type: model.archetype == Archetype.private_message ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::NEW_TOPIC, + user_id: model.user_id, + acting_user_id: model.user_id, + target_topic_id: model.id, + target_post_id: -1, + created_at: model.created_at } rows = [row] diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index f6dcf2b9fb..8b39399639 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -22,7 +22,8 @@ class UserActionSerializer < ApplicationSerializer :title, :deleted, :hidden, - :moderator_action, + :post_type, + :action_code, :edit_reason, :category_id, :uploaded_avatar_id, @@ -32,7 +33,7 @@ class UserActionSerializer < ApplicationSerializer def excerpt cooked = object.cooked || PrettyText.cook(object.raw) - PrettyText.excerpt(cooked, 300, { keep_emojis: true }) if cooked + PrettyText.excerpt(cooked, 300, keep_emojis: true) if cooked end def avatar_template @@ -67,10 +68,6 @@ class UserActionSerializer < ApplicationSerializer object.title.present? end - def moderator_action - object.post_type == Post.types[:moderator_action] || object.post_type == Post.types[:small_action] - end - def include_reply_to_post_number? object.action_type == UserAction::REPLY end From 76aa0795b35f3153a006084334c59090d61ec3b6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 31 Jul 2015 16:30:18 -0400 Subject: [PATCH 19/83] Use small actions for moving posts --- .../discourse/components/small-action.js.es6 | 3 +- .../discourse/controllers/merge-topic.js.es6 | 37 ++++++++------ .../discourse/controllers/split-topic.js.es6 | 49 +++++++++---------- .../discourse/lib/ajax-error.js.es6 | 4 +- .../javascripts/discourse/models/topic.js.es6 | 43 ++++++++-------- .../discourse/templates/modal/merge-topic.hbs | 10 ++-- .../{split_topic.hbs => split-topic.hbs} | 10 ++-- .../discourse/views/split-topic.js.es6 | 2 +- app/assets/javascripts/main_include.js | 2 + app/models/post_mover.rb | 8 ++- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 8 +-- 12 files changed, 88 insertions(+), 89 deletions(-) rename app/assets/javascripts/discourse/templates/modal/{split_topic.hbs => split-topic.hbs} (59%) diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6 index 977b408c80..4ffa7ac63f 100644 --- a/app/assets/javascripts/discourse/components/small-action.js.es6 +++ b/app/assets/javascripts/discourse/components/small-action.js.es6 @@ -10,7 +10,8 @@ const icons = { 'pinned_globally.enabled': 'thumb-tack', 'pinned_globally.disabled': 'thumb-tack unpinned', 'visible.enabled': 'eye', - 'visible.disabled': 'eye-slash' + 'visible.disabled': 'eye-slash', + 'split_topic': 'sign-out' }; export function actionDescription(actionCode, createdAt) { diff --git a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 index c923aaf2da..44f374ddbf 100644 --- a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 @@ -1,12 +1,15 @@ import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; +import { movePosts, mergeTopic } from 'discourse/models/topic'; // Modal related to merging of topics -export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, Presence, { +export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, { needs: ['topic'], + saving: false, + selectedTopicId: null, + topicController: Em.computed.alias('controllers.topic'), selectedPosts: Em.computed.alias('topicController.selectedPosts'), selectedReplies: Em.computed.alias('topicController.selectedReplies'), @@ -22,38 +25,40 @@ export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, P return I18n.t('topic.merge_topic.title'); }.property('saving'), - onShow: function() { + onShow() { this.set('controllers.modal.modalClass', 'split-modal'); }, actions: { - movePostsToExistingTopic: function() { + movePostsToExistingTopic() { + const topicId = this.get('model.id'); + this.set('saving', true); - var promise = null; + let promise = null; if (this.get('allPostsSelected')) { - promise = Discourse.Topic.mergeTopic(this.get('id'), this.get('selectedTopicId')); + promise = mergeTopic(topicId, this.get('selectedTopicId')); } else { - var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }), - replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }); + const postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }); + const replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }); - promise = Discourse.Topic.movePosts(this.get('id'), { + promise = movePosts(topicId, { destination_topic_id: this.get('selectedTopicId'), post_ids: postIds, reply_post_ids: replyPostIds }); } - var mergeTopicController = this; + const self = this; promise.then(function(result) { // Posts moved - mergeTopicController.send('closeModal'); - mergeTopicController.get('topicController').send('toggleMultiSelect'); + self.send('closeModal'); + self.get('topicController').send('toggleMultiSelect'); Em.run.next(function() { Discourse.URL.routeTo(result.url); }); - }, function() { - // Error moving posts - mergeTopicController.flash(I18n.t('topic.merge_topic.error')); - mergeTopicController.set('saving', false); + }).catch(function() { + self.flash(I18n.t('topic.merge_topic.error')); + }).finally(function() { + self.set('saving', false); }); return false; } diff --git a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 index 98aed2dcce..0c75ae3503 100644 --- a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 @@ -1,15 +1,20 @@ import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; +import { extractError } from 'discourse/lib/ajax-error'; +import { movePosts } from 'discourse/models/topic'; // Modal related to auto closing of topics -export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, Presence, { +export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, { needs: ['topic'], + topicName: null, + saving: false, + categoryId: null, topicController: Em.computed.alias('controllers.topic'), selectedPosts: Em.computed.alias('topicController.selectedPosts'), selectedReplies: Em.computed.alias('topicController.selectedReplies'), + allPostsSelected: Em.computed.alias('topicController.allPostsSelected'), buttonDisabled: function() { if (this.get('saving')) return true; @@ -21,7 +26,7 @@ export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, P return I18n.t('topic.split_topic.action'); }.property('saving'), - onShow: function() { + onShow() { this.setProperties({ 'controllers.modal.modalClass': 'split-modal', saving: false, @@ -31,39 +36,29 @@ export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, P }, actions: { - movePostsToNewTopic: function() { + movePostsToNewTopic() { this.set('saving', true); - var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }), - replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }), - self = this, - categoryId = this.get('categoryId'), - saveOpts = { - title: this.get('topicName'), - post_ids: postIds, - reply_post_ids: replyPostIds - }; + const postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }), + replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }), + self = this, + categoryId = this.get('categoryId'), + saveOpts = { + title: this.get('topicName'), + post_ids: postIds, + reply_post_ids: replyPostIds + }; if (!Ember.isNone(categoryId)) { saveOpts.category_id = categoryId; } - Discourse.Topic.movePosts(this.get('id'), saveOpts).then(function(result) { + movePosts(this.get('model.id'), saveOpts).then(function(result) { // Posts moved self.send('closeModal'); self.get('topicController').send('toggleMultiSelect'); - Em.run.next(function() { Discourse.URL.routeTo(result.url); }); + Ember.run.next(function() { Discourse.URL.routeTo(result.url); }); }).catch(function(xhr) { - - var error = I18n.t('topic.split_topic.error'); - - if (xhr) { - var json = xhr.responseJSON; - if (json && json.errors) { - error = json.errors[0]; - } - } - - // Error moving posts - self.flash(error); + self.flash(extractError(xhr, I18n.t('topic.split_topic.error'))); + }).finally(function() { self.set('saving', false); }); return false; diff --git a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 index 04d6d0dbc7..bb2a915727 100644 --- a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 +++ b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 @@ -1,4 +1,4 @@ -function extractError(error) { +export function extractError(error, defaultMessage) { if (error instanceof Error) { Ember.Logger.error(error.stack); } @@ -42,7 +42,7 @@ function extractError(error) { } } - return parsedError || I18n.t('generic_error'); + return parsedError || defaultMessage || I18n.t('generic_error'); } export function throwAjaxError(undoCallback) { diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 0f06dc4847..2455087411 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -1,3 +1,4 @@ +import { flushMap } from 'discourse/models/store'; import RestModel from 'discourse/models/rest'; const Topic = RestModel.extend({ @@ -462,28 +463,6 @@ Topic.reopenClass({ return Discourse.ajax(url + ".json", {data: data}); }, - mergeTopic(topicId, destinationTopicId) { - const promise = Discourse.ajax("/t/" + topicId + "/merge-topic", { - type: 'POST', - data: {destination_topic_id: destinationTopicId} - }).then(function (result) { - if (result.success) return result; - promise.reject(new Error("error merging topic")); - }); - return promise; - }, - - movePosts(topicId, opts) { - const promise = Discourse.ajax("/t/" + topicId + "/move-posts", { - type: 'POST', - data: opts - }).then(function (result) { - if (result.success) return result; - promise.reject(new Error("error moving posts topic")); - }); - return promise; - }, - changeOwners(topicId, opts) { const promise = Discourse.ajax("/t/" + topicId + "/change-owner", { type: 'POST', @@ -523,4 +502,24 @@ Topic.reopenClass({ } }); +function moveResult(result) { + if (result.success) { + // We should be hesitant to flush the map but moving ids is one rare case + flushMap(); + return result; + } + throw "error moving posts topic"; +} + +export function movePosts(topicId, data) { + return Discourse.ajax("/t/" + topicId + "/move-posts", { type: 'POST', data }).then(moveResult); +} + +export function mergeTopic(topicId, destinationTopicId) { + return Discourse.ajax("/t/" + topicId + "/merge-topic", { + type: 'POST', + data: {destination_topic_id: destinationTopicId} + }).then(moveResult); +} + export default Topic; diff --git a/app/assets/javascripts/discourse/templates/modal/merge-topic.hbs b/app/assets/javascripts/discourse/templates/modal/merge-topic.hbs index a05ac14f22..e4bf364fc2 100644 --- a/app/assets/javascripts/discourse/templates/modal/merge-topic.hbs +++ b/app/assets/javascripts/discourse/templates/modal/merge-topic.hbs @@ -1,10 +1,4 @@ diff --git a/app/assets/javascripts/discourse/templates/modal/split_topic.hbs b/app/assets/javascripts/discourse/templates/modal/split-topic.hbs similarity index 59% rename from app/assets/javascripts/discourse/templates/modal/split_topic.hbs rename to app/assets/javascripts/discourse/templates/modal/split-topic.hbs index ff72aab89c..fe80f88174 100644 --- a/app/assets/javascripts/discourse/templates/modal/split_topic.hbs +++ b/app/assets/javascripts/discourse/templates/modal/split-topic.hbs @@ -1,10 +1,4 @@ diff --git a/app/assets/javascripts/discourse/views/split-topic.js.es6 b/app/assets/javascripts/discourse/views/split-topic.js.es6 index 5ee0d76554..9291495adc 100644 --- a/app/assets/javascripts/discourse/views/split-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/split-topic.js.es6 @@ -2,6 +2,6 @@ import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalBodyView from "discourse/views/modal-body"; export default ModalBodyView.extend(SelectedPostsCount, { - templateName: 'modal/split_topic', + templateName: 'modal/split-topic', title: I18n.t('topic.split_topic.title') }); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 668234d4fa..8338d7921f 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -28,6 +28,8 @@ //= require_tree ./discourse/adapters //= require ./discourse/models/rest //= require ./discourse/models/model +//= require ./discourse/models/result-set +//= require ./discourse/models/store //= require ./discourse/models/post-action-type //= require ./discourse/models/action-summary //= require ./discourse/models/post diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index 77111f6151..272eb1c3f4 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -123,11 +123,15 @@ class PostMover end def create_moderator_post_in_original_topic + move_type_str = PostMover.move_types[@move_type].to_s + original_topic.add_moderator_post( user, - I18n.t("move_posts.#{PostMover.move_types[@move_type]}_moderator_post", + I18n.t("move_posts.#{move_type_str}_moderator_post", count: post_ids.count, - topic_link: "[#{destination_topic.title}](#{destination_topic.url})"), + topic_link: "[#{destination_topic.title}](#{destination_topic.relative_url})"), + post_type: Post.types[:small_action], + action_code: "split_topic", post_number: @first_post_number_moved ) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f6034fac9a..ce2a7574a2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -120,6 +120,7 @@ en: email: 'send this link in an email' action_codes: + split_topic: "split this topic" autoclosed: enabled: 'closed this topic %{when}' disabled: 'opened this topic %{when}' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 685aa84473..a390ee8a55 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1242,11 +1242,11 @@ en: move_posts: new_topic_moderator_post: - one: "I moved a post to a new topic: %{topic_link}" - other: "I moved %{count} posts to a new topic: %{topic_link}" + one: "A post was split to a new topic: %{topic_link}" + other: "%{count} posts were split to a new topic: %{topic_link}" existing_topic_moderator_post: - one: "I moved a post to an existing topic: %{topic_link}" - other: "I moved %{count} posts to an existing topic: %{topic_link}" + one: "A post was merged into an existing topic: %{topic_link}" + other: "%{count} posts were merged into an existing topic: %{topic_link}" change_owner: post_revision_text: "Ownership transferred from %{old_user} to %{new_user}" From 4f60344a9f61656aee6edf0b11c51064628ea0b3 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Jul 2015 14:46:16 -0700 Subject: [PATCH 20/83] copyedit on avatar reminder --- config/locales/client.en.yml | 2 +- config/locales/server.en.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ce2a7574a2..0c57a88f20 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -504,7 +504,7 @@ en: upload_title: "Upload your picture" upload_picture: "Upload Picture" image_is_not_a_square: "Warning: we've cropped your image; width and height were not equal." - cache_notice: "You've successfully changed your avatar but it might take some time to appear due to browser caching." + cache_notice: "You've successfully changed your profile picture but it might take some time to appear due to browser caching." change_profile_background: title: "Profile Background" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index a390ee8a55..d782ca94db 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -224,13 +224,13 @@ en: For more, [see our community guidelines](/guidelines). This panel will only appear for your first %{education_posts_text}. avatar: | - ### How about a new picture for your account? + ### How about a picture for your account? - You've posted a few topics and replies, but your avatar isn't as unique as you are -- it's the same default avatar all new users have. + You've posted a few topics and replies, but your profile picture isn't as unique as you are -- it's just a letter. - Have you considered **[visiting your user profile](%{profile_path})** and uploading a custom image that represents you? + Have you considered **[visiting your user profile](%{profile_path})** and uploading a picture that represents you? - It's easier to follow community discussions and find interesting people in conversations when everyone has a unique avatar! + It's easier to follow discussions and find interesting people in conversations when everyone has a unique profile picture! sequential_replies: | ### Consider replying to several posts at once @@ -1108,8 +1108,8 @@ en: email_editable: "Allow users to change their e-mail address after registration." logout_redirect: "Location to redirect browser to after logout EG: (http://somesite.com/logout)" - allow_uploaded_avatars: "Allow users to upload custom avatars." - allow_animated_avatars: "Allow users to use animated gif avatars. WARNING: run the avatars:refresh rake task after changing this setting." + allow_uploaded_avatars: "Allow users to upload custom profile pictures." + allow_animated_avatars: "Allow users to use animated gif profile pictures. WARNING: run the avatars:refresh rake task after changing this setting." allow_animated_thumbnails: "Generates animated thumbnails of animated gifs." default_avatars: "URLs to avatars that will be used by default for new users until they change them." automatically_download_gravatars: "Download Gravatars for users upon account creation or email change." @@ -1120,7 +1120,7 @@ en: disable_digest_emails: "Disable digest emails for all users." default_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." - detect_custom_avatars: "Whether or not to check that users have uploaded custom avatars." + detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures." max_daily_gravatar_crawls: "Maximum number of times Discourse will check Gravatar for custom avatars in a day" public_user_custom_fields: "A whitelist of custom fields for a user that can be shown publicly." staff_user_custom_fields: "A whitelist of custom fields for a user that can be shown to staff." From 9629f636123c2c5f9d353948ddf550d9f1d39bfc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 31 Jul 2015 18:48:58 -0400 Subject: [PATCH 21/83] FIX: Weird translation error. :fire:d --- app/assets/javascripts/discourse/components/small-action.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6 index 4ffa7ac63f..4457c6fb49 100644 --- a/app/assets/javascripts/discourse/components/small-action.js.es6 +++ b/app/assets/javascripts/discourse/components/small-action.js.es6 @@ -17,7 +17,7 @@ const icons = { export function actionDescription(actionCode, createdAt) { return function() { const ac = this.get(actionCode); - if (actionCode) { + if (ac) { const dt = new Date(this.get(createdAt)); const when = Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'}); return I18n.t(`action_codes.${ac}`, {when}).htmlSafe(); From 0d9899198f05224505d7aae1ce72a8ce00ea2d09 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 31 Jul 2015 19:06:19 -0400 Subject: [PATCH 22/83] Migrate old moved posts messages in English --- db/migrate/20150731225331_migrate_old_moved_posts.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 db/migrate/20150731225331_migrate_old_moved_posts.rb diff --git a/db/migrate/20150731225331_migrate_old_moved_posts.rb b/db/migrate/20150731225331_migrate_old_moved_posts.rb new file mode 100644 index 0000000000..73add43ded --- /dev/null +++ b/db/migrate/20150731225331_migrate_old_moved_posts.rb @@ -0,0 +1,6 @@ +class MigrateOldMovedPosts < ActiveRecord::Migration + def up + execute "UPDATE posts SET post_type = 3, action_code = 'split_topic' WHERE post_type = 2 AND raw ~* '^I moved [a\\d]+ posts? to a new topic:'" + execute "UPDATE posts SET post_type = 3, action_code = 'split_topic' WHERE post_type = 2 AND raw ~* '^I moved [a\\d]+ posts? to an existing topic:'" + end +end From a7f30adb18d8dc9c4a317dd796bc815f6cc87f43 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Jul 2015 18:26:06 -0700 Subject: [PATCH 23/83] minor Emoji tab tweaks --- .../javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 b/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 index 00287b21a5..c5b731da09 100644 --- a/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 +++ b/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 @@ -11,7 +11,7 @@ var groups = [ { name: "nature", fullname: "Nature", - tabicon: "leaves", + tabicon: "evergreen_tree", icons: ["seedling", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "tulip", "cherry_blossom", "rose", "hibiscus", "sunflower", "blossom", "bouquet", "ear_of_rice", "herb", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "mushroom", "chestnut", "rat", "mouse2", "mouse", "hamster", "ox", "water_buffalo", "cow2", "cow", "tiger2", "leopard", "tiger", "rabbit2", "rabbit", "cat2", "cat", "racehorse", "horse", "ram", "sheep", "goat", "rooster", "chicken", "baby_chick", "hatching_chick", "hatched_chick", "bird", "penguin", "elephant", "dromedary_camel", "camel", "boar", "pig2", "pig", "pig_nose", "dog2", "poodle", "dog", "wolf", "bear", "koala", "panda_face", "monkey_face", "see_no_evil", "hear_no_evil", "speak_no_evil", "monkey", "dragon", "dragon_face", "crocodile", "snake", "turtle", "frog", "whale2", "whale", "dolphin", "octopus", "fish", "tropical_fish", "blowfish", "shell", "snail", "bug", "ant", "bee", "beetle", "feet", "zap", "fire", "crescent_moon", "sunny", "partly_sunny", "cloud", "droplet", "sweat_drops", "umbrella", "dash", "snowflake", "star2", "star", "stars", "sunrise_over_mountains", "sunrise", "rainbow", "ocean", "volcano", "milky_way", "mount_fuji", "japan", "globe_with_meridians", "earth_africa", "earth_americas", "earth_asia", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "new_moon_with_face", "full_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "sun_with_face"] }, { @@ -144,7 +144,7 @@ var toolbar = function(selected){ var icon = g.tabicon; var title = g.fullname; if (g.name === "recent") { - icon = "star2"; + icon = "star"; title = "Recent"; } else if (g.name === "ungrouped") { icon = g.icons[0]; From 2fd4115fd9408d6b75fb3aa71570ed2df09a2653 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Sat, 1 Aug 2015 12:00:47 +0800 Subject: [PATCH 24/83] UX: Social login buttons alignment off on mobile. --- app/assets/stylesheets/common/components/buttons.css.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/components/buttons.css.scss b/app/assets/stylesheets/common/components/buttons.css.scss index 0c05fd1982..2821356313 100644 --- a/app/assets/stylesheets/common/components/buttons.css.scss +++ b/app/assets/stylesheets/common/components/buttons.css.scss @@ -128,8 +128,7 @@ &:before { margin-right: 9px; font-family: FontAwesome; - line-height: 1.6em; - font-size: 1.3em; + font-size: 17px; } &.google, &.google_oauth2 { background: $google; From 1f1d30bb7e871aebeeec3e63b9094a9d061939e0 Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Sat, 1 Aug 2015 15:00:39 -0700 Subject: [PATCH 25/83] Make it work --- .../stylesheets/common/admin/admin_base.scss | 87 +++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 8f2a24a2dd..ed98c2f1f0 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -3,6 +3,24 @@ @import "common/foundation/mixins"; @import "common/foundation/helpers"; +$mobile-breakpoint: 700px; + +// Change the box model for .admin-content +@media (max-width: $mobile-breakpoint) { + .admin-content { + box-sizing: border-box; + *, *:before, *:after { + box-sizing: inherit; + } + + input[type="text"] { + // Desktop/_discourse.scss sets a height on text-input elements. Using `box-sizing: border-box` + // this value either needs to be increased or set to auto. `mobile.css` seems to not set a height on text-inputs. + height: auto; + } + } +} + .admin-contents table { width: 100%; tr {text-align: left;} @@ -32,7 +50,7 @@ td.flaggers td { .admin-content { margin-bottom: 50px; .admin-contents { - padding: 8px; + padding: 8px 0; @include clearfix(); } @@ -96,6 +114,10 @@ td.flaggers td { margin-top: 20px; } +.admin-container .controls { + @include clearfix; +} + .admin-title { height: 45px; } @@ -103,7 +125,7 @@ td.flaggers td { .admin-controls { background-color: dark-light-diff($primary, $secondary, 90%, -75%); padding: 10px 10px 3px 0; - height: 35px; + @include clearfix; .nav.nav-pills { li.active { a { @@ -147,6 +169,14 @@ td.flaggers td { label { margin-top: 5px; } + .controls { + margin-left: 0; + } + // Hide the search text-input for very small screens + // Todo: find somewhere to display it - probably requires switching its order in the html + @media (max-width: 450px) { + display: none; + } } .toggle { margin-top: 8px; @@ -184,6 +214,9 @@ td.flaggers td { .admin-nav { width: 18.018%; + @media (max-width: $mobile-breakpoint) { + width: 33%; + } margin-top: 30px; .nav-stacked { border-right: none; @@ -196,10 +229,16 @@ td.flaggers td { .admin-detail { width: 76.5765%; + @media (max-width: $mobile-breakpoint) { + width: 67%; + } min-height: 800px; margin-left: 0; border-left: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); padding: 30px 0 30px 30px; + @media (max-width: $mobile-breakpoint) { + padding: 30px 0 30px 16px; + } } .settings { @@ -210,13 +249,27 @@ td.flaggers td { float: left; width: 17.6576%; margin-right: 12px; + @media (max-width: $mobile-breakpoint) { + float: none; + margin-right: 0; + width: 100%; + h3 { + margin-bottom: 6px; + } + } } .setting-value { float: left; width: 53%; - .select2-container { + @media (max-width: $mobile-breakpoint) { width: 100%; } + .select2-container { + width: 100% !important; // Needs !important to override hard-coded value + @media (max-width: $mobile-breakpoint) { + width: 100% !important; // !important overrides hard-coded mobile width of 68px + } + } .select2-container-multi .select2-choices { border: none; } @@ -227,10 +280,15 @@ td.flaggers td { .input-setting-string { width: 404px; @include medium-width { width: 314px; } - @include small-width { width: 284px; } + @media (max-width: $mobile-breakpoint) { + width: 100%; + } } .input-setting-list { width: 408px; + @media (max-width: $mobile-breakpoint) { + width: 100%; + } padding: 1px; background-color: $secondary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); @@ -255,7 +313,7 @@ td.flaggers td { border-radius: 3px; background-clip: padding-box; -moz-user-select: none; - background-color: none; + background-color: transparent; width: 3em; height: 1em; } @@ -553,6 +611,8 @@ section.details { .style-name { width: 350px; height: 25px; + // Remove height to for `box-sizing: border-box` + height: auto; } .ace-wrapper { position: relative; @@ -1146,6 +1206,7 @@ table.api-keys { .staff-actions { width: 100%; + min-width: 990px; .action { width: 10.810%; } @@ -1535,3 +1596,19 @@ table#user-badges { .permalink-title { margin-bottom: 10px; } + +// Mobile specific styles +// Mobile view text-inputs need some paddin +.mobile-view .admin-contents { + input[type="text"] { + padding: 4px; + } +} + +.mobile-view .admin-controls { + padding: 10px 10px 9px 0; +} + +.mobile-view .full-width { + margin: 0; +} \ No newline at end of file From 78edc465d56247ca9d727dcb9fe351415043139b Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Sat, 1 Aug 2015 16:57:41 -0700 Subject: [PATCH 26/83] Fix comment --- app/assets/stylesheets/common/admin/admin_base.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index ed98c2f1f0..30a83703ca 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -172,7 +172,7 @@ td.flaggers td { .controls { margin-left: 0; } - // Hide the search text-input for very small screens + // Hide the search checkbox for very small screens // Todo: find somewhere to display it - probably requires switching its order in the html @media (max-width: 450px) { display: none; @@ -1598,7 +1598,7 @@ table#user-badges { } // Mobile specific styles -// Mobile view text-inputs need some paddin +// Mobile view text-inputs need some padding .mobile-view .admin-contents { input[type="text"] { padding: 4px; From d9b0877616e6112783ad7205ad4a2714427670c9 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 2 Aug 2015 15:26:12 -0700 Subject: [PATCH 27/83] increase new user topic throttles for anti-bamwar --- config/site_settings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 204b00bddc..9195ae7187 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -688,7 +688,7 @@ rate_limits: default: 5 rate_limit_create_topic: 15 rate_limit_create_post: 5 - rate_limit_new_user_create_topic: 60 + rate_limit_new_user_create_topic: 120 rate_limit_new_user_create_post: 30 max_topics_per_day: 20 max_private_messages_per_day: 20 @@ -698,7 +698,7 @@ rate_limits: max_edits_per_day: 30 max_invites_per_day: 10 max_topic_invitations_per_day: 30 - max_topics_in_first_day: 5 + max_topics_in_first_day: 3 max_replies_in_first_day: 10 tl2_additional_likes_per_day_multiplier: 1.5 tl3_additional_likes_per_day_multiplier: 2 From 58af5797225c85e7a26f35cb78664a089cc1a2cb Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 2 Aug 2015 15:38:06 -0700 Subject: [PATCH 28/83] add shape hints to logo descriptions --- config/locales/server.bs_BA.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index d8279ca9fc..ae05accb88 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -502,11 +502,11 @@ bs_BA: post_excerpt_maxlength: "Maximum length of a post excerpt / summary." post_onebox_maxlength: "Maximum length of a oneboxed Discourse post in characters." onebox_domains_whitelist: "A list of domains to allow oneboxing for; these domains should support OpenGraph or oEmbed. Test them at http://iframely.com/debug" - logo_url: "The logo image at the top left of your site eg: http://example.com/logo.png" - digest_logo_url: "The alternate logo used at the top of your site's email digest. If left blank `logo_url` will be used. eg: http://example.com/logo.png" - logo_small_url: "The small logo image at the top left of your site, seen when scrolling down. eg: http://example.com/logo-small.png" + logo_url: "The logo image at the top left of your site, should be a wide rectangle shape." + digest_logo_url: "The alternate logo image used at the top of your site's email digest. Should be a wide rectangle shape. If left blank `logo_url` will be used." + logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down." favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon" - mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" + mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. Should be a square shape. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive." email_custom_headers: "A pipe-delimited list of custom email headers" From 5d406959082e219b5914b796017e0708c8e4cf03 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 2 Aug 2015 15:42:25 -0700 Subject: [PATCH 29/83] improved copy for logo help and put it in the correct translation this time... --- config/locales/server.en.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d782ca94db..7d59dc3189 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -797,11 +797,11 @@ en: post_onebox_maxlength: "Maximum length of a oneboxed Discourse post in characters." onebox_domains_whitelist: "A list of domains to allow oneboxing for; these domains should support OpenGraph or oEmbed. Test them at http://iframely.com/debug" - logo_url: "The logo image at the top left of your site; if left blank, the site title text will be shown." - digest_logo_url: "The alternate logo used at the top of your site's email digest. If left blank `logo_url` will be used. eg: http://example.com/logo.png" - logo_small_url: "The small logo image at the top left of your site, seen when scrolling down. If left blank, a home glyph will be shown." + logo_url: "The logo image at the top left of your site, should be a wide rectangle shape. If left blank site title text will be shown." + digest_logo_url: "The alternate logo image used at the top of your site's email digest. Should be a wide rectangle shape. If left blank `logo_url` will be used." + logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down. If left blank a home glyph will be shown." favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon" - mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. If left blank `logo_url` will be used." + mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. Should be a square shape. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive." From 7b8b96446ecebc1568ca14b23f64f0332265d5ef Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 3 Aug 2015 14:29:04 +1000 Subject: [PATCH 30/83] FEATURE: track statistics around post creation - how long were people typing? - how long was composer open? - how many drafts were created? - correct, draft saved to go away after you continue typing store in Post.find(xyz).post_stat --- .../discourse/controllers/composer.js.es6 | 2 +- .../discourse/models/composer.js.es6 | 76 +++++++++++++++++-- .../discourse/views/composer.js.es6 | 2 + app/controllers/posts_controller.rb | 5 +- app/models/draft.rb | 6 +- app/models/post.rb | 1 + app/models/post_stat.rb | 3 + db/migrate/20150802233112_add_post_stats.rb | 16 ++++ lib/post_creator.rb | 26 +++++++ spec/components/post_creator_spec.rb | 15 ++++ spec/models/draft_spec.rb | 8 +- 11 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 app/models/post_stat.rb create mode 100644 db/migrate/20150802233112_add_post_stats.rb diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 8ad012dd28..c4f1ac21b4 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -413,7 +413,7 @@ export default Ember.ObjectController.extend(Presence, { } // we need a draft sequence for the composer to work - if (opts.draftSequence === void 0) { + if (opts.draftSequence === undefined) { return Discourse.Draft.get(opts.draftKey).then(function(data) { opts.draftSequence = data.draft_sequence; opts.draft = data.draft; diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 49838fdf72..edde5df3c6 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -22,7 +22,9 @@ const CLOSED = 'closed', topic_id: 'topic.id', is_warning: 'isWarning', archetype: 'archetypeId', - target_usernames: 'targetUsernames' + target_usernames: 'targetUsernames', + typing_duration_msecs: 'typingTime', + composer_open_duration_msecs: 'composerTime' }, _edit_topic_serializer = { @@ -52,6 +54,31 @@ const Composer = RestModel.extend({ viewOpen: Em.computed.equal('composeState', OPEN), viewDraft: Em.computed.equal('composeState', DRAFT), + composeStateChanged: function() { + var oldOpen = this.get('composerOpened'); + + if (this.get('composeState') === OPEN) { + this.set('composerOpened', oldOpen || new Date()); + } else { + if (oldOpen) { + var oldTotal = this.get('composerTotalOpened') || 0; + this.set('composerTotalOpened', oldTotal + (new Date() - oldOpen)); + } + this.set('composerOpened', null); + } + }.observes('composeState'), + + composerTime: function() { + var total = this.get('composerTotalOpened') || 0; + + var oldOpen = this.get('composerOpened'); + if (oldOpen) { + total += (new Date() - oldOpen); + } + + return total; + }.property().volatile(), + archetype: function() { return this.get('archetypes').findProperty('id', this.get('archetypeId')); }.property('archetypeId'), @@ -60,6 +87,12 @@ const Composer = RestModel.extend({ return this.set('metaData', Em.Object.create()); }.observes('archetype'), + // view detected user is typing + typing: _.throttle(function(){ + var typingTime = this.get("typingTime") || 0; + this.set("typingTime", typingTime + 100); + }, 100, {leading: false, trailing: true}), + editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'), canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'), canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'), @@ -349,7 +382,9 @@ const Composer = RestModel.extend({ composeState: opts.composerState || OPEN, action: opts.action, topic: opts.topic, - targetUsernames: opts.usernames + targetUsernames: opts.usernames, + composerTotalOpened: opts.composerTime, + typingTime: opts.typingTime }); if (opts.post) { @@ -420,7 +455,10 @@ const Composer = RestModel.extend({ post: null, title: null, editReason: null, - stagedPost: false + stagedPost: false, + typingTime: 0, + composerOpened: null, + composerTotalOpened: 0 }); }, @@ -502,7 +540,9 @@ const Composer = RestModel.extend({ admin: user.get('admin'), yours: true, read: true, - wiki: false + wiki: false, + typingTime: this.get('typingTime'), + composerTime: this.get('composerTime') }); this.serialize(_create_serializer, createdPost); @@ -603,13 +643,20 @@ const Composer = RestModel.extend({ postId: this.get('post.id'), archetypeId: this.get('archetypeId'), metaData: this.get('metaData'), - usernames: this.get('targetUsernames') + usernames: this.get('targetUsernames'), + composerTime: this.get('composerTime'), + typingTime: this.get('typingTime') }; this.set('draftStatus', I18n.t('composer.saving_draft_tip')); const composer = this; + if (this._clearingStatus) { + Em.run.cancel(this._clearingStatus); + this._clearingStatus = null; + } + // try to save the draft return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data) .then(function() { @@ -617,7 +664,20 @@ const Composer = RestModel.extend({ }).catch(function() { composer.set('draftStatus', I18n.t('composer.drafts_offline')); }); - } + }, + + dataChanged: function(){ + const draftStatus = this.get('draftStatus'); + const self = this; + + if (draftStatus && !this._clearingStatus) { + + this._clearingStatus = Em.run.later(this, function(){ + self.set('draftStatus', null); + self._clearingStatus = null; + }, 1000); + } + }.observes('title','reply') }); @@ -657,7 +717,9 @@ Composer.reopenClass({ metaData: draft.metaData, usernames: draft.usernames, draft: true, - composerState: DRAFT + composerState: DRAFT, + composerTime: draft.composerTime, + typingTime: draft.typingTime }); } }, diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 36a4777933..9e8b538490 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -85,6 +85,8 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { const controller = this.get('controller'); controller.checkReplyLength(); + this.get('controller.model').typing(); + const lastKeyUp = new Date(); this.set('lastKeyUp', lastKeyUp); diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 0d301b1bbb..d594d3d719 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -403,7 +403,6 @@ class PostsController < ApplicationController # Awful hack, but you can't seem to remove the `default_scope` when joining # So instead I grab the topics separately topic_ids = posts.dup.pluck(:topic_id) - secured_category_ids = guardian.secure_category_ids topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message') topics = topics.secured(guardian) @@ -422,7 +421,9 @@ class PostsController < ApplicationController :category, :target_usernames, :reply_to_post_number, - :auto_track + :auto_track, + :typing_duration_msecs, + :composer_open_duration_msecs ] # param munging for WordPress diff --git a/app/models/draft.rb b/app/models/draft.rb index 2a209fd0f1..27378ea05e 100644 --- a/app/models/draft.rb +++ b/app/models/draft.rb @@ -7,7 +7,11 @@ class Draft < ActiveRecord::Base d = find_draft(user,key) if d return if d.sequence > sequence - d.update_columns(data: data, sequence: sequence) + exec_sql("UPDATE drafts + SET data = :data, + sequence = :sequence, + revisions = revisions + 1 + WHERE id = :id", id: d.id, sequence: sequence, data: data) else Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence) end diff --git a/app/models/post.rb b/app/models/post.rb index 7dfe6c3e57..00523e187d 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -37,6 +37,7 @@ class Post < ActiveRecord::Base has_many :uploads, through: :post_uploads has_one :post_search_data + has_one :post_stat has_many :post_details diff --git a/app/models/post_stat.rb b/app/models/post_stat.rb new file mode 100644 index 0000000000..b9b97acb20 --- /dev/null +++ b/app/models/post_stat.rb @@ -0,0 +1,3 @@ +class PostStat < ActiveRecord::Base + belongs_to :post +end diff --git a/db/migrate/20150802233112_add_post_stats.rb b/db/migrate/20150802233112_add_post_stats.rb new file mode 100644 index 0000000000..0ff42664da --- /dev/null +++ b/db/migrate/20150802233112_add_post_stats.rb @@ -0,0 +1,16 @@ +class AddPostStats < ActiveRecord::Migration + def change + + add_column :drafts, :revisions, :int, null: false, default: 1 + + create_table :post_stats do |t| + t.integer :post_id + t.integer :drafts_saved + t.integer :typing_duration_msecs + t.integer :composer_open_duration_msecs + t.timestamps + end + + add_index :post_stats, [:post_id] + end +end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 1f17538618..0bd6d9e547 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -113,6 +113,7 @@ class PostCreator def create if valid? transaction do + build_post_stats create_topic save_post extract_links @@ -146,6 +147,14 @@ class PostCreator @post end + def self.track_post_stats + Rails.env != "test".freeze || @track_post_stats + end + + def self.track_post_stats=(val) + @track_post_stats = val + end + def self.create(user, opts) PostCreator.new(user, opts).create end @@ -172,6 +181,23 @@ class PostCreator protected + def build_post_stats + if PostCreator.track_post_stats + draft_key = @topic ? "topic_#{@topic.id}" : "new_topic" + + sequence = DraftSequence.current(@user, draft_key) + revisions = Draft.where(sequence: sequence, + user_id: @user.id, + draft_key: draft_key).pluck(:revisions).first || 0 + + @post.build_post_stat( + drafts_saved: revisions, + typing_duration_msecs: @opts[:typing_duration_msecs] || 0, + composer_open_duration_msecs: @opts[:composer_open_duration_msecs] || 0 + ) + end + end + def trigger_after_events(post) DiscourseEvent.trigger(:topic_created, post.topic, @opts, @user) unless @opts[:topic_id] DiscourseEvent.trigger(:post_created, post, @opts, @user) diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 6a5feef97f..4d58fb9fae 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -213,6 +213,21 @@ describe PostCreator do }.to_not change { topic.excerpt } end + it 'creates post stats' do + + Draft.set(user, 'new_topic', 0, "test") + Draft.set(user, 'new_topic', 0, "test1") + + begin + PostCreator.track_post_stats = true + post = creator.create + expect(post.post_stat.typing_duration_msecs).to eq(0) + expect(post.post_stat.drafts_saved).to eq(2) + ensure + PostCreator.track_post_stats = false + end + end + describe "topic's auto close" do it "doesn't update topic's auto close when it's not based on last post" do diff --git a/spec/models/draft_spec.rb b/spec/models/draft_spec.rb index f0c418e28b..16746ad4ee 100644 --- a/spec/models/draft_spec.rb +++ b/spec/models/draft_spec.rb @@ -109,6 +109,12 @@ describe Draft do expect(Draft.get(p.user, p.topic.draft_key, s)).to eq nil end - it 'increases the sequence number when a post is revised' + it 'increases revision each time you set' do + u = User.first + Draft.set(u, 'new_topic', 0, 'hello') + Draft.set(u, 'new_topic', 0, 'goodbye') + + expect(Draft.find_draft(u, 'new_topic').revisions).to eq(2) + end end end From 64bbf2c1c49b3a10c3e64dfe7c9211431d8251a5 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 3 Aug 2015 16:18:28 +1000 Subject: [PATCH 31/83] correct closing logic for wd importer --- script/import_scripts/lithium.rb | 49 +++++++++++++------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index c3a1b23d7f..da7b3d9053 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -62,8 +62,8 @@ class ImportScripts::Lithium < ImportScripts::Base import_likes import_accepted_answers import_pms + close_topics - # close_topics post_process_posts end @@ -638,35 +638,26 @@ class ImportScripts::Lithium < ImportScripts::Base def close_topics - return "NOT WORKING CAUSE NO WAY TO FIND OUT" - # puts "\nclosing closed topics..." - # - # sql = "select unique_id post_id from message2 where (attributes & 0x20000000 ) != 0;" - # results = mysql_query(sql) - # - # # loading post map - # existing_map = {} - # PostCustomField.where(name: 'import_unique_id').pluck(:post_id, :value).each do |post_id, import_id| - # existing_map[import_id] = post_id - # end - # - # puts "loading data into temp table" - # PostAction.transaction do - # results.each do |result| - # - # - # p existing_map[result["post_id"].to_s] - # - # end - # end - # - # exit - # - # puts "\nfreezing frozen topics..." - # - # sql = "select unique_id post_id from message2 where (attributes & 0x2000000 ) != 0;" - # results = mysql_query(sql) + puts "\nclosing closed topics..." + + sql = "select unique_id post_id from message2 where root_id = id AND (attributes & 0x0002 ) != 0;" + results = mysql_query(sql) + + # loading post map + existing_map = {} + PostCustomField.where(name: 'import_unique_id').pluck(:post_id, :value).each do |post_id, import_id| + existing_map[import_id.to_i] = post_id.to_i + end + + results.map{|r| r["post_id"]}.each_slice(500) do |ids| + mapped = ids.map{|id| existing_map[id]}.compact + Topic.exec_sql(" + UPDATE topics SET closed = true + WHERE id IN (SELECT topic_id FROM posts where id in (:ids)) + ", ids: mapped) if mapped.present? + end + end From fd82107df8cef24c3492fa538f56b6242093587d Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 3 Aug 2015 17:16:19 +1000 Subject: [PATCH 32/83] correct bugs in lithium importer --- script/import_scripts/lithium.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index da7b3d9053..030ebc4677 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -737,7 +737,7 @@ SQL current = 0 max = Post.count - Post.where(topic_id: 164).find_each do |post| + Post.all.find_each do |post| begin new_raw = postprocess_post_raw(post.raw, post.user_id) post.raw = new_raw @@ -761,7 +761,7 @@ SQL uri.hostname = nil end - if !uri.hostname + if uri && !uri.hostname if l["href"] l["href"] = uri.path # we have an internal link, lets see if we can remap it? From a1f02d4baa41b0cd5404faa63bec12bb01f87dbd Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 3 Aug 2015 17:35:35 +1000 Subject: [PATCH 33/83] correct logic, add missing permalink creator --- script/import_scripts/lithium.rb | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index 030ebc4677..8f12331f82 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -63,6 +63,7 @@ class ImportScripts::Lithium < ImportScripts::Base import_accepted_answers import_pms close_topics + create_permalinks post_process_posts end @@ -728,9 +729,6 @@ SQL return nil end - def to_markdown(html) - end - def post_process_posts puts "", "Postprocessing posts..." @@ -773,15 +771,16 @@ SQL # we need an upload here upload_name = $1 if uri.path =~ /image-id\/([^\/]+)/ + if upload_name + png = UPLOAD_DIR + "/" + upload_name + ".png" + jpg = UPLOAD_DIR + "/" + upload_name + ".jpg" - png = UPLOAD_DIR + "/" + upload_name + ".png" - jpg = UPLOAD_DIR + "/" + upload_name + ".jpg" - - # check to see if we have it - if File.exist?(png) - image = png - elsif File.exists?(jpg) - image = jpg + # check to see if we have it + if File.exist?(png) + image = png + elsif File.exists?(jpg) + image = jpg + end end if image From 02a38eebbb2886074eb867851f101fffd4f0f8e8 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 3 Aug 2015 18:30:26 +1000 Subject: [PATCH 34/83] correct logic in importer --- script/import_scripts/lithium.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index 8f12331f82..4d7a84366c 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -763,7 +763,7 @@ SQL if l["href"] l["href"] = uri.path # we have an internal link, lets see if we can remap it? - permalink = Permalink.find_by_url(uri.path) + permalink = Permalink.find_by_url(uri.path) rescue nil if l["href"] && permalink && permalink.target_url l["href"] = permalink.target_url end @@ -774,12 +774,15 @@ SQL if upload_name png = UPLOAD_DIR + "/" + upload_name + ".png" jpg = UPLOAD_DIR + "/" + upload_name + ".jpg" + gif = UPLOAD_DIR + "/" + upload_name + ".gif" # check to see if we have it if File.exist?(png) image = png elsif File.exists?(jpg) image = jpg + elsif File.exists?(gif) + image = gif end end From e83b0619e8188e23d636a30cf57ee98c0ee4557e Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 3 Aug 2015 14:37:31 -0700 Subject: [PATCH 35/83] switch to refresh icon on resend invite --- .../javascripts/discourse/templates/user-invited-show.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index 29c5f62bf4..32ad817159 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -76,7 +76,7 @@ {{#if invite.reinvited}} {{i18n 'user.invited.reinvited'}} {{else}} - {{d-button icon="user-plus" action="reinvite" actionParam=invite class="btn" label="user.invited.reinvite"}} + {{d-button icon="refresh" action="reinvite" actionParam=invite class="btn" label="user.invited.reinvite"}} {{/if}} {{/if}} From 7d9ee9b3781485edea8d3b8d2aa107af7ec02b51 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 3 Aug 2015 15:38:32 -0700 Subject: [PATCH 36/83] make invite list styles match topic list --- app/assets/stylesheets/desktop/user.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 4ccd4f4d1e..027beaa530 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -168,12 +168,14 @@ th { text-align: left; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - padding: 5px; + border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -60%); + padding: 0 0 10px 0; + color: scale-color($primary, $lightness: 50%); + font-weight: normal; } td { - padding: 5px; + padding: 10px 0 10px 0; border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); } } From 2d7ba13223c7512470a5e993066b31d30d362c21 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 3 Aug 2015 16:16:46 -0700 Subject: [PATCH 37/83] full page search CSS tweaks --- app/assets/stylesheets/common/base/search.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index 33b5263269..fd459b2e62 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -10,7 +10,7 @@ a { color: scale-color($primary, $lightness: 10%); } - line-height: 20px; + padding-bottom: 2px; } .avatar { position: relative; @@ -24,7 +24,7 @@ } .blurb { font-size: 1.0em; - line-height: 24px; + line-height: 20px; word-wrap: break-word; clear: both; color: scale-color($primary, $lightness: 20%); From dc27ae3bf57ade6bcab1a963d97c6200b6f42815 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 3 Aug 2015 17:30:18 -0700 Subject: [PATCH 38/83] make search blurbs more grey to match google --- app/assets/stylesheets/common/base/search.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index fd459b2e62..edd52f26e9 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -27,9 +27,9 @@ line-height: 20px; word-wrap: break-word; clear: both; - color: scale-color($primary, $lightness: 20%); + color: scale-color($primary, $lightness: 40%); .date { - color: scale-color($primary, $lightness: 40%); + color: scale-color($primary, $lightness: 60%); } .search-highlight { From 3d7a2b4788110c7042b7018aa0ce0049a6c5b7e4 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 3 Aug 2015 17:34:06 -0700 Subject: [PATCH 39/83] use default link color on search page results --- app/assets/stylesheets/common/base/search.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index edd52f26e9..93e8d0e9a3 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -7,9 +7,6 @@ margin-bottom: 28px; max-width: 675px; .topic { - a { - color: scale-color($primary, $lightness: 10%); - } padding-bottom: 2px; } .avatar { From a2533e2a02ff4a528948aafb76a3cb1521f63195 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Aug 2015 10:15:10 +1000 Subject: [PATCH 40/83] lighten search blurb for full page search --- app/assets/stylesheets/common/base/search.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index 93e8d0e9a3..e0b3e48339 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -7,7 +7,7 @@ margin-bottom: 28px; max-width: 675px; .topic { - padding-bottom: 2px; + padding-bottom: 2px; } .avatar { position: relative; From 01ad88f1ed5869b0d41311f63269df825a01c588 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Aug 2015 10:55:59 +1000 Subject: [PATCH 41/83] FEATURE: min_first_post_typing_time If a user spends less than 3 seconds typing first post they will automatically enter the approval queue --- app/controllers/posts_controller.rb | 4 +++- config/locales/server.en.yml | 1 + config/site_settings.yml | 1 + lib/new_post_manager.rb | 14 +++++++++++--- spec/controllers/posts_controller_spec.rb | 17 +++++++++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index d594d3d719..f6f34fc7ed 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -89,6 +89,8 @@ class PostsController < ApplicationController def create @manager_params = create_params + @manager_params[:first_post_checks] = !is_api? + manager = NewPostManager.new(current_user, @manager_params) if is_api? @@ -353,7 +355,7 @@ class PostsController < ApplicationController # If a param is present it uses that result structure. def backwards_compatible_json(json_obj, success) json_obj.symbolize_keys! - if params[:nested_post].blank? && json_obj[:errors].blank? + if params[:nested_post].blank? && json_obj[:errors].blank? && json_obj[:action] != :enqueued json_obj = json_obj[:post] end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7d59dc3189..232e939ae2 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1077,6 +1077,7 @@ en: num_flags_to_close_topic: "Minimum number of active flags that is required to automatically pause a topic for intervention" auto_respond_to_flag_actions: "Enable automatic reply when disposing a flag." + min_first_post_typing_time: "Minimum amount of time in milliseconds a user must type during first post, if threshold is not met post will automatically enter the needs approval queue. Set to 0 to disable (not recommended)" reply_by_email_enabled: "Enable replying to topics via email." reply_by_email_address: "Template for reply by email incoming email address, for example: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" diff --git a/config/site_settings.yml b/config/site_settings.yml index 9195ae7187..9a57517787 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -680,6 +680,7 @@ spam: num_flaggers_to_close_topic: 5 num_flags_to_close_topic: 12 auto_respond_to_flag_actions: true + min_first_post_typing_time: 3000 rate_limits: unique_posts_mins: diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index 7c17135d02..472f5c8bc2 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -28,15 +28,23 @@ class NewPostManager @sorted_handlers.sort_by! {|h| -h[:priority]} end - def self.user_needs_approval?(user) + def self.user_needs_approval?(manager) + user = manager.user + args = manager.args + return false if user.staff? (user.post_count < SiteSetting.approve_post_count) || - (user.trust_level < SiteSetting.approve_unless_trust_level.to_i) + (user.trust_level < SiteSetting.approve_unless_trust_level.to_i) || + ( + args[:first_post_checks] && + user.post_count == 0 && + args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time + ) end def self.default_handler(manager) - manager.enqueue('default') if user_needs_approval?(manager.user) + manager.enqueue('default') if user_needs_approval?(manager) end def self.queue_enabled? diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index ce2948fd95..a9164e9fba 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -446,6 +446,10 @@ describe PostsController do describe 'creating a post' do + before do + SiteSetting.min_first_post_typing_time = 0 + end + include_examples 'action requires login', :post, :create context 'api' do @@ -477,6 +481,19 @@ describe PostsController do expect { xhr :post, :create }.to raise_error(ActionController::ParameterMissing) end + it 'queues the post if min_first_post_typing_time is not met' do + + SiteSetting.min_first_post_typing_time = 3000 + + xhr :post, :create, {raw: 'this is the test content', title: 'this is the test title for the topic'} + + expect(response).to be_success + parsed = ::JSON.parse(response.body) + + expect(parsed["action"]).to eq("enqueued") + + end + it 'creates the post' do xhr :post, :create, {raw: 'this is the test content', title: 'this is the test title for the topic'} From 2ad2ab503fb599212ad3e5604da742bc909c053b Mon Sep 17 00:00:00 2001 From: awesomerobot Date: Mon, 3 Aug 2015 21:03:11 -0400 Subject: [PATCH 42/83] aligning text-logos and header with flexbox --- app/assets/stylesheets/common/base/header.scss | 10 +++++++++- app/assets/stylesheets/common/base/topic-post.scss | 12 ++++++++---- app/assets/stylesheets/common/base/topic.scss | 7 ++++--- .../stylesheets/common/components/badges.css.scss | 4 ++++ app/assets/stylesheets/desktop/header.scss | 2 +- app/assets/stylesheets/mobile/header.scss | 1 - 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index ed398f4a06..b12f28f9a8 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -13,6 +13,11 @@ .contents { margin: 8px 0; + display: -ms-flexbox; + display: flex; + + -ms-flex-align: center; + align-items: center; } .title { @@ -34,8 +39,11 @@ } .panel { - float: right; position: relative; + margin-left: auto; + + -ms-flex-order: 3; + order: 3; } .login-button, button.sign-up-button { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index ec590ae39c..4ef0c4dba0 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -261,6 +261,12 @@ table.md-table { } .small-action { + display: -ms-flexbox; + display: flex; + + -ms-flex-align: center; + align-items: center; + .topic-avatar { padding: 5px 0; border-top: none; @@ -274,8 +280,7 @@ table.md-table { } .small-action-desc { - padding: 0.5em 0 0.5em 4em; - margin-top: 5px; + padding: 0 1.5%; text-transform: uppercase; font-weight: bold; font-size: 0.9em; @@ -287,7 +292,7 @@ table.md-table { font-weight: normal; font-size: 14px; p { - margin: 5px 0; + margin: 0; } } @@ -298,7 +303,6 @@ table.md-table { > p { margin: 0; - padding-top: 4px; } } diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index 18701110bc..a0e036ed04 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -27,11 +27,12 @@ } .extra-info-wrapper { + -ms-flex-order: 2; + order: 2; + + line-height: 1.5; .badge-wrapper { float: left; - &.bullet { - margin-top: 5px; - } } } diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 8008d48b21..caa751af5e 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -52,8 +52,12 @@ &.bullet { //bullet category style + display: -ms-inline-flexbox; display: inline-flex; + + -ms-flex-align: center; align-items: baseline; + margin-right: 10px; span.badge-category { diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index 3caae9f485..66fddf863c 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -8,7 +8,7 @@ padding-top: 3px; height: 60px; .fa-home { - padding:8px; + padding: 0 8px 0 0; font-size: 2.1em; } } diff --git a/app/assets/stylesheets/mobile/header.scss b/app/assets/stylesheets/mobile/header.scss index 08bb4dfb17..e3dd106200 100644 --- a/app/assets/stylesheets/mobile/header.scss +++ b/app/assets/stylesheets/mobile/header.scss @@ -11,7 +11,6 @@ // some protection for text-only site titles .title { max-width: 130px; - height: 39px; overflow: hidden; padding: 0; text-overflow: clip; From 6fdd53e3d6393401bbb2b4995b087b7fd2bf4fdc Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Aug 2015 12:06:07 +1000 Subject: [PATCH 43/83] FEATURE: auto block fast typers if tl0 enter text too fast they get automatically blocked, configurable --- config/locales/server.en.yml | 2 ++ config/site_settings.yml | 2 ++ lib/new_post_manager.rb | 32 ++++++++++++++++++----- spec/controllers/posts_controller_spec.rb | 2 ++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 232e939ae2..ec44654b8e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1078,6 +1078,8 @@ en: auto_respond_to_flag_actions: "Enable automatic reply when disposing a flag." min_first_post_typing_time: "Minimum amount of time in milliseconds a user must type during first post, if threshold is not met post will automatically enter the needs approval queue. Set to 0 to disable (not recommended)" + auto_block_fast_typers_on_first_post: "Automatically block users that do not meet min_first_post_typing_time" + auto_block_fast_typers_max_trust_level: "Maximum trust level to auto block fast typers" reply_by_email_enabled: "Enable replying to topics via email." reply_by_email_address: "Template for reply by email incoming email address, for example: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" diff --git a/config/site_settings.yml b/config/site_settings.yml index 9a57517787..9c41cddee1 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -681,6 +681,8 @@ spam: num_flags_to_close_topic: 12 auto_respond_to_flag_actions: true min_first_post_typing_time: 3000 + auto_block_fast_typers_on_first_post: true + auto_block_fast_typers_max_trust_level: 0 rate_limits: unique_posts_mins: diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index 472f5c8bc2..a2ae5217c5 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -28,23 +28,41 @@ class NewPostManager @sorted_handlers.sort_by! {|h| -h[:priority]} end - def self.user_needs_approval?(manager) + def self.is_fast_typer?(manager) user = manager.user args = manager.args + args[:first_post_checks] && + user.post_count == 0 && + args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time + end + + def self.user_needs_approval?(manager) + user = manager.user + return false if user.staff? (user.post_count < SiteSetting.approve_post_count) || (user.trust_level < SiteSetting.approve_unless_trust_level.to_i) || - ( - args[:first_post_checks] && - user.post_count == 0 && - args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time - ) + is_fast_typer?(manager) end def self.default_handler(manager) - manager.enqueue('default') if user_needs_approval?(manager) + if user_needs_approval?(manager) + + result = manager.enqueue('default') + + if is_fast_typer?(manager) && + SiteSetting.auto_block_fast_typers_on_first_post && + SiteSetting.auto_block_fast_typers_max_trust_level <= manager.user.trust_level + + manager.user.update_columns(blocked: true) + + end + + result + + end end def self.queue_enabled? diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index a9164e9fba..183045d427 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -492,6 +492,8 @@ describe PostsController do expect(parsed["action"]).to eq("enqueued") + expect(user.blocked).to eq(true) + end it 'creates the post' do From 3c8ae643b2cd10a9da6022e3d5c34e8988408601 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Aug 2015 12:56:20 +1000 Subject: [PATCH 44/83] UX: improve handling of users in queued-posts - Display an icon on already blocked users - Automatically unblock users that you approve --- .../javascripts/discourse/templates/queued-posts.hbs | 4 +++- app/models/queued_post.rb | 4 ++++ app/serializers/queued_post_serializer.rb | 2 +- config/locales/client.en.yml | 1 + spec/controllers/posts_controller_spec.rb | 9 +++++++++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/queued-posts.hbs b/app/assets/javascripts/discourse/templates/queued-posts.hbs index 400937caf5..5fc07e042b 100644 --- a/app/assets/javascripts/discourse/templates/queued-posts.hbs +++ b/app/assets/javascripts/discourse/templates/queued-posts.hbs @@ -6,7 +6,6 @@ {{#user-link user=ctrl.post.user}} {{avatar ctrl.post.user imageSize="large"}} {{/user-link}} -
@@ -14,6 +13,9 @@ {{#user-link user=ctrl.post.user}} {{ctrl.post.user.username}} {{/user-link}} + {{#if ctrl.post.user.blocked}} + + {{/if}}