diff --git a/.jshintignore b/.jshintignore index b48df2c179..ac11baa97e 100644 --- a/.jshintignore +++ b/.jshintignore @@ -20,4 +20,5 @@ vendor/ test/javascripts/helpers/ test/javascripts/test_helper.js test/javascripts/test_helper.js +app/assets/javascripts/ember-addons/ diff --git a/.jshintrc b/.jshintrc index f7c6762ffa..dab59943b4 100644 --- a/.jshintrc +++ b/.jshintrc @@ -48,6 +48,7 @@ "parseHTML", "deepEqual", "notEqual", + "define", "require", "requirejs", "hasModule", diff --git a/Gemfile b/Gemfile index b6d77cf94a..3f7a2446ad 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ gem 'active_model_serializers', '~> 0.8.3' gem 'onebox' gem 'ember-rails' -gem 'ember-source', '1.9.0.beta.4' +gem 'ember-source', '1.11.3.1' gem 'handlebars-source', '2.0.0' gem 'barber' gem 'babel-transpiler' diff --git a/Gemfile.lock b/Gemfile.lock index 2cd643c0b5..fd65ba4879 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,10 +45,9 @@ GEM babel-transpiler (0.6.0) babel-source (>= 4.0, < 5) execjs (~> 2.0) - barber (0.5.0) - ember-source - execjs - handlebars-source (>= 1.0.0.rc.4) + barber (0.9.0) + ember-source (>= 1.0, < 2) + execjs (>= 1.2, < 3) better_errors (2.1.1) coderay (>= 1.0.0) erubis (>= 2.6.6) @@ -68,23 +67,23 @@ GEM docile (1.1.5) dotenv (1.0.2) email_reply_parser (0.5.8) - ember-data-source (0.14) - ember-source - ember-rails (0.14.1) + ember-data-source (1.0.0.beta.16.1) + ember-source (~> 1.8) + ember-handlebars-template (0.1.5) + barber (>= 0.9.0) + sprockets (>= 2.1, < 3.1) + ember-rails (0.18.2) active_model_serializers - barber (>= 0.4.1) - ember-data-source - ember-source - execjs (>= 1.2) - handlebars-source + ember-data-source (>= 1.0.0.beta.5) + ember-handlebars-template (>= 0.1.1, < 1.0) + ember-source (>= 1.1.0) jquery-rails (>= 1.0.17) railties (>= 3.1) - ember-source (1.9.0.beta.4) - handlebars-source (~> 2.0) + ember-source (1.11.3.1) erubis (2.7.0) eventmachine (1.0.7) excon (0.44.4) - execjs (2.4.0) + execjs (2.5.2) exifr (1.1.3) fabrication (2.9.8) fakeweb (1.3.0) @@ -220,7 +219,7 @@ GEM method_source (0.8.2) mime-types (1.25.1) mini_portile (0.6.2) - minitest (5.6.0) + minitest (5.6.1) mocha (1.1.0) metaclass (~> 0.0.1) mock_redis (0.14.0) @@ -271,7 +270,7 @@ GEM omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) - onebox (1.5.16) + onebox (1.5.18) moneta (~> 0.7) multi_json (~> 1.7) mustache (~> 0.99) @@ -295,7 +294,7 @@ GEM qunit-rails (0.0.7) railties r2 (0.2.5) - rack (1.5.2) + rack (1.5.3) rack-mini-profiler (0.9.3) rack (>= 1.1.3) rack-openid (1.3.1) @@ -467,7 +466,7 @@ DEPENDENCIES certified email_reply_parser ember-rails - ember-source (= 1.9.0.beta.4) + ember-source (= 1.11.3.1) eventmachine fabrication (= 2.9.8) fakeweb (~> 1.3.0) diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 diff --git a/Vagrantfile b/Vagrantfile index 9702bba015..b598918036 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -17,7 +17,7 @@ Vagrant.configure("2") do |config| config.vm.provider :virtualbox do |v| # This setting gives the VM 1024MB of RAM instead of the default 384. - v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 2048].max] + v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 1024].max] # Who has a single core cpu these days anyways? cpu_count = 2 diff --git a/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 b/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 index 786797f665..81486978a4 100644 --- a/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 @@ -3,8 +3,8 @@ export default Ember.ObjectController.extend({ savedIpAddress: null, isRange: function() { - return this.get("ip_address").indexOf("/") > 0; - }.property("ip_address"), + return this.get("model.ip_address").indexOf("/") > 0; + }.property("model.ip_address"), actions: { allow: function(record) { @@ -19,14 +19,14 @@ export default Ember.ObjectController.extend({ edit: function() { if (!this.get('editing')) { - this.savedIpAddress = this.get('ip_address'); + this.savedIpAddress = this.get('model.ip_address'); } this.set('editing', true); }, cancel: function() { if (this.get('savedIpAddress') && this.get('editing')) { - this.set('ip_address', this.get('savedIpAddress')); + this.set('model.ip_address', this.get('savedIpAddress')); } this.set('editing', false); }, diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js.es6 index c744f96c48..5bf47ac421 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js.es6 @@ -1,7 +1,6 @@ -import Presence from 'discourse/mixins/presence'; import { outputExportResult } from 'discourse/lib/export-result'; -export default Ember.ArrayController.extend(Presence, { +export default Ember.ArrayController.extend({ loading: false, actions: { diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 index 1bc3596973..54eda1a502 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 @@ -1,7 +1,6 @@ -import Presence from 'discourse/mixins/presence'; import { outputExportResult } from 'discourse/lib/export-result'; -export default Ember.ArrayController.extend(Presence, { +export default Ember.ArrayController.extend({ loading: false, itemController: 'admin-log-screened-ip-address', filter: null, diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js.es6 index 06839b7456..a19701b13c 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js.es6 @@ -1,7 +1,6 @@ -import Presence from 'discourse/mixins/presence'; import { outputExportResult } from 'discourse/lib/export-result'; -export default Ember.ArrayController.extend(Presence, { +export default Ember.ArrayController.extend({ loading: false, show() { diff --git a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 index e3f56d5786..c51db8204e 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 @@ -1,7 +1,6 @@ -import Presence from 'discourse/mixins/presence'; import { outputExportResult } from 'discourse/lib/export-result'; -export default Ember.ArrayController.extend(Presence, { +export default Ember.ArrayController.extend({ loading: false, filters: null, diff --git a/app/assets/javascripts/admin/models/screened_ip_address.js b/app/assets/javascripts/admin/models/screened_ip_address.js index 4aafe4df9c..f487896206 100644 --- a/app/assets/javascripts/admin/models/screened_ip_address.js +++ b/app/assets/javascripts/admin/models/screened_ip_address.js @@ -1,11 +1,6 @@ /** Represents an IP address that is watched for during account registration (and possibly other times), and an action is taken. - - @class ScreenedIpAddress - @extends Discourse.Model - @namespace Discourse - @module Discourse **/ Discourse.ScreenedIpAddress = Discourse.Model.extend({ actionName: function() { @@ -17,21 +12,9 @@ Discourse.ScreenedIpAddress = Discourse.Model.extend({ }.property('action_name'), actionIcon: function() { - if (this.get('action_name') === 'block') { - return this.get('blockIcon'); - } else { - return this.get('doNothingIcon'); - } + return (this.get('action_name') === 'block') ? 'ban' : 'check'; }.property('action_name'), - blockIcon: function() { - return 'fa-ban'; - }.property(), - - doNothingIcon: function() { - return 'fa-check'; - }.property(), - save: function() { return Discourse.ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", { type: this.id ? 'PUT' : 'POST', diff --git a/app/assets/javascripts/admin/templates/email_preview_digest.hbs b/app/assets/javascripts/admin/templates/email_preview_digest.hbs index d5f3f932b2..7670d6194b 100644 --- a/app/assets/javascripts/admin/templates/email_preview_digest.hbs +++ b/app/assets/javascripts/admin/templates/email_preview_digest.hbs @@ -18,10 +18,10 @@ -{{#loading-spinner condition=loading}} +{{#conditional-loading-spinner condition=loading}} {{#if showHtml}} {{{html_content}}} {{else}}
{{{text_content}}}
{{/if}} -{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/flags-list.hbs b/app/assets/javascripts/admin/templates/flags-list.hbs index 6285d9e938..e001e92ae8 100644 --- a/app/assets/javascripts/admin/templates/flags-list.hbs +++ b/app/assets/javascripts/admin/templates/flags-list.hbs @@ -147,7 +147,7 @@ - {{loading-spinner condition=view.loading}} + {{conditional-loading-spinner condition=view.loading}} {{else}}

{{i18n 'admin.flags.no_results'}}

{{/if}} diff --git a/app/assets/javascripts/admin/templates/logs/screened_emails.hbs b/app/assets/javascripts/admin/templates/logs/screened_emails.hbs index 506ede054d..fba64d367d 100644 --- a/app/assets/javascripts/admin/templates/logs/screened_emails.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened_emails.hbs @@ -4,7 +4,7 @@


-{{#loading-spinner condition=loading}} +{{#conditional-loading-spinner condition=loading}} {{#if model.length}}
@@ -25,4 +25,4 @@ {{else}} {{i18n 'search.no_results'}} {{/if}} -{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/logs/screened_ip_addresses.hbs b/app/assets/javascripts/admin/templates/logs/screened_ip_addresses.hbs index 27fa423711..515c22a3a0 100644 --- a/app/assets/javascripts/admin/templates/logs/screened_ip_addresses.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened_ip_addresses.hbs @@ -7,7 +7,7 @@ {{screened-ip-address-form action="recordAdded"}}
-{{#loading-spinner condition=loading}} +{{#conditional-loading-spinner condition=loading}} {{#if model.length}}
@@ -27,4 +27,4 @@ {{else}} {{i18n 'search.no_results'}} {{/if}} -{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/logs/screened_ip_addresses_list_item.hbs b/app/assets/javascripts/admin/templates/logs/screened_ip_addresses_list_item.hbs index 94bc4155e9..27fe670e49 100644 --- a/app/assets/javascripts/admin/templates/logs/screened_ip_addresses_list_item.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened_ip_addresses_list_item.hbs @@ -1,35 +1,35 @@
{{#if editing}} - {{text-field value=ip_address autofocus="autofocus"}} + {{text-field value=model.ip_address autofocus="autofocus"}} {{else}} {{#if isRange}} - {{ip_address}} + {{model.ip_address}} {{else}} - {{ip_address}} + {{model.ip_address}} {{/if}} {{/if}}
- - {{actionName}} + {{fa-icon model.actionIcon}} + {{model.actionName}}
-
{{match_count}}
+
{{model.match_count}}
- {{#if last_match_at}} - {{age-with-tooltip last_match_at}} + {{#if model.last_match_at}} + {{age-with-tooltip model.last_match_at}} {{/if}}
-
{{age-with-tooltip created_at}}
+
{{age-with-tooltip model.created_at}}
{{#unless editing}} - {{#if isBlocked}} - + {{#if model.isBlocked}} + {{else}} - + {{/if}} {{else}} diff --git a/app/assets/javascripts/admin/templates/logs/screened_urls.hbs b/app/assets/javascripts/admin/templates/logs/screened_urls.hbs index aba722a9c7..4fa38f1170 100644 --- a/app/assets/javascripts/admin/templates/logs/screened_urls.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened_urls.hbs @@ -4,7 +4,7 @@


-{{#loading-spinner condition=loading}} +{{#conditional-loading-spinner condition=loading}} {{#if model.length}}
@@ -21,4 +21,4 @@ {{else}} {{i18n 'search.no_results'}} {{/if}} -{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/logs/staff_action_logs.hbs b/app/assets/javascripts/admin/templates/logs/staff_action_logs.hbs index c5ce3aa46e..acabdbb9d7 100644 --- a/app/assets/javascripts/admin/templates/logs/staff_action_logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff_action_logs.hbs @@ -33,7 +33,7 @@

-
+
{{i18n 'admin.logs.staff_actions.instructions'}}
@@ -48,11 +48,11 @@
- {{#loading-spinner condition=loading}} + {{#conditional-loading-spinner condition=loading}} {{#if model.length}} {{view "staff-action-logs-list" content=controller}} {{else}} {{i18n 'search.no_results'}} {{/if}} - {{/loading-spinner}} + {{/conditional-loading-spinner}}
diff --git a/app/assets/javascripts/admin/templates/reports.hbs b/app/assets/javascripts/admin/templates/reports.hbs index e4fdc87cbf..0bc26a8739 100644 --- a/app/assets/javascripts/admin/templates/reports.hbs +++ b/app/assets/javascripts/admin/templates/reports.hbs @@ -20,7 +20,7 @@ {{/if}}
-{{#loading-spinner condition=refreshing}} +{{#conditional-loading-spinner condition=refreshing}} @@ -43,4 +43,4 @@ {{/each}}
{{xaxis}}
-{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/user_badges.hbs b/app/assets/javascripts/admin/templates/user_badges.hbs index 5d55a2d0b5..c6e9a86b6d 100644 --- a/app/assets/javascripts/admin/templates/user_badges.hbs +++ b/app/assets/javascripts/admin/templates/user_badges.hbs @@ -6,7 +6,7 @@
-{{#loading-spinner condition=loading}} +{{#conditional-loading-spinner condition=loading}}

{{i18n 'admin.badges.grant_badge'}}


@@ -67,4 +67,4 @@ {{/each}}
-{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/user_index.hbs b/app/assets/javascripts/admin/templates/user_index.hbs index f2a6039293..b522067b0c 100644 --- a/app/assets/javascripts/admin/templates/user_index.hbs +++ b/app/assets/javascripts/admin/templates/user_index.hbs @@ -316,7 +316,7 @@ {{/if}} -
+
{{i18n 'admin.user.blocked'}}
{{blocked}}
diff --git a/app/assets/javascripts/admin/templates/users-list-show.hbs b/app/assets/javascripts/admin/templates/users-list-show.hbs index 750685509a..2c962574f9 100644 --- a/app/assets/javascripts/admin/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/templates/users-list-show.hbs @@ -1,7 +1,7 @@ {{#if hasSelection}}
- - + +
{{/if}} @@ -19,7 +19,7 @@ {{/unless}}
-{{#loading-spinner condition=refreshing}} +{{#conditional-loading-spinner condition=refreshing}} {{#if model}} @@ -81,4 +81,4 @@ {{else}}

{{i18n 'search.no_results'}}

{{/if}} -{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/version-checks.hbs b/app/assets/javascripts/admin/templates/version-checks.hbs index 5a66cdb15f..fdc5c79946 100644 --- a/app/assets/javascripts/admin/templates/version-checks.hbs +++ b/app/assets/javascripts/admin/templates/version-checks.hbs @@ -48,7 +48,7 @@ {{else}} {{#if versionCheck.behindByOneVersion}} - {{fa-icon "smile-o"}} + {{fa-icon "meh-o"}} {{else}} {{fa-icon "frown-o"}} {{/if}} diff --git a/app/assets/javascripts/admin/views/logs/screened_emails_list_view.js b/app/assets/javascripts/admin/views/logs/screened_emails_list_view.js deleted file mode 100644 index ea0f05050f..0000000000 --- a/app/assets/javascripts/admin/views/logs/screened_emails_list_view.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.ScreenedEmailsListView = Ember.ListView.extend({ - height: 700, - rowHeight: 32, - itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/screened_emails_list_item"}) -}); diff --git a/app/assets/javascripts/admin/views/logs/screened_ip_addresses_list_view.js b/app/assets/javascripts/admin/views/logs/screened_ip_addresses_list_view.js deleted file mode 100644 index a6faf21e1b..0000000000 --- a/app/assets/javascripts/admin/views/logs/screened_ip_addresses_list_view.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.ScreenedIpAddressesListView = Ember.ListView.extend({ - height: 700, - rowHeight: 32, - itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/screened_ip_addresses_list_item"}) -}); diff --git a/app/assets/javascripts/admin/views/logs/screened_urls_list_view.js b/app/assets/javascripts/admin/views/logs/screened_urls_list_view.js deleted file mode 100644 index 98f65049cb..0000000000 --- a/app/assets/javascripts/admin/views/logs/screened_urls_list_view.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.ScreenedUrlsListView = Ember.ListView.extend({ - height: 700, - rowHeight: 32, - itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/screened_urls_list_item"}) -}); diff --git a/app/assets/javascripts/admin/views/logs/staff_action_logs_list_view.js b/app/assets/javascripts/admin/views/logs/staff_action_logs_list_view.js deleted file mode 100644 index ceb1f6fe29..0000000000 --- a/app/assets/javascripts/admin/views/logs/staff_action_logs_list_view.js +++ /dev/null @@ -1,5 +0,0 @@ -Discourse.StaffActionLogsListView = Ember.ListView.extend({ - height: 700, - rowHeight: 75, - itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/staff_action_logs_list_item"}) -}); diff --git a/app/assets/javascripts/admin/views/screened-emails-list.js.es6 b/app/assets/javascripts/admin/views/screened-emails-list.js.es6 new file mode 100644 index 0000000000..1b32fb36fe --- /dev/null +++ b/app/assets/javascripts/admin/views/screened-emails-list.js.es6 @@ -0,0 +1,8 @@ +import ListView from 'ember-addons/list-view'; +import ListItemView from 'ember-addons/list-item-view'; + +export default ListView.extend({ + height: 700, + rowHeight: 32, + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_emails_list_item"}) +}); diff --git a/app/assets/javascripts/admin/views/screened-ip-addresses-list.js.es6 b/app/assets/javascripts/admin/views/screened-ip-addresses-list.js.es6 new file mode 100644 index 0000000000..0d30fc6d48 --- /dev/null +++ b/app/assets/javascripts/admin/views/screened-ip-addresses-list.js.es6 @@ -0,0 +1,8 @@ +import ListView from 'ember-addons/list-view'; +import ListItemView from 'ember-addons/list-item-view'; + +export default ListView.extend({ + height: 700, + rowHeight: 32, + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_ip_addresses_list_item"}) +}); diff --git a/app/assets/javascripts/admin/views/screened-urls-list.js.es6 b/app/assets/javascripts/admin/views/screened-urls-list.js.es6 new file mode 100644 index 0000000000..b9d8b76667 --- /dev/null +++ b/app/assets/javascripts/admin/views/screened-urls-list.js.es6 @@ -0,0 +1,8 @@ +import ListView from 'ember-addons/list-view'; +import ListItemView from 'ember-addons/list-item-view'; + +export default ListView.extend({ + height: 700, + rowHeight: 32, + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_urls_list_item"}) +}); diff --git a/app/assets/javascripts/admin/views/staff-action-logs-list.js.es6 b/app/assets/javascripts/admin/views/staff-action-logs-list.js.es6 new file mode 100644 index 0000000000..cec82dba43 --- /dev/null +++ b/app/assets/javascripts/admin/views/staff-action-logs-list.js.es6 @@ -0,0 +1,8 @@ +import ListView from 'ember-addons/list-view'; +import ListItemView from 'ember-addons/list-item-view'; + +export default ListView.extend({ + height: 700, + rowHeight: 75, + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/staff_action_logs_list_item"}) +}); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 45dbef9147..5a6cf92823 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -1,6 +1,11 @@ /*global Favcount:true*/ var DiscourseResolver = require('discourse/ember/resolver').default; +// Allow us to import Ember +define('ember', ['exports'], function(__exports__) { + __exports__["default"] = Ember; +}); + window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { rootElement: '#main', _docTitle: document.title, diff --git a/app/assets/javascripts/discourse/views/activity-filter.js.es6 b/app/assets/javascripts/discourse/components/activity-filter.js.es6 similarity index 88% rename from app/assets/javascripts/discourse/views/activity-filter.js.es6 rename to app/assets/javascripts/discourse/components/activity-filter.js.es6 index 8fe07f7e6b..876e7bdb26 100644 --- a/app/assets/javascripts/discourse/views/activity-filter.js.es6 +++ b/app/assets/javascripts/discourse/components/activity-filter.js.es6 @@ -15,21 +15,21 @@ export default Ember.Component.extend(StringBuffer, { if (this.get('isIndexStream')) { return !this.get('userActionType'); } - var content = this.get('content'); + const content = this.get('content'); if (content) { return parseInt(this.get('userActionType'), 10) === parseInt(Em.get(content, 'action_type'), 10); } - }.property('userActionType', 'indexStream'), + }.property('userActionType', 'isIndexStream'), activityCount: function() { return this.get('content.count') || this.get('count') || 0; }.property('content.count', 'count'), typeKey: function() { - var actionType = this.get('content.action_type'); + const actionType = this.get('content.action_type'); if (actionType === Discourse.UserAction.TYPES.messages_received) { return ""; } - var result = Discourse.UserAction.TYPES_INVERTED[actionType]; + const result = Discourse.UserAction.TYPES_INVERTED[actionType]; if (!result) { return ""; } // We like our URLS to have hyphens, not underscores @@ -44,9 +44,9 @@ export default Ember.Component.extend(StringBuffer, { return this.get('content.description') || I18n.t("user.filters.all"); }.property('content.description'), - renderString: function(buffer) { + renderString(buffer) { buffer.push(""); - var icon = this.get('icon'); + const icon = this.get('icon'); if (icon) { buffer.push(" "); } diff --git a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 index f203799b22..fdad14df53 100644 --- a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 +++ b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 @@ -20,7 +20,7 @@ export default Ember.Component.extend({ }.observes("autoCloseTime", "limited"), _isAutoCloseValid: function(autoCloseTime, limited) { - var t = (autoCloseTime || "").trim(); + var t = (autoCloseTime || "").toString().trim(); if (t.length === 0) { // "empty" is always valid return true; diff --git a/app/assets/javascripts/discourse/views/category-chooser.js.es6 b/app/assets/javascripts/discourse/components/category-chooser.js.es6 similarity index 96% rename from app/assets/javascripts/discourse/views/category-chooser.js.es6 rename to app/assets/javascripts/discourse/components/category-chooser.js.es6 index 9f6fc1674f..ed2b8a55ea 100644 --- a/app/assets/javascripts/discourse/views/category-chooser.js.es6 +++ b/app/assets/javascripts/discourse/components/category-chooser.js.es6 @@ -1,4 +1,4 @@ -import ComboboxView from 'discourse/views/combo-box'; +import ComboboxView from 'discourse/components/combo-box'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; export default ComboboxView.extend({ @@ -41,7 +41,7 @@ export default ComboboxView.extend({ } }.property(), - template(item) { + comboTemplate(item) { let category; diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6 index faa87ea45c..bb63d0bbf9 100644 --- a/app/assets/javascripts/discourse/components/category-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/category-drop.js.es6 @@ -2,10 +2,7 @@ var get = Ember.get; export default Ember.Component.extend({ classNameBindings: ['category::no-category', 'categories:has-drop','categoryStyle'], - - categoryStyle: function(){ - return Discourse.SiteSettings.category_style; - }.property(), + categoryStyle: Discourse.computed.setting('category_style'), tagName: 'li', @@ -50,11 +47,11 @@ export default Ember.Component.extend({ if (color) { var style = ""; if (color) { style += "background-color: #" + color + ";" } - return style; + return style.htmlSafe(); } } - return "background-color: #eee;"; + return "background-color: #eee;".htmlSafe(); }.property('category'), badgeStyle: function() { @@ -68,11 +65,11 @@ export default Ember.Component.extend({ var style = ""; if (color) { style += "background-color: #" + color + "; border-color: #" + color + ";"; } if (textColor) { style += "color: #" + textColor + "; "; } - return style; + return style.htmlSafe(); } } - return "background-color: #eee; color: #333"; + return "background-color: #eee; color: #333".htmlSafe(); }.property('category'), clickEventName: function() { diff --git a/app/assets/javascripts/discourse/views/combo-box.js.es6 b/app/assets/javascripts/discourse/components/combo-box.js.es6 similarity index 92% rename from app/assets/javascripts/discourse/views/combo-box.js.es6 rename to app/assets/javascripts/discourse/components/combo-box.js.es6 index 96afbc9b89..e440f839ac 100644 --- a/app/assets/javascripts/discourse/views/combo-box.js.es6 +++ b/app/assets/javascripts/discourse/components/combo-box.js.es6 @@ -1,5 +1,4 @@ -// This view handles rendering of a combobox -export default Discourse.View.extend({ +export default Ember.Component.extend({ tagName: 'select', attributeBindings: ['tabindex'], classNames: ['combobox'], @@ -65,7 +64,7 @@ export default Discourse.View.extend({ o.selected = !!$(o).attr('selected'); }); - $elem.select2({formatResult: this.template, minimumResultsForSearch: 5, width: 'resolve'}); + $elem.select2({formatResult: this.comboTemplate, minimumResultsForSearch: 5, width: 'resolve'}); const castInteger = this.get('castInteger'); $elem.on("change", function (e) { diff --git a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 index 19103360dd..807990d707 100644 --- a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 +++ b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 @@ -9,7 +9,7 @@ export default Ember.Component.extend({ if (this.get('condition')) { buffer.push('
'); } else { - return this._super(); + return this._super(buffer); } }, diff --git a/app/assets/javascripts/discourse/components/count-i18n.js.es6 b/app/assets/javascripts/discourse/components/count-i18n.js.es6 new file mode 100644 index 0000000000..28c908be23 --- /dev/null +++ b/app/assets/javascripts/discourse/components/count-i18n.js.es6 @@ -0,0 +1,8 @@ +export default Ember.Component.extend(Discourse.StringBuffer, { + tagName: 'span', + rerenderTriggers: ['count', 'suffix'], + + renderString: function(buffer) { + buffer.push(I18n.t(this.get('key') + (this.get('suffix') || ''), { count: this.get('count') })); + } +}); diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index 973405128f..7673e60ac4 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -26,7 +26,7 @@ export default Ember.Component.extend({ if (label) { buffer.push(label); } } else { // If no label or icon is present, yield - return this._super(); + return this._super(buffer); } }, diff --git a/app/assets/javascripts/discourse/components/input-tip.js.es6 b/app/assets/javascripts/discourse/components/input-tip.js.es6 new file mode 100644 index 0000000000..2ba1c074c4 --- /dev/null +++ b/app/assets/javascripts/discourse/components/input-tip.js.es6 @@ -0,0 +1,17 @@ +import StringBuffer from 'discourse/mixins/string-buffer'; +import { iconHTML } from 'discourse/helpers/fa-icon'; + +export default Ember.Component.extend(StringBuffer, { + classNameBindings: [':tip', 'good', 'bad'], + rerenderTriggers: ['validation'], + + bad: Em.computed.alias('validation.failed'), + good: Em.computed.not('bad'), + + renderString(buffer) { + const reason = this.get('validation.reason'); + if (reason) { + buffer.push(iconHTML(this.get('good') ? 'check' : 'times') + ' ' + reason); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index 4942a90ca6..0b4e3695e5 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -1,22 +1,55 @@ +const INVITED_TYPE = 8; + export default Ember.Component.extend({ tagName: 'li', classNameBindings: ['notification.read', 'notification.is_warning'], + scope: function() { + return "notifications." + this.site.get("notificationLookup")[this.get("notification.notification_type")]; + }.property("notification.notification_type"), + + url: function() { + const it = this.get('notification'); + const badgeId = it.get("data.badge_id"); + if (badgeId) { + const badgeName = it.get("data.badge_name"); + return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase()); + } + + const topicId = it.get('topic_id'); + if (topicId) { + return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number")); + } + + if (it.get('notification_type') === INVITED_TYPE) { + return Discourse.getURL('/my/invited'); + } + }.property("notification.data.{badge_id,badge_name}", "model.slug", "model.topic_id", "model.post_number"), + + description: function() { + const badgeName = this.get("notification.data.badge_name"); + if (badgeName) { return Handlebars.Utils.escapeExpression(badgeName); } + + const title = this.get('notification.data.topic_title'); + return Ember.isEmpty(title) ? "" : Handlebars.Utils.escapeExpression(title); + }.property("notification.data.{badge_name,topic_title}"), + _markRead: function(){ - var self = this; - this.$('a').click(function(){ - self.set('notification.read', true); + this.$('a').click(() => { + this.set('notification.read', true); return true; }); }.on('didInsertElement'), - render: function(buffer) { - var notification = this.get('notification'), - text = I18n.t(this.get('scope'), Em.getProperties(notification, 'description', 'username')); + render(buffer) { + const notification = this.get('notification'); + const description = this.get('description'); + const username = notification.get('data.display_username'); + const text = I18n.t(this.get('scope'), {description, username}); - var url = notification.get('url'); + const url = this.get('url'); if (url) { - buffer.push('
' + text + ''); + buffer.push('' + text + ''); } else { buffer.push(text); } diff --git a/app/assets/javascripts/discourse/views/popup_input_tip_view.js b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 similarity index 63% rename from app/assets/javascripts/discourse/views/popup_input_tip_view.js rename to app/assets/javascripts/discourse/components/popup-input-tip.js.es6 index a21d186295..5c276aa907 100644 --- a/app/assets/javascripts/discourse/views/popup_input_tip_view.js +++ b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 @@ -1,23 +1,14 @@ -/** - This view extends the functionality of InputTipView with these extra features: - * it can be dismissed - * it bounces when it's shown - * it's absolutely positioned beside the input element, with the help of - extra css you'll need to write to line it up correctly. +import StringBuffer from 'discourse/mixins/string-buffer'; +import { iconHTML } from 'discourse/helpers/fa-icon'; - @class PopupInputTipView - @extends Discourse.View - @namespace Discourse - @module Discourse -**/ -Discourse.PopupInputTipView = Discourse.View.extend({ - templateName: 'popup_input_tip', +export default Ember.Component.extend(StringBuffer, { classNameBindings: [':popup-tip', 'good', 'bad', 'shownAt::hide'], animateAttribute: null, bouncePixels: 6, bounceDelay: 100, + rerenderTriggers: ['validation.reason'], - click: function() { + click() { this.set('shownAt', false); }, @@ -43,17 +34,23 @@ Discourse.PopupInputTipView = Discourse.View.extend({ } }.observes('shownAt'), - bounceLeft: function($elem) { + renderString(buffer) { + const reason = this.get('validation.reason'); + if (!reason) { return; } + + buffer.push("" + iconHTML('times-circle') + ""); + buffer.push(reason); + }, + + bounceLeft($elem) { for( var i = 0; i < 5; i++ ) { $elem.animate({ left: '+=' + this.bouncePixels }, this.bounceDelay).animate({ left: '-=' + this.bouncePixels }, this.bounceDelay); } }, - bounceRight: function($elem) { + bounceRight($elem) { for( var i = 0; i < 5; i++ ) { $elem.animate({ right: '-=' + this.bouncePixels }, this.bounceDelay).animate({ right: '+=' + this.bouncePixels }, this.bounceDelay); } } }); - -Discourse.View.registerHelper('popupInputTip', Discourse.PopupInputTipView); diff --git a/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 b/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 index a672696f78..72ad47d004 100644 --- a/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 +++ b/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 @@ -1,11 +1,3 @@ -/** - The controls for toggling the supression of deleted posts - - @class ToggleDeletedComponent - @extends Ember.Component - @namespace Discourse - @module Discourse -**/ export default Ember.Component.extend({ layoutName: 'components/toggle-deleted', tagName: 'section', diff --git a/app/assets/javascripts/discourse/components/user-stat.js.es6 b/app/assets/javascripts/discourse/components/user-stat.js.es6 new file mode 100644 index 0000000000..290aae5a97 --- /dev/null +++ b/app/assets/javascripts/discourse/components/user-stat.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ['user-stat'] +}); diff --git a/app/assets/javascripts/discourse/components/visible.js.es6 b/app/assets/javascripts/discourse/components/visible.js.es6 index 041f6131f9..f9570e412e 100644 --- a/app/assets/javascripts/discourse/components/visible.js.es6 +++ b/app/assets/javascripts/discourse/components/visible.js.es6 @@ -4,9 +4,8 @@ export default Ember.Component.extend({ }.observes("visible"), render: function(buffer){ - if(!this.get("visible")){ - return; - } + if (this._state !== 'inDOM' && this._state !== 'preRender' && this._state !== 'inBuffer') { return; } + if (!this.get("visible")) { return; } return this._super(buffer); } diff --git a/app/assets/javascripts/discourse/controllers/about.js.es6 b/app/assets/javascripts/discourse/controllers/about.js.es6 index e35882706a..3452f63d3c 100644 --- a/app/assets/javascripts/discourse/controllers/about.js.es6 +++ b/app/assets/javascripts/discourse/controllers/about.js.es6 @@ -1,6 +1,4 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend({ +export default Ember.Controller.extend({ faqOverriden: Ember.computed.gt('siteSettings.faq_url.length', 0), contactInfo: function() { diff --git a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 b/app/assets/javascripts/discourse/controllers/badges/show.js.es6 index ed43f55888..77bfa341bf 100644 --- a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/badges/show.js.es6 @@ -1,25 +1,19 @@ import ObjectController from 'discourse/controllers/object'; -/** - Controller for showing a particular badge. - - @class BadgesShowController - @extends ObjectController - @namespace Discourse - @module Discourse -**/ export default ObjectController.extend({ + noMoreBadges: false, + userBadges: null, needs: ["application"], actions: { - loadMore: function() { - var self = this; - var userBadges = this.get('userBadges'); + loadMore() { + const self = this; + const userBadges = this.get('userBadges'); Discourse.UserBadge.findByBadgeId(this.get('model.id'), { offset: userBadges.length - }).then(function(userBadges) { - self.get('userBadges').pushObjects(userBadges); + }).then(function(result) { + userBadges.pushObjects(result); if(userBadges.length === 0){ self.set('noMoreBadges', true); } diff --git a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 index 63f86045e9..2ba32b23fa 100644 --- a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 +++ b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 @@ -1,8 +1,10 @@ +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'; // Modal related to changing the ownership of posts -export default ObjectController.extend(Discourse.SelectedPostsCount, ModalFunctionality, { +export default ObjectController.extend(Presence, SelectedPostsCount, ModalFunctionality, { needs: ['topic'], topicController: Em.computed.alias('controllers.topic'), diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 01698d22b5..f3da08e9b5 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -1,6 +1,6 @@ -import DiscourseController from 'discourse/controllers/controller'; +import Presence from 'discourse/mixins/presence'; -export default DiscourseController.extend({ +export default Ember.ObjectController.extend(Presence, { needs: ['modal', 'topic', 'composer-messages', 'application'], replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY), @@ -10,6 +10,14 @@ export default DiscourseController.extend({ editReason: null, maxTitleLength: Discourse.computed.setting('max_topic_title_length'), scopedCategoryId: null, + similarTopics: null, + similarTopicsMessage: null, + lastSimilaritySearch: null, + + topic: null, + + // TODO: Remove this, very bad + view: null, _initializeSimilar: function() { this.set('similarTopics', []); @@ -183,7 +191,7 @@ export default DiscourseController.extend({ // for now handle a very narrow use case // if we are replying to a topic AND not on the topic pop the window up if (!force && composer.get('replyingToTopic')) { - const topic = this.get('topic'); + const topic = this.get('model.topic'); if (!topic || topic.get('id') !== composer.get('topic.id')) { const message = I18n.t("composer.posting_not_on_topic"); @@ -226,7 +234,6 @@ export default DiscourseController.extend({ imageSizes: this.get('view').imageSizes(), editReason: this.get("editReason") }).then(function(result) { - if (result.responseJson.action === "enqueued") { self.send('postWasEnqueued', result.responseJson); self.destroyDraft(); @@ -285,7 +292,7 @@ export default DiscourseController.extend({ // Checks to see if a reply has been typed. // This is signaled by a keyUp event in a view. checkReplyLength() { - if (this.present('model.reply')) { + if (!Ember.isEmpty('model.reply')) { // Notify the composer messages controller that a reply has been typed. Some // messages only appear after typing. this.get('controllers.composer-messages').typedReply(); @@ -441,6 +448,23 @@ export default DiscourseController.extend({ if (opts.topicCategoryId) { this.set('model.categoryId', opts.topicCategoryId); + } else if (opts.topicCategory) { + const splitCategory = opts.topicCategory.split("/"); + let category; + + if (!splitCategory[1]) { + category = this.site.get('categories').findProperty('nameLower', splitCategory[0].toLowerCase()); + } else { + const categories = Discourse.Category.list(); + const mainCategory = categories.findProperty('nameLower', splitCategory[0].toLowerCase()); + category = categories.find(function(item) { + return item && item.get('nameLower') === splitCategory[1].toLowerCase() && item.get('parent_category_id') === mainCategory.id; + }); + } + + if (category) { + this.set('model.categoryId', category.get('id')); + } } if (opts.topicBody) { @@ -452,7 +476,7 @@ export default DiscourseController.extend({ // View a new reply we've made viewNewReply() { - Discourse.URL.routeTo(this.get('createdPost.url')); + Discourse.URL.routeTo(this.get('model.createdPost.url')); this.close(); return false; }, diff --git a/app/assets/javascripts/discourse/controllers/discovery.js.es6 b/app/assets/javascripts/discourse/controllers/discovery.js.es6 index abaaa1e2b4..5276b912d6 100644 --- a/app/assets/javascripts/discourse/controllers/discovery.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery.js.es6 @@ -1,13 +1,11 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend({ +export default Ember.ObjectController.extend({ needs: ['navigation/category', 'discovery/topics', 'application'], loading: false, category: Em.computed.alias('controllers.navigation/category.category'), noSubcategories: Em.computed.alias('controllers.navigation/category.noSubcategories'), - loadedAllItems: Em.computed.not("controllers.discovery/topics.canLoadMore"), + loadedAllItems: Em.computed.not("controllers.discovery/topics.model.canLoadMore"), _showFooter: function() { this.set("controllers.application.showFooter", this.get("loadedAllItems")); diff --git a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 index 37078d492d..ebda2c04a1 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 @@ -3,7 +3,7 @@ import DiscoveryController from 'discourse/controllers/discovery'; export default DiscoveryController.extend({ needs: ['modal', 'discovery'], - withLogo: Em.computed.filterBy('categories', 'logo_url'), + withLogo: Em.computed.filterBy('model.categories', 'logo_url'), showPostsColumn: Em.computed.empty('withLogo'), actions: { @@ -35,7 +35,7 @@ export default DiscoveryController.extend({ }.property(), latestTopicOnly: function() { - return this.get('categories').find(function(c) { return c.get('featuredTopics.length') > 1; }) === undefined; - }.property('categories.@each.featuredTopics.length') + return this.get('model.categories').find(function(c) { return c.get('featuredTopics.length') > 1; }) === undefined; + }.property('model.categories.@each.featuredTopics.length') }); diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index 293d9c826c..15c8d66c2e 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -13,6 +13,8 @@ var controllerOpts = { order: 'default', ascending: false, + expandGloballyPinned: false, + expandAllPinned: false, actions: { @@ -80,27 +82,28 @@ var controllerOpts = { }.property(), isFilterPage: function(filter, filterType) { + if (!filter) { return false; } return filter.match(new RegExp(filterType + '$', 'gi')) ? true : false; }, showDismissRead: function() { - return this.isFilterPage(this.get('filter'), 'unread') && this.get('topics.length') > 0; - }.property('filter', 'topics.length'), + return this.isFilterPage(this.get('model.filter'), 'unread') && this.get('model.topics.length') > 0; + }.property('model.filter', 'model.topics.length'), showResetNew: function() { - return this.get('filter') === 'new' && this.get('topics.length') > 0; - }.property('filter', 'topics.length'), + return this.get('model.filter') === 'new' && this.get('model.topics.length') > 0; + }.property('model.filter', 'model.topics.length'), showDismissAtTop: function() { - return (this.isFilterPage(this.get('filter'), 'new') || - this.isFilterPage(this.get('filter'), 'unread')) && - this.get('topics.length') >= 30; - }.property('filter', 'topics.length'), + return (this.isFilterPage(this.get('model.filter'), 'new') || + this.isFilterPage(this.get('model.filter'), 'unread')) && + this.get('model.topics.length') >= 30; + }.property('model.filter', 'model.topics.length'), - hasTopics: Em.computed.gt('topics.length', 0), - allLoaded: Em.computed.empty('more_topics_url'), - latest: Discourse.computed.endWith('filter', 'latest'), - new: Discourse.computed.endWith('filter', 'new'), + hasTopics: Em.computed.gt('model.topics.length', 0), + allLoaded: Em.computed.empty('model.more_topics_url'), + latest: Discourse.computed.endWith('model.filter', 'latest'), + new: Discourse.computed.endWith('model.filter', 'new'), top: Em.computed.notEmpty('period'), yearly: Em.computed.equal('period', 'yearly'), monthly: Em.computed.equal('period', 'monthly'), @@ -114,8 +117,8 @@ var controllerOpts = { if( category ) { return I18n.t('topics.bottom.category', {category: category.get('name')}); } else { - var split = this.get('filter').split('/'); - if (this.get('topics.length') === 0) { + var split = (this.get('model.filter') || '').split('/'); + if (this.get('model.topics.length') === 0) { return I18n.t("topics.none." + split[0], { category: split[1] }); @@ -125,19 +128,19 @@ var controllerOpts = { }); } } - }.property('allLoaded', 'topics.length'), + }.property('allLoaded', 'model.topics.length'), footerEducation: function() { - if (!this.get('allLoaded') || this.get('topics.length') > 0 || !Discourse.User.current()) { return; } + if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !Discourse.User.current()) { return; } - var split = this.get('filter').split('/'); + var split = (this.get('model.filter') || '').split('/'); if (split[0] !== 'new' && split[0] !== 'unread') { return; } return I18n.t("topics.none.educate." + split[0], { userPrefsUrl: Discourse.getURL("/users/") + (Discourse.User.currentProp("username_lower")) + "/preferences" }); - }.property('allLoaded', 'topics.length'), + }.property('allLoaded', 'model.topics.length'), loadMoreTopics() { return this.get('model').loadMore(); diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 index 559bb80e06..24319137dc 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 @@ -9,20 +9,20 @@ export default ObjectController.extend(ModalFunctionality, { setAutoCloseTime: function() { var autoCloseTime = null; - if (this.get("details.auto_close_based_on_last_post")) { - autoCloseTime = this.get("details.auto_close_hours"); - } else if (this.get("details.auto_close_at")) { - var closeTime = new Date(this.get("details.auto_close_at")); + if (this.get("model.details.auto_close_based_on_last_post")) { + autoCloseTime = this.get("model.details.auto_close_hours"); + } else if (this.get("model.details.auto_close_at")) { + var closeTime = new Date(this.get("model.details.auto_close_at")); if (closeTime > new Date()) { autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm"); } } - this.set("auto_close_time", autoCloseTime); - }.observes("details.{auto_close_at,auto_close_hours}"), + this.set("model.auto_close_time", autoCloseTime); + }.observes("model.details.{auto_close_at,auto_close_hours}"), actions: { - saveAutoClose: function() { this.setAutoClose(this.get("auto_close_time")); }, + saveAutoClose: function() { this.setAutoClose(this.get("model.auto_close_time")); }, removeAutoClose: function() { this.setAutoClose(null); } }, @@ -30,18 +30,18 @@ export default ObjectController.extend(ModalFunctionality, { var self = this; this.send('hideModal'); Discourse.ajax({ - url: '/t/' + this.get('id') + '/autoclose', + url: '/t/' + this.get('model.id') + '/autoclose', type: 'PUT', dataType: 'json', data: { auto_close_time: time, - auto_close_based_on_last_post: this.get("details.auto_close_based_on_last_post"), + auto_close_based_on_last_post: this.get("model.details.auto_close_based_on_last_post"), } }).then(function(result){ if (result.success) { self.send('closeModal'); - self.set('details.auto_close_at', result.auto_close_at); - self.set('details.auto_close_hours', result.auto_close_hours); + self.set('model.details.auto_close_at', result.auto_close_at); + self.set('model.details.auto_close_hours', result.auto_close_hours); } else { bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); } diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 index a3eb9d56d1..a00c79a5b3 100644 --- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 @@ -11,14 +11,14 @@ export default ObjectController.extend(ModalFunctionality, { bannerCount: 0, categoryLink: function() { - return categoryLinkHTML(this.get("category"), { allowUncategorized: true }); - }.property("category"), + return categoryLinkHTML(this.get("model.category"), { allowUncategorized: true }); + }.property("model.category"), unPinMessage: function() { - return this.get("pinned_globally") ? + return this.get("model.pinned_globally") ? I18n.t("topic.feature_topic.unpin_globally") : I18n.t("topic.feature_topic.unpin", { categoryLink: this.get("categoryLink") }); - }.property("categoryLink", "pinned_globally"), + }.property("categoryLink", "model.pinned_globally"), pinMessage: function() { return I18n.t("topic.feature_topic.pin", { categoryLink: this.get("categoryLink") }); @@ -32,7 +32,7 @@ export default ObjectController.extend(ModalFunctionality, { this.set("loading", true); return Discourse.ajax("/topics/feature_stats.json", { - data: { category_id: this.get("category.id") } + data: { category_id: this.get("model.category.id") } }).then(result => { if (result) { this.setProperties({ diff --git a/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 b/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 index 484fd12f66..7ff592a4c1 100644 --- a/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 @@ -7,22 +7,22 @@ export default ObjectController.extend({ message: Em.computed.alias('controllers.flag.message'), customPlaceholder: function(){ - return I18n.t("flagging.custom_placeholder_" + this.get('name_key')); - }.property('name_key'), + return I18n.t("flagging.custom_placeholder_" + this.get('model.name_key')); + }.property('model.name_key'), formattedName: function(){ - if (this.get("is_custom_flag")) { - return this.get('name').replace("{{username}}", this.get('controllers.flag.username')); + if (this.get("model.is_custom_flag")) { + return this.get('model.name').replace("{{username}}", this.get('controllers.flag.model.username')); } else { - return I18n.t("flagging.formatted_name." + this.get('name_key')); + return I18n.t("flagging.formatted_name." + this.get('model.name_key')); } - }.property('name', 'name_key', 'is_custom_flag'), + }.property('model.name', 'model.name_key', 'model.is_custom_flag'), selected: function() { return this.get('model') === this.get('controllers.flag.selected'); }.property('controllers.flag.selected'), - showMessageInput: Em.computed.and('is_custom_flag', 'selected'), + showMessageInput: Em.computed.and('model.is_custom_flag', 'selected'), showDescription: Em.computed.not('showMessageInput'), customMessageLengthClasses: function() { diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6 index 4691318c29..4708d2e950 100644 --- a/app/assets/javascripts/discourse/controllers/flag.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag.js.es6 @@ -2,8 +2,13 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ObjectController from 'discourse/controllers/object'; export default ObjectController.extend(ModalFunctionality, { + userDetails: null, + selected: null, + flagTopic: null, + message: null, + topicActionByName: null, - onShow: function() { + onShow() { this.set('selected', null); }, @@ -11,32 +16,31 @@ export default ObjectController.extend(ModalFunctionality, { if (!this.get('flagTopic')) { return this.get('model.flagsAvailable'); } else { - var self = this, + const self = this, lookup = Em.Object.create(); - _.each(this.get("actions_summary"),function(a) { - var actionSummary; + _.each(this.get("model.actions_summary"),function(a) { a.flagTopic = self.get('model'); a.actionType = self.site.topicFlagTypeById(a.id); - actionSummary = Discourse.ActionSummary.create(a); + const actionSummary = Discourse.ActionSummary.create(a); lookup.set(a.actionType.get('name_key'), actionSummary); }); this.set('topicActionByName', lookup); return this.site.get('topic_flag_types').filter(function(item) { - return _.any(self.get("actions_summary"), function(a) { + return _.any(self.get("model.actions_summary"), function(a) { return (a.id === item.get('id') && a.can_act); }); }); } - }.property('post', 'flagTopic', 'actions_summary.@each.can_act'), + }.property('post', 'flagTopic', 'model.actions_summary.@each.can_act'), submitEnabled: function() { - var selected = this.get('selected'); + const selected = this.get('selected'); if (!selected) return false; if (selected.get('is_custom_flag')) { - var len = this.get('message.length') || 0; + const len = this.get('message.length') || 0; return len >= Discourse.SiteSettings.min_private_message_post_length && len <= Discourse.PostActionType.MAX_MESSAGE_LENGTH; } @@ -63,27 +67,29 @@ export default ObjectController.extend(ModalFunctionality, { }.property('selected.is_custom_flag'), actions: { - takeAction: function() { + takeAction() { this.send('createFlag', {takeAction: true}); - this.set('hidden', true); + this.set('model.hidden', true); }, - createFlag: function(opts) { - var self = this; - var postAction; // an instance of ActionSummary + createFlag(opts) { + const self = this; + let postAction; // an instance of ActionSummary if (!this.get('flagTopic')) { - postAction = this.get('actionByName.' + this.get('selected.name_key')); + postAction = this.get('model.actionByName.' + this.get('selected.name_key')); } else { postAction = this.get('topicActionByName.' + this.get('selected.name_key')); } - var params = this.get('selected.is_custom_flag') ? {message: this.get('message')} : {}; - - if (opts) params = $.extend(params, opts); + let params = this.get('selected.is_custom_flag') ? {message: this.get('message')} : {}; + if (opts) { params = $.extend(params, opts); } this.send('hideModal'); postAction.act(this.get('model'), params).then(function() { self.send('closeModal'); + if (params.message) { + self.set('message', ''); + } }, function(errors) { self.send('closeModal'); if (errors && errors.responseText) { @@ -94,7 +100,7 @@ export default ObjectController.extend(ModalFunctionality, { }); }, - changePostActionType: function(action) { + changePostActionType(action) { this.set('selected', action); }, }, @@ -112,12 +118,12 @@ export default ObjectController.extend(ModalFunctionality, { usernameChanged: function() { this.set('userDetails', null); this.fetchUserDetails(); - }.observes('username'), + }.observes('model.username'), fetchUserDetails: function() { - if( Discourse.User.currentProp('staff') && this.get('username') ) { - var flagController = this; - Discourse.AdminUser.find(this.get('username').toLowerCase()).then(function(user){ + if( Discourse.User.currentProp('staff') && this.get('model.username') ) { + const flagController = this; + Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){ flagController.set('userDetails', user); }); } diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index c0c4cb17dd..c414227b6b 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -41,10 +41,11 @@ const HeaderController = DiscourseController.extend({ if (self.get("loadingNotifications")) { return; } self.set("loadingNotifications", true); - Discourse.NotificationContainer.loadRecent().then(function(result) { + + this.store.find('notification', {recent: true}).then(function(notifications) { self.setProperties({ 'currentUser.unread_notifications': 0, - notifications: result + notifications }); }).catch(function() { self.setProperties({ @@ -79,27 +80,18 @@ function addFlagProperty(prop) { _flagProperties.pushObject(prop); } -let _appliedFlagProps = false; -HeaderController.reopenClass({ - create() { - // We only want to change the class the first time it's created - if (!_appliedFlagProps && _flagProperties.length) { - _appliedFlagProps = true; - - const args = _flagProperties.slice(); - args.push(function() { - let sum = 0; - _flagProperties.forEach((fp) => sum += (this.get(fp) || 0)); - return sum; - }); - HeaderController.reopen({ flaggedPostsCount: Ember.computed.apply(this, args) }); - } - return this._super.apply(this, Array.prototype.slice.call(arguments)); - } -}); +function applyFlaggedProperties() { + const args = _flagProperties.slice(); + args.push(function() { + let sum = 0; + _flagProperties.forEach((fp) => sum += (this.get(fp) || 0)); + return sum; + }); + HeaderController.reopen({ flaggedPostsCount: Ember.computed.apply(this, args) }); +} addFlagProperty('currentUser.site_flagged_posts_count'); addFlagProperty('currentUser.post_queue_new_count'); -export { addFlagProperty }; +export { addFlagProperty, applyFlaggedProperties }; export default HeaderController; diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 2201c3eb57..ac1cc0628c 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -1,7 +1,8 @@ +import Presence from 'discourse/mixins/presence'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ObjectController from 'discourse/controllers/object'; -export default ObjectController.extend(ModalFunctionality, { +export default ObjectController.extend(Presence, ModalFunctionality, { needs: ['user-invited'], // If this isn't defined, it will proxy to the user model on the preferences diff --git a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 index 658efcd5fc..c923aaf2da 100644 --- a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 @@ -1,8 +1,10 @@ +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'; // Modal related to merging of topics -export default ObjectController.extend(Discourse.SelectedPostsCount, ModalFunctionality, { +export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, Presence, { needs: ['topic'], topicController: Em.computed.alias('controllers.topic'), diff --git a/app/assets/javascripts/discourse/controllers/notification.js.es6 b/app/assets/javascripts/discourse/controllers/notification.js.es6 deleted file mode 100644 index e13377d69e..0000000000 --- a/app/assets/javascripts/discourse/controllers/notification.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -import ObjectController from 'discourse/controllers/object'; - -const INVITED_TYPE = 8; - -export default ObjectController.extend({ - - notificationUrl: function(it) { - var badgeId = it.get("data.badge_id"); - if (badgeId) { - var badgeName = it.get("data.badge_name"); - return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase()); - } - - var topicId = it.get('topic_id'); - if (topicId) { - return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number")); - } - - if (it.get('notification_type') === INVITED_TYPE) { - return Discourse.getURL('/my/invited'); - } - }, - - scope: function() { - return "notifications." + this.site.get("notificationLookup")[this.get("notification_type")]; - }.property("notification_type"), - - username: Em.computed.alias("data.display_username"), - - url: function() { - return this.notificationUrl(this); - }.property("data.{badge_id,badge_name}", "slug", "topic_id", "post_number"), - - description: function() { - const badgeName = this.get("data.badge_name"); - if (badgeName) { return Handlebars.Utils.escapeExpression(badgeName); } - return this.blank("data.topic_title") ? "" : Handlebars.Utils.escapeExpression(this.get("data.topic_title")); - }.property("data.{badge_name,topic_title}") - -}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 index c0bacaaa27..ed6699ad4a 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 @@ -1,6 +1,7 @@ +import Presence from 'discourse/mixins/presence'; import ObjectController from 'discourse/controllers/object'; -export default ObjectController.extend({ +export default ObjectController.extend(Presence, { taken: false, saving: false, error: false, diff --git a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 index c73822188c..0a4c0e3ada 100644 --- a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 @@ -43,7 +43,7 @@ export default DiscourseController.extend({ if (this.get('buffer') === selectedText) return; // we need to retrieve the post data from the posts collection in the topic controller - const postStream = this.get('controllers.topic.postStream'); + const postStream = this.get('controllers.topic.model.postStream'); this.set('post', postStream.findLoadedPost(postId)); this.set('buffer', selectedText); diff --git a/app/assets/javascripts/discourse/controllers/share.js.es6 b/app/assets/javascripts/discourse/controllers/share.js.es6 index abf8cd081d..2e662dc217 100644 --- a/app/assets/javascripts/discourse/controllers/share.js.es6 +++ b/app/assets/javascripts/discourse/controllers/share.js.es6 @@ -2,7 +2,7 @@ import Sharing from 'discourse/lib/sharing'; export default Ember.Controller.extend({ needs: ['topic'], - title: Ember.computed.alias('controllers.topic.title'), + title: Ember.computed.alias('controllers.topic.model.title'), displayDate: function() { return Discourse.Formatter.longDateNoYear(new Date(this.get('date'))); diff --git a/app/assets/javascripts/discourse/controllers/site-map-category.js.es6 b/app/assets/javascripts/discourse/controllers/site-map-category.js.es6 index 52da588ccf..0fcce87f97 100644 --- a/app/assets/javascripts/discourse/controllers/site-map-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/site-map-category.js.es6 @@ -1,10 +1,10 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ needs: ['site-map'], unreadTotal: function() { - return parseInt(this.get('unreadTopics'), 10) + - parseInt(this.get('newTopics'), 10); - }.property('unreadTopics', 'newTopics'), + return parseInt(this.get('model.unreadTopics'), 10) + + parseInt(this.get('model.newTopics'), 10); + }.property('model.unreadTopics', 'model.newTopics'), showTopicCount: Em.computed.not('currentUser') }); diff --git a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 index 7ef7702000..98aed2dcce 100644 --- a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 @@ -1,8 +1,10 @@ +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'; // Modal related to auto closing of topics -export default ObjectController.extend(Discourse.SelectedPostsCount, ModalFunctionality, { +export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, Presence, { needs: ['topic'], topicController: Em.computed.alias('controllers.topic'), diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index f2e18692d0..2e9050b2d5 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -1,9 +1,9 @@ -export default Em.ObjectController.extend({ - showLoginButton: Em.computed.equal('path', 'login'), +export default Ember.Controller.extend({ + showLoginButton: Em.computed.equal('model.path', 'login'), actions: { markFaqRead: function() { - if (Discourse.User.current()) { + if (this.currentUser) { Discourse.ajax("/users/read-faq", { method: "POST" }); } } diff --git a/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 index c89caf04c1..20bb123a0c 100644 --- a/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 @@ -3,8 +3,8 @@ import ObjectController from 'discourse/controllers/object'; // This controller supports the admin menu on topics export default ObjectController.extend({ menuVisible: false, - showRecover: Em.computed.and('deleted', 'details.can_recover'), - isFeatured: Em.computed.or("pinned_at", "isBanner"), + showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'), + isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"), actions: { show: function() { this.set('menuVisible', true); }, diff --git a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 index 411496b06f..b98b14550d 100644 --- a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 @@ -17,7 +17,7 @@ function entranceDate(dt, showTime) { ); } -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ position: null, createdDate: function() { @@ -51,11 +51,11 @@ export default Ember.ObjectController.extend({ }, enterTop: function() { - Discourse.URL.routeTo(this.get('url')); + Discourse.URL.routeTo(this.get('model.url')); }, enterBottom: function() { - Discourse.URL.routeTo(this.get('lastPostUrl')); + Discourse.URL.routeTo(this.get('model.lastPostUrl')); } } }); diff --git a/app/assets/javascripts/discourse/controllers/topic-list-item.js.es6 b/app/assets/javascripts/discourse/controllers/topic-list-item.js.es6 deleted file mode 100644 index 073180ea78..0000000000 --- a/app/assets/javascripts/discourse/controllers/topic-list-item.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -import ObjectController from 'discourse/controllers/object'; - -// Handles displaying of a topic as a list item -export default ObjectController.extend({ - needs: ['discovery/topics'], - - canStar: Em.computed.alias('controllers.discovery/topics.currentUser.id'), - bulkSelectEnabled: Em.computed.alias('controllers.discovery/topics.bulkSelectEnabled'), - showTopicPostBadges: Em.computed.not('controllers.discovery/topics.new'), - - checked: function(key, value) { - var selected = this.get('controllers.discovery/topics.selected'), - topic = this.get('model'); - - if (arguments.length > 1) { - if (value) { - selected.addObject(topic); - } else { - selected.removeObject(topic); - } - } - return selected.contains(topic); - }.property('controllers.discovery/topics.selected.length'), - - titleColSpan: function() { - // Uncategorized pinned topics will span the title and category column in the topic list. - return (!this.get('controllers.discovery/topics.hideCategory') && - this.get('model.isPinnedUncategorized') ? 2 : 1); - }.property('controllers.discovery/topics.hideCategory', 'model.isPinnedUncategorized'), - - hideCategory: function() { - return this.get('controllers.discovery/topics.hideCategory') || this.get('titleColSpan') > 1; - }.property('controllers.discovery/topics.hideCategory', 'titleColSpan'), - - actions: { - toggleStar: function() { - this.get('model').toggleStar(); - } - } -}); diff --git a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 index 1eef23fce3..ee844f4c6d 100644 --- a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 @@ -24,11 +24,11 @@ export default Ember.ObjectController.extend({ if (isNaN(postIndex) || postIndex < 1) { postIndex = 1; } - if (postIndex > this.get('postStream.filteredPostsCount')) { - postIndex = this.get('postStream.filteredPostsCount'); + if (postIndex > this.get('model.postStream.filteredPostsCount')) { + postIndex = this.get('model.postStream.filteredPostsCount'); } this.set('toPostIndex', postIndex); - var stream = this.get('postStream'), + var stream = this.get('model.postStream'), postId = stream.findPostIdForPostNumber(postIndex); if (!postId) { @@ -65,36 +65,36 @@ export default Ember.ObjectController.extend({ }, streamPercentage: function() { - if (!this.get('postStream.loaded')) { return 0; } - if (this.get('postStream.highest_post_number') === 0) { return 0; } - var perc = this.get('progressPosition') / this.get('postStream.filteredPostsCount'); + if (!this.get('model.postStream.loaded')) { return 0; } + if (this.get('model.postStream.highest_post_number') === 0) { return 0; } + var perc = this.get('progressPosition') / this.get('model.postStream.filteredPostsCount'); return (perc > 1.0) ? 1.0 : perc; - }.property('postStream.loaded', 'progressPosition', 'postStream.filteredPostsCount'), + }.property('model.postStream.loaded', 'progressPosition', 'model.postStream.filteredPostsCount'), jumpTopDisabled: function() { return this.get('progressPosition') <= 3; }.property('progressPosition'), filteredPostCountChanged: function(){ - if(this.get('postStream.filteredPostsCount') < this.get('progressPosition')){ - this.set('progressPosition', this.get('postStream.filteredPostsCount')); + if(this.get('model.postStream.filteredPostsCount') < this.get('progressPosition')){ + this.set('progressPosition', this.get('model.postStream.filteredPostsCount')); } - }.observes('postStream.filteredPostsCount'), + }.observes('model.postStream.filteredPostsCount'), jumpBottomDisabled: function() { - return this.get('progressPosition') >= this.get('postStream.filteredPostsCount') || + return this.get('progressPosition') >= this.get('model.postStream.filteredPostsCount') || this.get('progressPosition') >= this.get('highest_post_number'); - }.property('postStream.filteredPostsCount', 'highest_post_number', 'progressPosition'), + }.property('model.postStream.filteredPostsCount', 'highest_post_number', 'progressPosition'), hideProgress: function() { - if (!this.get('postStream.loaded')) return true; - if (!this.get('currentPost')) return true; - if (this.get('postStream.filteredPostsCount') < 2) return true; + if (!this.get('model.postStream.loaded')) return true; + if (!this.get('model.currentPost')) return true; + if (this.get('model.postStream.filteredPostsCount') < 2) return true; return false; - }.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'), + }.property('model.postStream.loaded', 'model.currentPost', 'model.postStream.filteredPostsCount'), hugeNumberOfPosts: function() { - return (this.get('postStream.filteredPostsCount') >= Discourse.SiteSettings.short_progress_text_threshold); + return (this.get('model.postStream.filteredPostsCount') >= Discourse.SiteSettings.short_progress_text_threshold); }.property('highest_post_number'), jumpToBottomTitle: function() { diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 205c750075..6256857f60 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -1,9 +1,10 @@ import ObjectController from 'discourse/controllers/object'; import BufferedContent from 'discourse/mixins/buffered-content'; +import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import Topic from 'discourse/models/topic'; -export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, { +export default ObjectController.extend(SelectedPostsCount, BufferedContent, { multiSelect: false, needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'], allPostsSelected: false, @@ -12,6 +13,9 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon selectedReplies: null, queryParams: ['filter', 'username_filters', 'show_deleted'], searchHighlight: null, + loadedAllPosts: false, + enteredAt: null, + firstPostExpanded: false, maxTitleLength: Discourse.computed.setting('max_topic_title_length'), @@ -20,14 +24,14 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }.observes('topic'), _titleChanged: function() { - const title = this.get('title'); + const title = this.get('model.title'); if (!Ember.isEmpty(title)) { // Note normally you don't have to trigger this, but topic titles can be updated // and are sometimes lazily loaded. this.send('refreshTitle'); } - }.observes('title', 'category'), + }.observes('model.title', 'category'), termChanged: function() { const dropdown = this.get('controllers.header.visibleDropdown'); @@ -47,47 +51,47 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon // semantics of loaded all posts are slightly diff at topic level, // it just means that we "once" loaded all posts, this means we don't // keep re-rendering the suggested topics when new posts zoom in - let loaded = this.get('postStream.loadedAllPosts'); + let loaded = this.get('model.postStream.loadedAllPosts'); if (loaded) { - this.set('loadedTopicId', this.get('model.id')); + this.set('model.loadedTopicId', this.get('model.id')); } else { - loaded = this.get('loadedTopicId') === this.get('model.id'); + loaded = this.get('model.loadedTopicId') === this.get('model.id'); } this.set('loadedAllPosts', loaded); - }.observes('postStream', 'postStream.loadedAllPosts'), + }.observes('model.postStream', 'model.postStream.loadedAllPosts'), show_deleted: function(key, value) { - const postStream = this.get('postStream'); + const postStream = this.get('model.postStream'); if (!postStream) { return; } if (arguments.length > 1) { postStream.set('show_deleted', value); } return postStream.get('show_deleted') ? true : undefined; - }.property('postStream.summary'), + }.property('model.postStream.summary'), filter: function(key, value) { - const postStream = this.get('postStream'); + const postStream = this.get('model.postStream'); if (!postStream) { return; } if (arguments.length > 1) { postStream.set('summary', value === "summary"); } return postStream.get('summary') ? "summary" : undefined; - }.property('postStream.summary'), + }.property('model.postStream.summary'), username_filters: function(key, value) { - const postStream = this.get('postStream'); + const postStream = this.get('model.postStream'); if (!postStream) { return; } if (arguments.length > 1) { postStream.set('streamFilters.username_filters', value); } return postStream.get('streamFilters.username_filters'); - }.property('postStream.streamFilters.username_filters'), + }.property('model.postStream.streamFilters.username_filters'), _clearSelected: function() { this.set('selectedPosts', []); @@ -95,7 +99,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }.on('init'), _togglePinnedStates(property) { - const value = this.get('pinned_at') ? false : true, + const value = this.get('model.pinned_at') ? false : true, topic = this.get('content'); // optimistic update @@ -189,7 +193,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon 'class': 'btn-primary', callback() { Discourse.Post.deleteMany([post], [post]); - self.get('postStream.posts').forEach(function (p) { + self.get('model.postStream.posts').forEach(function (p) { if (p === post || p.get('reply_to_post_number') === post.get('post_number')) { p.setDeletedState(user); } @@ -246,7 +250,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }, selectAll() { - const posts = this.get('postStream.posts'), + const posts = this.get('model.postStream.posts'), selectedPosts = this.get('selectedPosts'); if (posts) { selectedPosts.addObjects(posts); @@ -261,11 +265,11 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }, toggleParticipant(user) { - this.get('postStream').toggleParticipant(Em.get(user, 'username')); + this.get('model.postStream').toggleParticipant(Em.get(user, 'username')); }, editTopic() { - if (!this.get('details.can_edit')) return false; + if (!this.get('model.details.can_edit')) return false; this.set('editingTopic', true); return false; @@ -326,7 +330,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon const selectedPosts = self.get('selectedPosts'), selectedReplies = self.get('selectedReplies'), - postStream = self.get('postStream'), + postStream = self.get('model.postStream'), toRemove = []; Discourse.Post.deleteMany(selectedPosts, selectedReplies); @@ -365,7 +369,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }, togglePinned() { - const value = this.get('pinned_at') ? false : true, + const value = this.get('model.pinned_at') ? false : true, topic = this.get('content'); // optimistic update @@ -403,7 +407,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }, togglePinnedForUser() { - if (this.get('pinned_at')) { + if (this.get('model.pinned_at')) { if (this.get('pinned')) { this.get('content').clearPin(); } else { @@ -428,7 +432,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon return Em.isEmpty(quotedText) ? Discourse.Post.loadQuote(post.get('id')) : quotedText; }).then(function(q) { const postUrl = "" + location.protocol + "//" + location.host + post.get('url'), - postLink = "[" + Handlebars.escapeExpression(self.get('title')) + "](" + postUrl + ")"; + postLink = "[" + Handlebars.escapeExpression(self.get('model.title')) + "](" + postUrl + ")"; composerController.appendText(I18n.t("post.continue_discussion", { postLink: postLink }) + "\n\n" + q); }); }, @@ -448,7 +452,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon retryLoading() { const self = this; self.set('retrying', true); - this.get('postStream').refresh().then(function() { + this.get('model.postStream').refresh().then(function() { self.set('retrying', false); }, function() { self.set('retrying', false); @@ -491,12 +495,12 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }.property(), canMergeTopic: function() { - if (!this.get('details.can_move_posts')) return false; + if (!this.get('model.details.can_move_posts')) return false; return (this.get('selectedPostsCount') > 0); }.property('selectedPostsCount'), canSplitTopic: function() { - if (!this.get('details.can_move_posts')) return false; + if (!this.get('model.details.can_move_posts')) return false; if (this.get('allPostsSelected')) return false; return (this.get('selectedPostsCount') > 0); }.property('selectedPostsCount'), @@ -533,7 +537,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon return canDelete; }.property('selectedPostsCount'), - hasError: Ember.computed.or('notFoundHtml', 'message'), + hasError: Ember.computed.or('model.notFoundHtml', 'model.message'), noErrorYet: Ember.computed.not('hasError'), multiSelectChanged: function() { @@ -564,8 +568,8 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }, showStarButton: function() { - return Discourse.User.current() && !this.get('isPrivateMessage'); - }.property('isPrivateMessage'), + return Discourse.User.current() && !this.get('model.isPrivateMessage'); + }.property('model.isPrivateMessage'), loadingHTML: function() { return spinnerHTML; @@ -586,7 +590,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon this.unsubscribe(); const self = this; - this.messageBus.subscribe("/topic/" + this.get('id'), function(data) { + this.messageBus.subscribe("/topic/" + this.get('model.id'), function(data) { const topic = self.get('model'); if (data.notification_level_change) { @@ -595,7 +599,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon return; } - const postStream = self.get('postStream'); + const postStream = self.get('model.postStream'); switch (data.type) { case "revised": case "acted": @@ -643,27 +647,24 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon return false; } else { selectedPosts.addObject(post); - // If the user manually selects all posts, all posts are selected - if (selectedPosts.length === this.get('posts_count')) { - this.set('allPostsSelected', true); - } + this.set('allPostsSelected', selectedPosts.length === this.get('model.posts_count')); return true; } }, // If our current post is changed, notify the router _currentPostChanged: function() { - const currentPost = this.get('currentPost'); + const currentPost = this.get('model.currentPost'); if (currentPost) { this.send('postChangedRoute', currentPost); } - }.observes('currentPost'), + }.observes('model.currentPost'), readPosts(topicId, postNumbers) { - const postStream = this.get('postStream'); + const postStream = this.get('model.postStream'); - if(this.get('postStream.topic.id') === topicId){ + if (postStream.get('topic.id') === topicId){ _.each(postStream.get('posts'), function(post){ // optimise heavy loop // TODO identity map for postNumber @@ -673,8 +674,8 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }); const max = _.max(postNumbers); - if(max > this.get('last_read_post_number')){ - this.set('last_read_post_number', max); + if(max > this.get('model.last_read_post_number')){ + this.set('model.sast_read_post_number', max); } } }, @@ -683,10 +684,10 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon topVisibleChanged(post) { if (!post) { return; } - const postStream = this.get('postStream'), - firstLoadedPost = postStream.get('firstLoadedPost'); + const postStream = this.get('model.postStream'), + firstLoadedPost = postStream.get('firstLoadedPost'); - this.set('currentPost', post.get('post_number')); + this.set('model.currentPost', post.get('post_number')); if (post.get('post_number') === 1) { return; } @@ -721,7 +722,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon bottomVisibleChanged(post) { if (!post) { return; } - const postStream = this.get('postStream'), + const postStream = this.get('model.postStream'), lastLoadedPost = postStream.get('lastLoadedPost'); this.set('controllers.topic-progress.progressPosition', postStream.progressIndexOfPost(post)); @@ -732,7 +733,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon }, _showFooter: function() { - this.set("controllers.application.showFooter", this.get("postStream.loadedAllPosts")); - }.observes("postStream.loadedAllPosts") + this.set("controllers.application.showFooter", this.get("model.postStream.loadedAllPosts")); + }.observes("model.postStream.loadedAllPosts") }); diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 index 878fa3fde4..6113cedff3 100644 --- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 @@ -1,15 +1,16 @@ export default Ember.ObjectController.extend({ + userActionType: null, needs: ["application"], _showFooter: function() { var showFooter; if (this.get("userActionType")) { - var stat = _.find(this.get("stats"), { action_type: this.get("userActionType") }); - showFooter = stat && stat.count <= this.get("stream.itemsLoaded"); + var stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") }); + showFooter = stat && stat.count <= this.get("model.stream.itemsLoaded"); } else { - showFooter = this.get("statsCountNonPM") <= this.get("stream.itemsLoaded"); + showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded"); } this.set("controllers.application.showFooter", showFooter); - }.observes("userActionType", "stream.itemsLoaded") + }.observes("userActionType", "model.stream.itemsLoaded") }); diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 60922c4c15..2786d25f77 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -1,6 +1,4 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend({ +export default Ember.Controller.extend({ needs: ['topic', 'application'], visible: false, user: null, @@ -13,7 +11,7 @@ export default ObjectController.extend({ // If inside a topic topicPostCount: null, - postStream: Em.computed.alias('controllers.topic.postStream'), + postStream: Em.computed.alias('controllers.topic.model.postStream'), enoughPostsForFiltering: Em.computed.gte('topicPostCount', 2), viewingTopic: Em.computed.match('controllers.application.currentPath', /^topic\./), viewingAdmin: Em.computed.match('controllers.application.currentPath', /^admin\./), @@ -47,7 +45,7 @@ export default ObjectController.extend({ const currentUsername = this.get('username'), wasVisible = this.get('visible'), - post = this.get('viewingTopic') && postId ? this.get('controllers.topic.postStream').findLoadedPost(postId) : null; + post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; this.setProperties({ avatar: null, post: post, username: username }); @@ -92,7 +90,7 @@ export default ObjectController.extend({ actions: { togglePosts(user) { - const postStream = this.get('controllers.topic.postStream'); + const postStream = this.get('postStream'); postStream.toggleParticipant(user.get('username')); this.close(); }, diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index 3943daa868..85865c16e5 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -1,43 +1,21 @@ - export default Ember.ArrayController.extend({ - needs: ['user-notifications', 'application'], - loading: false, + needs: ['application'], _showFooter: function() { - this.set("controllers.application.showFooter", !this.get("canLoadMore")); - }.observes("canLoadMore"), + this.set("controllers.application.showFooter", !this.get("model.canLoadMore")); + }.observes("model.canLoadMore"), - showDismissButton: function() { - return this.get('user').total_unread_notifications > 0; - }.property('user'), + showDismissButton: Ember.computed.gt('user.total_unread_notifications', 0), actions: { resetNew: function() { - var self = this; - Discourse.NotificationContainer.resetNew().then(function() { - self.get('controllers.user-notifications').setEach('read', true); + Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { + this.setEach('read', true); }); }, loadMore: function() { - if (this.get('canLoadMore') && !this.get('loading')) { - this.set('loading', true); - var self = this; - Discourse.NotificationContainer.loadHistory( - self.get('model.lastObject.created_at'), - self.get('user.username')).then(function(result) { - self.set('loading', false); - var notifications = result.get('content'); - self.pushObjects(notifications); - // Stop trying if it's the end - if (notifications && (notifications.length === 0 || notifications.length < 60)) { - self.set('canLoadMore', false); - } - }).catch(function(error) { - self.set('loading', false); - Em.Logger.error(error); - }); - } + this.get('model').loadMore(); } } }); diff --git a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 index 5bab37f838..88b8b98c8c 100644 --- a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 @@ -7,8 +7,8 @@ export default ObjectController.extend({ showParticipants: false, _showFooter: function() { - this.set("controllers.application.showFooter", !this.get("canLoadMore")); - }.observes("canLoadMore"), + this.set("controllers.application.showFooter", !this.get("model.canLoadMore")); + }.observes("model.canLoadMore"), actions: { loadMore: function() { diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 4a0e666cfa..472bd3fc82 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -3,7 +3,9 @@ import CanCheckEmails from 'discourse/mixins/can-check-emails'; export default ObjectController.extend(CanCheckEmails, { indexStream: false, - needs: ['user-notifications', 'user_topics_list'], + pmView: false, + userActionType: null, + needs: ['user-notifications', 'user-topics-list'], viewingSelf: function() { return this.get('content.username') === Discourse.User.currentProp('username'); @@ -12,12 +14,16 @@ export default ObjectController.extend(CanCheckEmails, { collapsedInfo: Em.computed.not('indexStream'), websiteName: function() { - var website = this.get('website'); + var website = this.get('model.website'); if (Em.isEmpty(website)) { return; } - return this.get('website').split("/")[2]; - }.property('website'), + return website.split("/")[2]; + }.property('model.website'), - linkWebsite: Em.computed.not('isBasic'), + linkWebsite: Em.computed.not('model.isBasic'), + + removeNoFollow: function() { + return this.get('model.trust_level') > 2 && !this.siteSettings.tl3_links_no_follow; + }.property('model.trust_level'), canSeePrivateMessages: Ember.computed.or('viewingSelf', 'currentUser.admin'), canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'), @@ -36,13 +42,13 @@ export default ObjectController.extend(CanCheckEmails, { }.property(), canDeleteUser: function() { - return this.get('can_be_deleted') && this.get('can_delete_all_posts'); - }.property('can_be_deleted', 'can_delete_all_posts'), + return this.get('model.can_be_deleted') && this.get('model.can_delete_all_posts'); + }.property('model.can_be_deleted', 'model.can_delete_all_posts'), publicUserFields: function() { var siteUserFields = this.site.get('user_fields'); if (!Ember.isEmpty(siteUserFields)) { - var userFields = this.get('user_fields'); + var userFields = this.get('model.user_fields'); return siteUserFields.filterProperty('show_on_profile', true).sortBy('id').map(function(uf) { var val = userFields ? userFields[uf.get('id').toString()] : null; if (Ember.isEmpty(val)) { @@ -52,7 +58,7 @@ export default ObjectController.extend(CanCheckEmails, { } }).compact(); } - }.property('user_fields.@each.value'), + }.property('model.user_fields.@each.value'), privateMessagesActive: Em.computed.equal('pmView', 'index'), privateMessagesMineActive: Em.computed.equal('pmView', 'mine'), diff --git a/app/assets/javascripts/discourse/helpers/border-color.js.es6 b/app/assets/javascripts/discourse/helpers/border-color.js.es6 new file mode 100644 index 0000000000..eda61b6aed --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/border-color.js.es6 @@ -0,0 +1,4 @@ +export default Ember.Handlebars.makeBoundHelper(function(value) { + return ("border-color: #" + value).htmlSafe(); +}); + diff --git a/app/assets/javascripts/discourse/helpers/count-i18n.js.es6 b/app/assets/javascripts/discourse/helpers/count-i18n.js.es6 deleted file mode 100644 index 85fb9bc46d..0000000000 --- a/app/assets/javascripts/discourse/helpers/count-i18n.js.es6 +++ /dev/null @@ -1,17 +0,0 @@ -/** - Set up an i18n binding that will update as a count changes, complete with pluralization. - - @method countI18n - @for Handlebars -**/ -Ember.Handlebars.registerHelper('countI18n', function(key, options) { - var view = Discourse.View.extend(Discourse.StringBuffer, { - tagName: 'span', - rerenderTriggers: ['count', 'suffix'], - - renderString: function(buffer) { - buffer.push(I18n.t(key + (this.get('suffix') || ''), { count: this.get('count') })); - } - }); - return Ember.Handlebars.helpers.view.call(this, view, options); -}); diff --git a/app/assets/javascripts/discourse/helpers/custom-html.js.es6 b/app/assets/javascripts/discourse/helpers/custom-html.js.es6 index 42155096de..7929888dc0 100644 --- a/app/assets/javascripts/discourse/helpers/custom-html.js.es6 +++ b/app/assets/javascripts/discourse/helpers/custom-html.js.es6 @@ -1,9 +1,11 @@ -Handlebars.registerHelper('custom-html', function(name, contextString, options) { - var html = Discourse.HTML.getCustomHTML(name); +Ember.HTMLBars._registerHelper('custom-html', function(params, hash, options, env) { + const name = params[0]; + const html = Discourse.HTML.getCustomHTML(name); if (html) { return html; } - var container = (options || contextString).data.view.container; + const contextString = params[1]; + const container = (env || contextString).data.view.container; if (container.lookup('template:' + name)) { - return Ember.Handlebars.helpers.partial.apply(this, arguments); + return env.helpers.partial.helperFunction.apply(this, arguments); } }); diff --git a/app/assets/javascripts/discourse/helpers/loading-spinner.es6 b/app/assets/javascripts/discourse/helpers/loading-spinner.es6 index 4d0cdb7af2..86ffcc89f1 100644 --- a/app/assets/javascripts/discourse/helpers/loading-spinner.es6 +++ b/app/assets/javascripts/discourse/helpers/loading-spinner.es6 @@ -1,5 +1,3 @@ -import ConditionalLoadingSpinner from 'discourse/components/conditional-loading-spinner'; - function renderSpinner(cssClass) { var html = "
1) ? Ember.ContainerView : childViews[0]; delete options.fn; // we don't need the default template since we have a connector - Ember.Handlebars.helpers.view.call(this, viewClass, options); + env.helpers.view.helperFunction.call(this, [viewClass], hash, options, env); - const cvs = options.data.view._childViews; + const cvs = env.data.view._childViews; if (childViews.length > 1 && cvs && cvs.length) { const inserted = cvs[cvs.length-1]; if (inserted) { @@ -121,16 +123,5 @@ export default function(connectionName, options) { }); } } - } else if (options.fn) { - // If a block is passed, render its content. - return Ember.Handlebars.helpers.view.call(this, - Ember.View.extend({ - isVirtual: true, - tagName: '', - template: function() { - return options.hash.template; - }.property() - }), - options); } -} +}); diff --git a/app/assets/javascripts/discourse/helpers/register-unbound.js.es6 b/app/assets/javascripts/discourse/helpers/register-unbound.js.es6 index 282cb4e0e5..86e0969040 100644 --- a/app/assets/javascripts/discourse/helpers/register-unbound.js.es6 +++ b/app/assets/javascripts/discourse/helpers/register-unbound.js.es6 @@ -22,11 +22,14 @@ function resolveParams(ctx, options) { } export default function registerUnbound(name, fn) { - Handlebars.registerHelper(name, function(property, options) { + const func = function(property, options) { if (options.types && options.types[0] === "ID") { property = get(this, property, options); } return fn.call(this, property, resolveParams(this, options)); - }); + }; + + Handlebars.registerHelper(name, func); + Ember.Handlebars.registerHelper(name, func); } diff --git a/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 b/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 new file mode 100644 index 0000000000..f446f60c28 --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 @@ -0,0 +1,7 @@ +import { applyFlaggedProperties } from 'discourse/controllers/header'; + +export default { + name: 'apply-flagged-properties', + after: 'register-discourse-location', + initialize: applyFlaggedProperties +}; diff --git a/app/assets/javascripts/discourse/initializers/banner.js.es6 b/app/assets/javascripts/discourse/initializers/banner.js.es6 index d4a1cb6f7e..a7dd7b61ef 100644 --- a/app/assets/javascripts/discourse/initializers/banner.js.es6 +++ b/app/assets/javascripts/discourse/initializers/banner.js.es6 @@ -3,6 +3,7 @@ export default { after: "message-bus", initialize(container) { + const banner = Em.Object.create(PreloadStore.get("banner")), site = container.lookup('site:main'); diff --git a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 index 66592eb59d..2a3eded1dd 100644 --- a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 @@ -38,13 +38,13 @@ export default { app.register('session:main', Session.current(), { instantiate: false }); injectAll(app, 'session'); + app.register('store:main', Store); + inject(app, 'store', 'route', 'controller'); + app.register('current-user:main', Discourse.User.current(), { instantiate: false }); inject(app, 'currentUser', 'component', 'route', 'controller'); app.register('message-bus:main', window.MessageBus, { instantiate: false }); inject(app, 'messageBus', 'route', 'controller', 'view'); - - app.register('store:main', Store); - inject(app, 'store', 'route', 'controller'); } }; diff --git a/app/assets/javascripts/discourse/initializers/map-routes.js.es6 b/app/assets/javascripts/discourse/initializers/map-routes.js.es6 new file mode 100644 index 0000000000..e83537569e --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/map-routes.js.es6 @@ -0,0 +1,17 @@ +import { mapRoutes } from 'discourse/router'; + +export default { + name: "map-routes", + after: 'inject-objects', + + initialize(container, app) { + app.register('router:main', mapRoutes()); + + // HACK to fix: https://github.com/emberjs/ember.js/issues/10310 + const originalBuildInstance = originalBuildInstance || Ember.Application.prototype.buildInstance; + Ember.Application.prototype.buildInstance = function() { + this.registry = this.buildRegistry(); + return originalBuildInstance.apply(this); + }; + } +}; diff --git a/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 b/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 index e3ae8551ad..2e17369755 100644 --- a/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 +++ b/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 @@ -1,8 +1,10 @@ +import DiscourseLocation from 'discourse/lib/discourse-location'; + export default { name: "register-discourse-location", after: 'inject-objects', initialize: function(container, application) { - application.register('location:discourse-location', Ember.DiscourseLocation); + application.register('location:discourse-location', DiscourseLocation); } }; diff --git a/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6 b/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6 index e48f4a9d12..803717002c 100644 --- a/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6 +++ b/app/assets/javascripts/discourse/initializers/sharing-sources.js.es6 @@ -19,12 +19,14 @@ export default { return "http://twitter.com/intent/tweet?url=" + encodeURIComponent(link) + "&text=" + encodeURIComponent(title); }, shouldOpenInPopup: true, + title: I18n.t('share.twitter'), popupHeight: 265 }); Sharing.addSource({ id: 'facebook', faIcon: 'fa-facebook-square', + title: I18n.t('share.facebook'), generateUrl: function(link, title) { return "http://www.facebook.com/sharer.php?u=" + encodeURIComponent(link) + '&t=' + encodeURIComponent(title); }, @@ -34,6 +36,7 @@ export default { Sharing.addSource({ id: 'google+', faIcon: 'fa-google-plus-square', + title: I18n.t('share.google+'), generateUrl: function(link) { return "https://plus.google.com/share?url=" + encodeURIComponent(link); }, @@ -44,6 +47,7 @@ export default { Sharing.addSource({ id: 'email', faIcon: 'fa-envelope-square', + title: I18n.t('share.email'), generateUrl: function(link, title) { return "mailto:?to=&subject=" + encodeURIComponent('[' + Discourse.SiteSettings.title + '] ' + title) + "&body=" + encodeURIComponent(link); } diff --git a/app/assets/javascripts/discourse/initializers/view-helpers.js.es6 b/app/assets/javascripts/discourse/initializers/view-helpers.js.es6 deleted file mode 100644 index 3ef954caa9..0000000000 --- a/app/assets/javascripts/discourse/initializers/view-helpers.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -var helpers = ['input-tip', - 'category-chooser', - 'combo-box', - 'choose-topic', - 'activity-filter']; - -/** - Creates view helpers for some views. Many of these should probably be converted - into components in the long term as it's a better fit. -**/ -export default { - name: 'view-hlpers', - initialize: function(container) { - helpers.forEach(function(h) { - Ember.Handlebars.registerHelper(h, function(options) { - var helper = container.lookupFactory('view:' + h), - hash = options.hash, - types = options.hashTypes; - - Discourse.Utilities.normalizeHash(hash, types); - return Ember.Handlebars.helpers.view.call(this, helper, options); - }); - }); - } -}; diff --git a/app/assets/javascripts/discourse/lib/Markdown.Editor.js b/app/assets/javascripts/discourse/lib/Markdown.Editor.js index 3a45faaae7..8b680db6aa 100644 --- a/app/assets/javascripts/discourse/lib/Markdown.Editor.js +++ b/app/assets/javascripts/discourse/lib/Markdown.Editor.js @@ -1403,6 +1403,10 @@ xPosition += 25; button.id = id + postfix; button.title = title; + // we really should just use jquery here + if (button.setAttribute) { + button.setAttribute('aria-label', title); + } if (textOp) button.textOp = textOp; setupButton(button, true); diff --git a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 index f1a34add67..567fc2e45c 100644 --- a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 +++ b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 @@ -7,20 +7,33 @@ function extractError(error) { Ember.Logger.error(error); } - let parsedError; - if (error.responseText) { + if (error.jqXHR) { + error = error.jqXHR; + } + + let parsedError, parsedJSON; + + if (error.responseJSON) { + parsedJSON = error.responseJSON; + } + + if (!parsedJSON && error.responseText) { try { - const parsedJSON = $.parseJSON(error.responseText); - if (parsedJSON.errors) { - parsedError = parsedJSON.errors[0]; - } else if (parsedJSON.failed) { - parsedError = parsedJSON.message; - } + parsedJSON = $.parseJSON(error.responseText); } catch(ex) { // in case the JSON doesn't parse Ember.Logger.error(ex.stack); } } + + if (parsedJSON) { + if (parsedJSON.errors && parsedJSON.errors.length > 0) { + parsedError = parsedJSON.errors[0]; + } else if (parsedJSON.failed) { + parsedError = parsedJSON.message; + } + } + return parsedError || I18n.t('generic_error'); } @@ -28,11 +41,10 @@ export function throwAjaxError(undoCallback) { return function(error) { // If we provided an `undo` callback if (undoCallback) { undoCallback(error); } - throw extractError(error); }; } -export function popupAjaxError(err) { - bootbox.alert(extractError(err)); +export function popupAjaxError(error) { + bootbox.alert(extractError(error)); } diff --git a/app/assets/javascripts/discourse/routes/discourse_location.js b/app/assets/javascripts/discourse/lib/discourse-location.js.es6 similarity index 92% rename from app/assets/javascripts/discourse/routes/discourse_location.js rename to app/assets/javascripts/discourse/lib/discourse-location.js.es6 index 22fcc3c80c..41a415fcdd 100644 --- a/app/assets/javascripts/discourse/routes/discourse_location.js +++ b/app/assets/javascripts/discourse/lib/discourse-location.js.es6 @@ -1,12 +1,14 @@ +import CloakedCollectionView from 'discourse/views/cloaked-collection'; + /** @module Discourse */ -var get = Ember.get, set = Ember.set; -var popstateFired = false; -var supportsHistoryState = window.history && 'state' in window.history; +const get = Ember.get, set = Ember.set; +let popstateFired = false; +const supportsHistoryState = window.history && 'state' in window.history; -var popstateCallbacks = []; +const popstateCallbacks = []; /** `Ember.DiscourseLocation` implements the location API using the browser's @@ -16,7 +18,7 @@ var popstateCallbacks = []; @namespace Discourse @extends Ember.Object */ -Ember.DiscourseLocation = Ember.Object.extend({ +const DiscourseLocation = Ember.Object.extend({ init: function() { set(this, 'location', get(this, 'location') || window.location); @@ -226,7 +228,7 @@ Ember.DiscourseLocation = Ember.Object.extend({ eject itself when the popState occurs. This results in better back button behavior. **/ -Ember.CloakedCollectionView.reopen({ +CloakedCollectionView.reopen({ _watchForPopState: function() { var self = this, cb = function() { @@ -241,7 +243,7 @@ Ember.CloakedCollectionView.reopen({ // topic_route deactivate $('.posts,#topic-title').hide(); self.cleanUp(); - self.set('controller.postStream.loaded', false); + self.set('controller.model.postStream.loaded', false); }; this.set('_callback', cb); popstateCallbacks.addObject(cb); @@ -252,3 +254,5 @@ Ember.CloakedCollectionView.reopen({ this.set('_callback', null); }.on('willDestroyElement') }); + +export default DiscourseLocation; diff --git a/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js b/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js index 293dcfcf5b..d6e85b19a7 100644 --- a/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js +++ b/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js @@ -22,6 +22,8 @@ RawHandlebars.helpers.get = function(context, options){ var firstContext = options.contexts[0]; var val = firstContext[context]; + + if (val && val.isDescriptor) { return Em.get(firstContext, context); } val = val === undefined ? Em.get(firstContext, context): val; return val; }; diff --git a/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js b/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js index 893f34b98c..16ce57a554 100644 --- a/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js +++ b/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js @@ -196,7 +196,7 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ var selectedPostId = parseInt($('.topic-post.selected article.boxed').data('post-id'), 10); if (selectedPostId) { var topicController = container.lookup('controller:topic'), - post = topicController.get('postStream.posts').findBy('id', selectedPostId); + post = topicController.get('model.postStream.posts').findBy('id', selectedPostId); if (post) { topicController.send(action, post); } diff --git a/app/assets/javascripts/discourse/lib/url.js b/app/assets/javascripts/discourse/lib/url.js index 7c58c1be80..25f6bb32fc 100644 --- a/app/assets/javascripts/discourse/lib/url.js +++ b/app/assets/javascripts/discourse/lib/url.js @@ -221,7 +221,7 @@ Discourse.URL = Ember.Object.createWithMixins({ var container = Discourse.__container__, topicController = container.lookup('controller:topic'), opts = {}, - postStream = topicController.get('postStream'); + postStream = topicController.get('model.postStream'); if (newMatches[3]) opts.nearPost = newMatches[3]; if (path.match(/last$/)) { opts.nearPost = topicController.get('highest_post_number'); } @@ -295,7 +295,7 @@ Discourse.URL = Ember.Object.createWithMixins({ **/ router: function() { return Discourse.__container__.lookup('router:main'); - }.property(), + }.property().volatile(), /** @private diff --git a/app/assets/javascripts/discourse/mixins/add-category-class.js.es6 b/app/assets/javascripts/discourse/mixins/add-category-class.js.es6 index d0226ff7a0..fb9c504bff 100644 --- a/app/assets/javascripts/discourse/mixins/add-category-class.js.es6 +++ b/app/assets/javascripts/discourse/mixins/add-category-class.js.es6 @@ -15,7 +15,7 @@ export default { if (categoryFullSlug) { $('body').addClass('category-' + categoryFullSlug); } - }.observes('categoryFullSlug'), + }.observes('categoryFullSlug').on('init'), _leaveView: function() { this._removeClasses(); }.on('willDestroyElement') }; diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index c69c8ef051..a659ed57be 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -60,7 +60,7 @@ Discourse.Ajax = Em.Mixin.create({ Ember.run(null, resolve, data); }; - args.error = function(xhr, textStatus) { + args.error = function(xhr, textStatus, errorThrown) { // note: for bad CSRF we don't loop an extra request right away. // this allows us to eliminate the possibility of having a loop. if (xhr.status === 403 && xhr.responseText === "['BAD CSRF']") { @@ -74,7 +74,11 @@ Discourse.Ajax = Em.Mixin.create({ xhr.jqTextStatus = textStatus; xhr.requestedUrl = url; - Ember.run(null, reject, xhr); + Ember.run(null, reject, { + jqXHR: xhr, + textStatus: textStatus, + errorThrown: errorThrown + }); }; // We default to JSON on GET. If we don't, sometimes if the server doesn't return the proper header diff --git a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 index 2fb98297f8..67378e6c42 100644 --- a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 +++ b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 @@ -1,6 +1,6 @@ export default Ember.Mixin.create({ - isOwnEmail: Discourse.computed.propertyEqual("id", "currentUser.id"), - showEmailOnProfile: Discourse.computed.setting("show_email_on_profile"), + isOwnEmail: Discourse.computed.propertyEqual("model.id", "currentUser.id"), + showEmailOnProfile: Discourse.computed.setting("model.show_email_on_profile"), canStaffCheckEmails: Em.computed.and("showEmailOnProfile", "currentUser.staff"), canAdminCheckEmails: Em.computed.alias("currentUser.admin"), canCheckEmails: Em.computed.or("isOwnEmail", "canStaffCheckEmails", "canAdminCheckEmails"), diff --git a/app/assets/javascripts/discourse/mixins/load-more.js.es6 b/app/assets/javascripts/discourse/mixins/load-more.js.es6 index 136c5aeca4..0726b13bd7 100644 --- a/app/assets/javascripts/discourse/mixins/load-more.js.es6 +++ b/app/assets/javascripts/discourse/mixins/load-more.js.es6 @@ -1,22 +1,15 @@ -/** - Provides the ability to load more items for a view which is scrolled to the bottom. -**/ - +// Provides the ability to load more items for a view which is scrolled to the bottom. export default Em.Mixin.create(Ember.ViewTargetActionSupport, Discourse.Scrolling, { scrolled: function() { - var eyeline = this.get('eyeline'); + const eyeline = this.get('eyeline'); if (eyeline) { eyeline.update(); } }, _bindEyeline: function() { - var eyeline = new Discourse.Eyeline(this.get('eyelineSelector') + ":last"); + const eyeline = new Discourse.Eyeline(this.get('eyelineSelector') + ":last"); this.set('eyeline', eyeline); - - var self = this; - eyeline.on('sawBottom', function() { - self.send('loadMore'); - }); + eyeline.on('sawBottom', () => this.send('loadMore')); this.bindScrolling(); }.on('didInsertElement'), diff --git a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 index 83b0bce41c..e73b685d4e 100644 --- a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 +++ b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 @@ -1,4 +1,6 @@ export default Em.Mixin.create({ + flashMessage: null, + needs: ['modal'], flash: function(message, messageClass) { diff --git a/app/assets/javascripts/discourse/mixins/open-composer.js.es6 b/app/assets/javascripts/discourse/mixins/open-composer.js.es6 new file mode 100644 index 0000000000..fc4f9c784c --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/open-composer.js.es6 @@ -0,0 +1,26 @@ +// This mixin allows a route to open the composer + +export default Ember.Mixin.create({ + + openComposer(controller) { + this.controllerFor('composer').open({ + categoryId: controller.get('category.id'), + action: Discourse.Composer.CREATE_TOPIC, + draftKey: controller.get('model.draft_key'), + draftSequence: controller.get('model.draft_sequence') + }); + }, + + openComposerWithParams(controller, topicTitle, topicBody, topicCategoryId, topicCategory) { + this.controllerFor('composer').open({ + action: Discourse.Composer.CREATE_TOPIC, + topicTitle, + topicBody, + topicCategoryId, + topicCategory, + draftKey: controller.get('draft_key'), + draftSequence: controller.get('draft_sequence') + }); + } + +}); diff --git a/app/assets/javascripts/discourse/mixins/open_composer.js b/app/assets/javascripts/discourse/mixins/open_composer.js deleted file mode 100644 index 64a79f7965..0000000000 --- a/app/assets/javascripts/discourse/mixins/open_composer.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - This mixin allows a route to open the composer - - @class Discourse.OpenComposer - @extends Ember.Mixin - @namespace Discourse - @module Discourse -**/ -Discourse.OpenComposer = Em.Mixin.create({ - - openComposer: function(controller) { - this.controllerFor('composer').open({ - categoryId: controller.get('category.id'), - action: Discourse.Composer.CREATE_TOPIC, - draftKey: controller.get('draft_key'), - draftSequence: controller.get('draft_sequence') - }); - }, - - openComposerWithParams: function(controller, title, body, category_id) { - this.controllerFor('composer').open({ - action: Discourse.Composer.CREATE_TOPIC, - topicTitle: title, - topicBody: body, - topicCategoryId: category_id, - draftKey: controller.get('draft_key'), - draftSequence: controller.get('draft_sequence') - }); - } - -}); diff --git a/app/assets/javascripts/discourse/mixins/selected_posts_count.js b/app/assets/javascripts/discourse/mixins/selected-posts-count.js.es6 similarity index 52% rename from app/assets/javascripts/discourse/mixins/selected_posts_count.js rename to app/assets/javascripts/discourse/mixins/selected-posts-count.js.es6 index 66be20271e..31fd617b7e 100644 --- a/app/assets/javascripts/discourse/mixins/selected_posts_count.js +++ b/app/assets/javascripts/discourse/mixins/selected-posts-count.js.es6 @@ -1,15 +1,9 @@ -/** - This mixin allows a modal to list a selected posts count nicely. - - @class Discourse.SelectedPostsCount - @extends Ember.Mixin - @namespace Discourse - @module Discourse -**/ -Discourse.SelectedPostsCount = Em.Mixin.create({ +export default Em.Mixin.create({ selectedPostsCount: function() { - if (this.get('allPostsSelected')) return this.get('posts_count') || this.get('topic.posts_count'); + if (this.get('allPostsSelected')) { + return this.get('model.posts_count') || this.get('topic.posts_count') || this.get('posts_count'); + } var sum = this.get('selectedPosts.length') || 0; if (this.get('selectedReplies')) { @@ -21,25 +15,18 @@ Discourse.SelectedPostsCount = Em.Mixin.create({ return sum; }.property('selectedPosts.length', 'allPostsSelected', 'selectedReplies.length'), - /** - The username that owns every selected post, or undefined if no selection or if - ownership is mixed. - - @returns {String|undefined} username that owns all selected posts - **/ + // The username that owns every selected post, or undefined if no selection or if ownership is mixed. selectedPostsUsername: function() { // Don't proceed if replies are selected or usernames are mixed // Changing ownership in those cases normally doesn't make sense - if (this.get('selectedReplies') && this.get('selectedReplies').length > 0) return; - if (this.get('selectedPosts').length <= 0) return; + if (this.get('selectedReplies') && this.get('selectedReplies').length > 0) { return; } + if (this.get('selectedPosts').length <= 0) { return; } - var selectedPosts = this.get('selectedPosts'), - username = selectedPosts[0].username; + const selectedPosts = this.get('selectedPosts'), + username = selectedPosts[0].username; if (selectedPosts.every(function(post) { return post.username === username; })) { return username; } }.property('selectedPosts.length', 'selectedReplies.length') }); - - diff --git a/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 b/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 index 469873d22f..ee40bf5ec0 100644 --- a/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 +++ b/app/assets/javascripts/discourse/mixins/string-buffer.js.es6 @@ -19,9 +19,13 @@ export default Ember.Mixin.create({ }, _rerenderString() { + const $sel = this.$(); + if (!$sel) { return; } + const buffer = []; this.renderString(buffer); - this.$().html(buffer.join('')); + + $sel.html(buffer.join('')); }, rerenderString() { diff --git a/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6 b/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6 new file mode 100644 index 0000000000..35084dcf1f --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6 @@ -0,0 +1,6 @@ +export default { + viewingActionType(userActionType) { + this.controllerFor('user').set('userActionType', userActionType); + this.controllerFor('user-activity').set('userActionType', userActionType); + } +}; diff --git a/app/assets/javascripts/discourse/models/notification.js b/app/assets/javascripts/discourse/models/notification.js deleted file mode 100644 index 469a1f2db0..0000000000 --- a/app/assets/javascripts/discourse/models/notification.js +++ /dev/null @@ -1,54 +0,0 @@ -Discourse.NotificationContainer = Ember.ArrayProxy.extend({ - -}); - -Discourse.NotificationContainer.reopenClass({ - - createFromJson: function(json_array) { - return Discourse.NotificationContainer.create({content: json_array}); - }, - - createFromError: function(error) { - return Discourse.NotificationContainer.create({ - content: [], - error: true, - forbidden: error.status === 403 - }); - }, - - loadRecent: function() { - // TODO - add .json (breaks tests atm) - return Discourse.ajax('/notifications').then(function(result) { - return Discourse.NotificationContainer.createFromJson(result); - }).catch(function(error) { - // TODO HeaderController can't handle a createFromError - // just throw for now - throw error; - }); - }, - - loadHistory: function(beforeDate, username) { - var url = '/notifications/history.json', - params = [ - beforeDate ? ('before=' + beforeDate) : null, - username ? ('user=' + username) : null - ]; - - // Remove nulls - params = params.filter(function(param) { return !!param; }); - // Build URL - params.forEach(function(param, idx) { - url = url + (idx === 0 ? '?' : '&') + param; - }); - - return Discourse.ajax(url).then(function(result) { - return Discourse.NotificationContainer.createFromJson(result); - }).catch(function(error) { - return Discourse.NotificationContainer.createFromError(error); - }); - }, - - resetNew: function() { - return Discourse.ajax("/notifications/reset-new", {type: 'PUT'}); - } -}); diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 532cc6a991..c96a4f9e31 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -707,7 +707,7 @@ const PostStream = RestModel.extend({ const status = result.status; const topic = this.get('topic'); - topic.set('loadingFilter', false); + this.set('loadingFilter', false); topic.set('errorLoading', true); // If the result was 404 the post is not found diff --git a/app/assets/javascripts/discourse/models/result-set.js.es6 b/app/assets/javascripts/discourse/models/result-set.js.es6 index cda174b609..754fdadd20 100644 --- a/app/assets/javascripts/discourse/models/result-set.js.es6 +++ b/app/assets/javascripts/discourse/models/result-set.js.es6 @@ -4,6 +4,10 @@ export default Ember.ArrayProxy.extend({ totalRows: 0, refreshing: false, + canLoadMore: function() { + return this.get('length') < this.get('totalRows'); + }.property('totalRows', 'length'), + loadMore() { const loadMoreUrl = this.get('loadMoreUrl'); if (!loadMoreUrl) { return; } diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6 index c4d77f0504..7f158b924a 100644 --- a/app/assets/javascripts/discourse/models/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-list.js.es6 @@ -41,7 +41,7 @@ const TopicList = RestModel.extend({ refreshSort: function(order, ascending) { const self = this, - params = this.get('params'); + params = this.get('params') || {}; params.order = order || params.order; diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index abd3ef443f..9823080304 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -1,6 +1,9 @@ import RestModel from 'discourse/models/rest'; const Topic = RestModel.extend({ + message: null, + errorTitle: null, + errorLoading: false, // returns createdAt if there's no bumped date bumpedAt: function() { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 4fc9d1a8f5..d03ca5b000 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -434,16 +434,13 @@ User.reopenClass(Discourse.Singleton, { return user.findDetails(options); }, - /** - The current singleton will retrieve its attributes from the `PreloadStore` - if it exists. Otherwise, no instance is created. - - @method createCurrent - @returns {Discourse.User} the user, if logged in. - **/ + // TODO: Use app.register and junk Discourse.Singleton createCurrent: function() { var userJson = PreloadStore.get('currentUser'); - if (userJson) { return Discourse.User.create(userJson); } + if (userJson) { + const store = Discourse.__container__.lookup('store:main'); + return store.createRecord('user', userJson); + } return null; }, diff --git a/app/assets/javascripts/discourse/router.js.es6 b/app/assets/javascripts/discourse/router.js.es6 new file mode 100644 index 0000000000..fc2a6f879c --- /dev/null +++ b/app/assets/javascripts/discourse/router.js.es6 @@ -0,0 +1,83 @@ +const rootURL = Discourse.BaseUri && Discourse.BaseUri !== "/" ? Discourse.BaseUri : undefined; + +const BareRouter = Ember.Router.extend({ + rootURL, + location: Ember.testing ? 'none': 'discourse-location' +}); + +export function mapRoutes() { + + var Router = BareRouter.extend(); + const resources = {}; + const paths = {}; + + // If a module is defined as `route-map` in discourse or a plugin, its routes + // will be built automatically. You can supply a `resource` property to + // automatically put it in that resource, such as `admin`. That way plugins + // can define admin routes. + Ember.keys(requirejs._eak_seen).forEach(function(key) { + if (/route-map$/.test(key)) { + var module = require(key, null, null, true); + if (!module || !module.default) { throw new Error(key + ' must export a route map.'); } + + var mapObj = module.default; + if (typeof mapObj === 'function') { + mapObj = { resource: 'root', map: mapObj }; + } + + if (!resources[mapObj.resource]) { resources[mapObj.resource] = []; } + resources[mapObj.resource].push(mapObj.map); + if (mapObj.path) { paths[mapObj.resource] = mapObj.path; } + } + }); + + return Router.map(function() { + var router = this; + + // Do the root resources first + if (resources.root) { + resources.root.forEach(function(m) { + m.call(router); + }); + delete resources.root; + } + + // Even if no plugins set it up, we need an `adminPlugins` route + var adminPlugins = 'admin.adminPlugins'; + resources[adminPlugins] = resources[adminPlugins] || [Ember.K]; + paths[adminPlugins] = paths[adminPlugins] || "/plugins"; + + var segments = {}, + standalone = []; + + Object.keys(resources).forEach(function(r) { + var m = /^([^\.]+)\.(.*)$/.exec(r); + if (m) { + segments[m[1]] = m[2]; + } else { + standalone.push(r); + } + }); + + // Apply other resources next. A little hacky but works! + standalone.forEach(function(r) { + router.resource(r, {path: paths[r]}, function() { + var res = this; + resources[r].forEach(function(m) { m.call(res); }); + + var s = segments[r]; + if (s) { + var full = r + '.' + s; + res.resource(s, {path: paths[full]}, function() { + var nestedRes = this; + resources[full].forEach(function(m) { m.call(nestedRes); }); + }); + } + }); + }); + + this.route('unknown', {path: '*path'}); + }); +} + +export default BareRouter; diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index 4e6a61e1c0..17d5d824e9 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -1,4 +1,5 @@ import showModal from 'discourse/lib/show-modal'; +import OpenComposer from "discourse/mixins/open-composer"; function unlessReadOnly(method) { return function() { @@ -10,7 +11,7 @@ function unlessReadOnly(method) { }; } -const ApplicationRoute = Discourse.Route.extend(Discourse.OpenComposer, { +const ApplicationRoute = Discourse.Route.extend(OpenComposer, { siteTitle: Discourse.computed.setting('title'), @@ -148,8 +149,8 @@ const ApplicationRoute = Discourse.Route.extend(Discourse.OpenComposer, { this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'}); }, - createNewTopicViaParams: function(title, body, category_id) { - this.openComposerWithParams(this.controllerFor('discovery/topics'), title, body, category_id); + createNewTopicViaParams: function(title, body, category_id, category) { + this.openComposerWithParams(this.controllerFor('discovery/topics'), title, body, category_id, category); } }, 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 c8c345e88b..b5a83808d1 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 @@ -20,7 +20,7 @@ export default function (viewName, path) { setupController: function() { this._super.apply(this, arguments); - this.controllerFor('user_topics_list').setProperties({ + this.controllerFor('user-topics-list').setProperties({ hideCategory: true, showParticipants: true }); diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index 1f5bdab25f..62c7b652dc 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -110,106 +110,6 @@ const DiscourseRoute = Ember.Route.extend({ isPoppedState: function(transition) { return (!transition._discourse_intercepted) && (!!transition.intent.url); } - -}); - -var routeBuilder; - -DiscourseRoute.reopenClass({ - - buildRoutes: function(builder) { - var oldBuilder = routeBuilder; - routeBuilder = function() { - if (oldBuilder) oldBuilder.call(this); - return builder.call(this); - }; - }, - - mapRoutes: function() { - var resources = {}, - paths = {}; - - // If a module is defined as `route-map` in discourse or a plugin, its routes - // will be built automatically. You can supply a `resource` property to - // automatically put it in that resource, such as `admin`. That way plugins - // can define admin routes. - Ember.keys(requirejs._eak_seen).forEach(function(key) { - if (/route-map$/.test(key)) { - var module = require(key, null, null, true); - if (!module || !module.default) { throw new Error(key + ' must export a route map.'); } - - var mapObj = module.default; - if (typeof mapObj === 'function') { - mapObj = { resource: 'root', map: mapObj }; - } - - if (!resources[mapObj.resource]) { resources[mapObj.resource] = []; } - resources[mapObj.resource].push(mapObj.map); - if (mapObj.path) { paths[mapObj.resource] = mapObj.path; } - } - }); - - if (Discourse.BaseUri && Discourse.BaseUri !== "/") { - Discourse.Router.reopen({ - rootURL: Discourse.BaseUri + "/" - }); - } - - Discourse.Router.map(function() { - var router = this; - - // Do the root resources first - if (resources.root) { - resources.root.forEach(function(m) { - m.call(router); - }); - delete resources.root; - } - - // Even if no plugins set it up, we need an `adminPlugins` route - var adminPlugins = 'admin.adminPlugins'; - resources[adminPlugins] = resources[adminPlugins] || [Ember.K]; - paths[adminPlugins] = paths[adminPlugins] || "/plugins"; - - var segments = {}, - standalone = []; - - Object.keys(resources).forEach(function(r) { - var m = /^([^\.]+)\.(.*)$/.exec(r); - if (m) { - segments[m[1]] = m[2]; - } else { - standalone.push(r); - } - }); - - // Apply other resources next. A little hacky but works! - standalone.forEach(function(r) { - router.resource(r, {path: paths[r]}, function() { - var res = this; - resources[r].forEach(function(m) { m.call(res); }); - - var s = segments[r]; - if (s) { - var full = r + '.' + s; - res.resource(s, {path: paths[full]}, function() { - var nestedRes = this; - resources[full].forEach(function(m) { m.call(nestedRes); }); - }); - } - }); - }); - - if (routeBuilder) { - Ember.warn("The Discourse `routeBuilder` is deprecated. Export a `route-map` instead"); - routeBuilder.call(router); - } - - - this.route('unknown', {path: '*path'}); - }); - } - }); export default DiscourseRoute; 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 636a646db2..c46b9b8275 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 @@ -1,7 +1,8 @@ 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(Discourse.OpenComposer, ShowFooter, { +Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, ShowFooter, { 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 c12cb9a72c..572b9536bd 100644 --- a/app/assets/javascripts/discourse/routes/discovery.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery.js.es6 @@ -4,8 +4,9 @@ **/ import ShowFooter from "discourse/mixins/show-footer"; +import OpenComposer from "discourse/mixins/open-composer"; -const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, Discourse.OpenComposer, ShowFooter, { +const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, OpenComposer, ShowFooter, { redirect: function() { return this.redirectIfLoginRequired(); }, beforeModel: function(transition) { diff --git a/app/assets/javascripts/discourse/routes/new-topic.js.es6 b/app/assets/javascripts/discourse/routes/new-topic.js.es6 index f09dcae028..4ce26ba279 100644 --- a/app/assets/javascripts/discourse/routes/new-topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-topic.js.es6 @@ -7,7 +7,7 @@ export default Discourse.Route.extend({ if (self.controllerFor('navigation/default').get('canCreateTopic')) { // User can create topic Ember.run.next(function() { - e.send('createNewTopicViaParams', transition.queryParams.title, transition.queryParams.body, transition.queryParams.category_id); + e.send('createNewTopicViaParams', transition.queryParams.title, transition.queryParams.body, transition.queryParams.category_id, transition.queryParams.category); }); } }); diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 index b8f43a433d..040b4bb5f0 100644 --- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 @@ -30,7 +30,7 @@ export default Discourse.Route.extend({ progress = postStream.progressIndexOfPost(closestPost); topicController.setProperties({ - currentPost: closest, + 'model.currentPost': closest, enteredAt: new Date().getTime().toString(), }); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 381692afea..61f660991a 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -182,7 +182,7 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, { this.controllerFor('user-card').set('visible', false); const topicController = this.controllerFor('topic'), - postStream = topicController.get('postStream'); + postStream = topicController.get('model.postStream'); postStream.cancelFilter(); topicController.set('multiSelect', false); 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 7257f7358f..bec825f849 100644 --- a/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-activity-stream.js.es6 @@ -1,6 +1,7 @@ import ShowFooter from "discourse/mixins/show-footer"; +import ViewingActionType from "discourse/mixins/viewing-action-type"; -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend(ShowFooter, ViewingActionType, { model: function() { return this.modelFor('user').get('stream'); }, @@ -15,7 +16,7 @@ export default Discourse.Route.extend(ShowFooter, { setupController: function(controller, model) { controller.set('model', model); - this.controllerFor('user-activity').set('userActionType', this.get('userActionType')); + this.viewingActionType(this.get('userActionType')); }, actions: { diff --git a/app/assets/javascripts/discourse/routes/user-badges.js.es6 b/app/assets/javascripts/discourse/routes/user-badges.js.es6 index 5455257881..d3b39a3b93 100644 --- a/app/assets/javascripts/discourse/routes/user-badges.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-badges.js.es6 @@ -1,14 +1,13 @@ import ShowFooter from "discourse/mixins/show-footer"; +import ViewingActionType from "discourse/mixins/viewing-action-type"; -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend(ShowFooter, ViewingActionType, { model: function() { return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'), {grouped: true}); }, setupController: function(controller, model) { - if (this.controllerFor('user_activity').get('content')) { - this.controllerFor('user_activity').set('userActionType', -1); - } + this.viewingActionType(-1); controller.set('model', model); }, diff --git a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications.js.es6 index d5d942417f..a90c131af2 100644 --- a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-notifications.js.es6 @@ -1,31 +1,22 @@ import ShowFooter from "discourse/mixins/show-footer"; +import ViewingActionType from "discourse/mixins/viewing-action-type"; -export default Discourse.Route.extend(ShowFooter, { +export default Discourse.Route.extend(ShowFooter, ViewingActionType, { actions: { - didTransition: function() { - this.controllerFor("user_notifications")._showFooter(); + didTransition() { + this.controllerFor("user-notifications")._showFooter(); return true; } }, - model: function() { + model() { var user = this.modelFor('user'); - return Discourse.NotificationContainer.loadHistory(undefined, user.get('username')); + return this.store.find('notification', {username: user.get('username')}); }, - setupController: function(controller, model) { + setupController(controller, model) { controller.set('model', model); controller.set('user', this.modelFor('user')); - - if (this.controllerFor('user_activity').get('content')) { - this.controllerFor('user_activity').set('userActionType', -1); - } - - // properly initialize "canLoadMore" - controller.set("canLoadMore", model.get("length") === 60); - }, - - renderTemplate: function() { - this.render('user-notification-history', {into: 'user'}); + this.viewingActionType(-1); } }); diff --git a/app/assets/javascripts/discourse/routes/user-topic-list.js.es6 b/app/assets/javascripts/discourse/routes/user-topic-list.js.es6 index 5455b03e2f..a8cb245fd5 100644 --- a/app/assets/javascripts/discourse/routes/user-topic-list.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-topic-list.js.es6 @@ -1,12 +1,16 @@ -export default Discourse.Route.extend({ - renderTemplate: function() { - this.render('user_topics_list'); +import ViewingActionType from "discourse/mixins/viewing-action-type"; + +export default Discourse.Route.extend(ViewingActionType, { + renderTemplate() { + this.render('user-topics-list'); }, - setupController: function(controller, model) { - this.controllerFor('user-activity').set('userActionType', this.get('userActionType')); + setupController(controller, model) { + const userActionType = this.get('userActionType'); + this.controllerFor('user').set('userActionType', userActionType); + this.controllerFor('user-activity').set('userActionType', userActionType); this.controllerFor('user-topics-list').setProperties({ - model: model, + model, hideCategory: false, showParticipants: false }); diff --git a/app/assets/javascripts/discourse/templates/about.hbs b/app/assets/javascripts/discourse/templates/about.hbs index b555a33486..ca3f050ffe 100644 --- a/app/assets/javascripts/discourse/templates/about.hbs +++ b/app/assets/javascripts/discourse/templates/about.hbs @@ -14,15 +14,15 @@
-

{{i18n 'about.title' title=title}}

-

{{description}}

+

{{i18n 'about.title' title=model.title}}

+

{{model.description}}

- {{#if admins}} + {{#if model.admins}}

{{i18n 'about.our_admins'}}

- {{#each a in admins}} + {{#each a in model.admins}} {{user-small user=a}} {{/each}}
@@ -30,12 +30,12 @@
{{/if}} - {{#if moderators}} + {{#if model.moderators}}

{{i18n 'about.our_moderators'}}

- {{#each m in moderators}} + {{#each m in model.moderators}} {{user-small user=m}} {{/each}}
@@ -55,33 +55,33 @@
- - - + + + - - - + + + - - - + + + - - + + - - - + + +
{{i18n 'about.topic_count'}}{{number stats.topic_count}}{{number stats.topics_7_days}}{{number stats.topics_30_days}}{{number model.stats.topic_count}}{{number model.stats.topics_7_days}}{{number model.stats.topics_30_days}}
{{i18n 'about.post_count'}}{{number stats.post_count}}{{number stats.posts_7_days}}{{number stats.posts_30_days}}{{number model.stats.post_count}}{{number model.stats.posts_7_days}}{{number model.stats.posts_30_days}}
{{i18n 'about.user_count'}}{{number stats.user_count}}{{number stats.users_7_days}}{{number stats.users_30_days}}{{number model.stats.user_count}}{{number model.stats.users_7_days}}{{number model.stats.users_30_days}}
{{i18n 'about.active_user_count'}} {{number stats.active_users_7_days}}{{number stats.active_users_30_days}}{{number model.stats.active_users_7_days}}{{number model.stats.active_users_30_days}}
{{i18n 'about.like_count'}}{{number stats.like_count}}{{number stats.likes_7_days}}{{number stats.likes_30_days}}{{number model.stats.like_count}}{{number model.stats.likes_7_days}}{{number model.stats.likes_30_days}}
diff --git a/app/assets/javascripts/discourse/templates/badges/show.hbs b/app/assets/javascripts/discourse/templates/badges/show.hbs index 98910de960..ed72176c52 100644 --- a/app/assets/javascripts/discourse/templates/badges/show.hbs +++ b/app/assets/javascripts/discourse/templates/badges/show.hbs @@ -1,16 +1,16 @@

{{#link-to 'badges.index'}}{{i18n 'badges.title'}}{{/link-to}} - - {{displayName}} + {{fa-icon "angle-right"}} + {{model.displayName}}

- - - + + + @@ -42,6 +42,6 @@ {{/each}} - {{loading-spinner condition=canLoadMore}} + {{conditional-loading-spinner condition=canLoadMore}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs index 9fa600c9b9..f8afeb5755 100644 --- a/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs +++ b/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs @@ -1,4 +1,4 @@ -{{#loading-spinner condition=loading}} +{{#conditional-loading-spinner condition=loading}} {{#if topics}} {{topic-list showParticipants=showParticipants @@ -9,4 +9,4 @@ {{i18n 'choose_topic.none_found'}} {{/if}} -{{/loading-spinner}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/discourse/templates/components/category-drop.hbs b/app/assets/javascripts/discourse/templates/components/category-drop.hbs index 9e50cf11c5..f997416464 100644 --- a/app/assets/javascripts/discourse/templates/components/category-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-drop.hbs @@ -1,6 +1,6 @@ {{#if category}} - - + + {{#if category.read_restricted}} {{fa-icon "lock"}} {{/if}} @@ -8,15 +8,15 @@ {{else}} {{#if noSubcategories}} - {{i18n 'categories.no_subcategory'}} + {{i18n 'categories.no_subcategory'}} {{else}} - {{allCategoriesLabel}} + {{allCategoriesLabel}} {{/if}} {{/if}} {{#if categories}} - -
+ +
{{#if subCategory}} diff --git a/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs b/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs index 825bcc44be..39b419d6ff 100644 --- a/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs +++ b/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs @@ -1,5 +1,5 @@
-
{{user-badge badge=this}}{{{displayDescriptionHtml}}}{{i18n 'badges.granted' count=grant_count}}{{user-badge badge=model}}{{{model.displayDescriptionHtml}}}{{i18n 'badges.granted' count=model.grant_count}} {{i18n 'badges.allow_title'}} {{{view.allowTitle}}}
{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
@@ -73,7 +73,7 @@
{{/if}} - {{/loading-spinner}} + {{/conditional-loading-spinner}}
{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/share-source.hbs b/app/assets/javascripts/discourse/templates/components/share-source.hbs index aca17c8b79..87248269b7 100644 --- a/app/assets/javascripts/discourse/templates/components/share-source.hbs +++ b/app/assets/javascripts/discourse/templates/components/share-source.hbs @@ -1,4 +1,4 @@ - + {{#if source.faIcon}} {{else}} diff --git a/app/assets/javascripts/discourse/templates/components/user-stat.hbs b/app/assets/javascripts/discourse/templates/components/user-stat.hbs new file mode 100644 index 0000000000..e2e4561516 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/user-stat.hbs @@ -0,0 +1,5 @@ + + {{#if icon}}{{fa-icon icon}}{{/if}} + {{number value}} + +{{i18n label}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 454ff27932..ed94f9f450 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -57,13 +57,13 @@ so I'm going to stop rendering it until we figure out what's up
{{text-field value=model.title tabindex="2" id="reply-title" maxLength=maxTitleLength placeholderKey="composer.title_placeholder"}} - {{popupInputTip validation=view.titleValidation shownAt=view.showTitleTip}} + {{popup-input-tip validation=view.titleValidation shownAt=view.showTitleTip}}
{{#unless model.privateMessage}}
{{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} - {{popupInputTip validation=view.categoryValidation shownAt=view.showCategoryTip}} + {{popup-input-tip validation=view.categoryValidation shownAt=view.showCategoryTip}}
{{#if model.archetype.hasOptions}} @@ -80,11 +80,11 @@ so I'm going to stop rendering it until we figure out what's up
{{composer-text-area tabindex="4" value=model.reply}} - {{popupInputTip validation=view.replyValidation shownAt=view.showReplyTip}} + {{popup-input-tip validation=view.replyValidation shownAt=view.showReplyTip}}
-
+
{{{model.toggleText}}} diff --git a/app/assets/javascripts/discourse/templates/discovery.hbs b/app/assets/javascripts/discourse/templates/discovery.hbs index 1bf0814aaa..be18d07bae 100644 --- a/app/assets/javascripts/discourse/templates/discovery.hbs +++ b/app/assets/javascripts/discourse/templates/discovery.hbs @@ -10,7 +10,7 @@
-{{loading-spinner condition=loading}} +{{conditional-loading-spinner condition=loading}}
diff --git a/app/assets/javascripts/discourse/templates/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/discovery/categories.hbs index 3fdfdafd99..0aa789da06 100644 --- a/app/assets/javascripts/discourse/templates/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/categories.hbs @@ -1,4 +1,4 @@ -{{#if categories}} +{{#if model.categories}}
@@ -9,9 +9,9 @@ - {{#each c in categories}} + {{#each c in model.categories}} - {{/if}} + - {{#each item in model itemController="directory-item"}} - - - - - - - - - - {{#if showTimeRead}} - - {{/if}} + {{#each ic in model itemController="directory-item"}} + + {{#with ic.model as |it|}} + + + + + + + + + {{#if showTimeRead}} + + {{/if}} + {{/with}} {{/each}}
{{user-small user=item.model.user}}{{number item.model.likes_received}}{{number item.model.likes_given}}{{number item.model.topic_count}}{{number item.model.post_count}}{{number item.model.topics_entered}}{{number item.model.posts_read}}{{number item.model.days_visited}}{{unbound item.model.time_read}}
{{user-small user=it.user}}{{number it.likes_received}}{{number it.likes_given}}{{number it.topic_count}}{{number it.post_count}}{{number it.topics_entered}}{{number it.posts_read}}{{number it.days_visited}}{{unbound it.time_read}}
- {{loading-spinner condition=model.loadingMore}} + {{conditional-loading-spinner condition=model.loadingMore}} {{else}}

{{i18n "directory.no_results"}}

{{/if}} - {{/loading-spinner}} + {{/conditional-loading-spinner}}
diff --git a/app/assets/javascripts/discourse/views/bookmark-button.js.es6 b/app/assets/javascripts/discourse/views/bookmark-button.js.es6 index c99f955c8c..4fc63222c0 100644 --- a/app/assets/javascripts/discourse/views/bookmark-button.js.es6 +++ b/app/assets/javascripts/discourse/views/bookmark-button.js.es6 @@ -4,22 +4,24 @@ export default ButtonView.extend({ classNames: ['bookmark'], attributeBindings: ['disabled'], - textKey: function() { - return this.get('controller.bookmarked') ? 'bookmarked.clear_bookmarks' : 'bookmarked.title'; - }.property('controller.bookmarked'), + bookmarked: Ember.computed.alias('controller.model.bookmarked'), - rerenderTriggers: ['controller.bookmarked'], + textKey: function() { + return this.get('bookmarked') ? 'bookmarked.clear_bookmarks' : 'bookmarked.title'; + }.property('bookmarked'), + + rerenderTriggers: ['bookmarked'], helpKey: function() { - return this.get("controller.bookmarked") ? "bookmarked.help.unbookmark" : "bookmarked.help.bookmark"; - }.property("controller.bookmarked"), + return this.get("bookmarked") ? "bookmarked.help.unbookmark" : "bookmarked.help.bookmark"; + }.property("bookmarked"), click: function() { this.get('controller').send('toggleBookmark'); }, renderIcon: function(buffer) { - var className = this.get("controller.bookmarked") ? "bookmarked" : ""; + var className = this.get("bookmarked") ? "bookmarked" : ""; buffer.push(""); } }); diff --git a/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 b/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 new file mode 100644 index 0000000000..03a121efe8 --- /dev/null +++ b/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 @@ -0,0 +1,286 @@ +import CloakedView from 'discourse/views/cloaked'; + +const CloakedCollectionView = Ember.CollectionView.extend({ + cloakView: Ember.computed.alias('itemViewClass'), + topVisible: null, + bottomVisible: null, + offsetFixedTopElement: null, + offsetFixedBottomElement: null, + loadingHTML: 'Loading...', + scrollDebounce: 10, + + init: function() { + const cloakView = this.get('cloakView'), + idProperty = this.get('idProperty'), + uncloakDefault = !!this.get('uncloakDefault'); + + // Set the slack ratio differently to allow for more or less slack in preloading + const slackRatio = parseFloat(this.get('slackRatio')); + if (!slackRatio) { this.set('slackRatio', 1.0); } + + this.set('itemViewClass', CloakedView.extend({ + classNames: [cloakView + '-cloak'], + cloaks: cloakView, + preservesContext: this.get('preservesContext') === 'true', + cloaksController: this.get('itemController'), + defaultHeight: this.get('defaultHeight'), + + init: function() { + this._super(); + + if (idProperty) { + this.set('elementId', cloakView + '-cloak-' + this.get('content.' + idProperty)); + } + if (uncloakDefault) { + this.uncloak(); + } else { + this.cloak(); + } + } + })); + + this._super(); + Ember.run.next(this, 'scrolled'); + }, + + /** + If the topmost visible view changed, we will notify the controller if it has an appropriate hook. + + @method _topVisibleChanged + @observes topVisible + **/ + _topVisibleChanged: function() { + const controller = this.get('controller'); + if (controller.topVisibleChanged) { controller.topVisibleChanged(this.get('topVisible')); } + }.observes('topVisible'), + + /** + If the bottommost visible view changed, we will notify the controller if it has an appropriate hook. + + @method _bottomVisible + @observes bottomVisible + **/ + _bottomVisible: function() { + const controller = this.get('controller'); + if (controller.bottomVisibleChanged) { controller.bottomVisibleChanged(this.get('bottomVisible')); } + }.observes('bottomVisible'), + + /** + Binary search for finding the topmost view on screen. + + @method findTopView + @param {Array} childViews the childViews to search through + @param {Number} windowTop The top of the viewport to search against + @param {Number} min The minimum index to search through of the child views + @param {Number} max The max index to search through of the child views + @returns {Number} the index into childViews of the topmost view + **/ + findTopView(childViews, viewportTop, min, max) { + if (max < min) { return min; } + + const wrapperTop = this.get('wrapperTop')>>0; + + while(max>min){ + const mid = Math.floor((min + max) / 2), + // in case of not full-window scrolling + $view = childViews[mid].$(), + viewBottom = $view.position().top + wrapperTop + $view.height(); + + if (viewBottom > viewportTop) { + max = mid-1; + } else { + min = mid+1; + } + } + + return min; + }, + + + /** + Determine what views are onscreen and cloak/uncloak them as necessary. + + @method scrolled + **/ + scrolled() { + if (!this.get('scrollingEnabled')) { return; } + + const childViews = this.get('childViews'); + if ((!childViews) || (childViews.length === 0)) { return; } + + const self = this, + toUncloak = [], + onscreen = [], + onscreenCloaks = [], + $w = $(window), + windowHeight = this.get('wrapperHeight') || ( window.innerHeight ? window.innerHeight : $w.height() ), + slack = Math.round(windowHeight * this.get('slackRatio')), + offsetFixedTopElement = this.get('offsetFixedTopElement'), + offsetFixedBottomElement = this.get('offsetFixedBottomElement'), + bodyHeight = this.get('wrapperHeight') ? this.$().height() : $('body').height(); + + let windowTop = this.get('wrapperTop') || $w.scrollTop(); + + const viewportTop = windowTop - slack, + topView = this.findTopView(childViews, viewportTop, 0, childViews.length-1); + + let windowBottom = windowTop + windowHeight, + viewportBottom = windowBottom + slack; + if (windowBottom > bodyHeight) { windowBottom = bodyHeight; } + if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; } + + if (offsetFixedTopElement) { + windowTop += (offsetFixedTopElement.outerHeight(true) || 0); + } + + if (offsetFixedBottomElement) { + windowBottom -= (offsetFixedBottomElement.outerHeight(true) || 0); + } + + // Find the bottom view and what's onscreen + let bottomView = topView; + while (bottomView < childViews.length) { + const view = childViews[bottomView], + $view = view.$(); + + if (!$view) { break; } + + // in case of not full-window scrolling + const scrollOffset = this.get('wrapperTop') || 0, + viewTop = $view.offset().top + scrollOffset, + viewBottom = viewTop + $view.height(); + + if (viewTop > viewportBottom) { break; } + toUncloak.push(view); + + if (viewBottom > windowTop && viewTop <= windowBottom) { + onscreen.push(view.get('content')); + onscreenCloaks.push(view); + } + + bottomView++; + } + if (bottomView >= childViews.length) { bottomView = childViews.length - 1; } + + // If our controller has a `sawObjects` method, pass the on screen objects to it. + const controller = this.get('controller'); + if (onscreen.length) { + this.setProperties({topVisible: onscreen[0], bottomVisible: onscreen[onscreen.length-1]}); + if (controller && controller.sawObjects) { + Em.run.schedule('afterRender', function() { + controller.sawObjects(onscreen); + }); + } + } else { + this.setProperties({topVisible: null, bottomVisible: null}); + } + + const toCloak = childViews.slice(0, topView).concat(childViews.slice(bottomView+1)); + + this._uncloak = toUncloak; + if(this._nextUncloak){ + Em.run.cancel(this._nextUncloak); + this._nextUncloak = null; + } + + Em.run.schedule('afterRender', this, function() { + onscreenCloaks.forEach(function (v) { + if(v && v.uncloak) { + v.uncloak(); + } + }); + toCloak.forEach(function (v) { v.cloak(); }); + if (self._nextUncloak) { Em.run.cancel(self._nextUncloak); } + self._nextUncloak = Em.run.later(self, self.uncloakQueue,50); + }); + + for (let j=bottomView; j0){ + const view = this._uncloak.shift(); + if(view && view.uncloak && !view._containedView){ + Em.run.schedule('afterRender', view, view.uncloak); + processed++; + } + } + if(this._uncloak.length === 0){ + this._uncloak = null; + } else { + Em.run.schedule('afterRender', self, function(){ + if(self._nextUncloak){ + Em.run.cancel(self._nextUncloak); + } + self._nextUncloak = Em.run.next(self, function(){ + if(self._nextUncloak){ + Em.run.cancel(self._nextUncloak); + } + self._nextUncloak = Em.run.later(self,self.uncloakQueue,delay); + }); + }); + } + } + }, + + scrollTriggered() { + Em.run.scheduleOnce('afterRender', this, 'scrolled'); + }, + + _startEvents: function() { + if (this.get('offsetFixed')) { + Em.warn("Cloaked-collection's `offsetFixed` is deprecated. Use `offsetFixedTop` instead."); + } + + const self = this, + offsetFixedTop = this.get('offsetFixedTop') || this.get('offsetFixed'), + offsetFixedBottom = this.get('offsetFixedBottom'), + scrollDebounce = this.get('scrollDebounce'), + onScrollMethod = function() { + Ember.run.debounce(self, 'scrollTriggered', scrollDebounce); + }; + + if (offsetFixedTop) { + this.set('offsetFixedTopElement', $(offsetFixedTop)); + } + + if (offsetFixedBottom) { + this.set('offsetFixedBottomElement', $(offsetFixedBottom)); + } + + $(document).bind('touchmove.ember-cloak', onScrollMethod); + $(window).bind('scroll.ember-cloak', onScrollMethod); + this.addObserver('wrapperTop', self, onScrollMethod); + this.addObserver('wrapperHeight', self, onScrollMethod); + this.addObserver('content.@each', self, onScrollMethod); + this.scrollTriggered(); + + this.set('scrollingEnabled', true); + }.on('didInsertElement'), + + cleanUp() { + $(document).unbind('touchmove.ember-cloak'); + $(window).unbind('scroll.ember-cloak'); + this.set('scrollingEnabled', false); + }, + + _endEvents: function() { + this.cleanUp(); + }.on('willDestroyElement') +}); + +Ember.Handlebars.helper('cloaked-collection', Ember.testing ? Ember.CollectionView : CloakedCollectionView); +export default CloakedCollectionView; diff --git a/app/assets/javascripts/discourse/views/cloaked.js.es6 b/app/assets/javascripts/discourse/views/cloaked.js.es6 new file mode 100644 index 0000000000..ddb3a00ae2 --- /dev/null +++ b/app/assets/javascripts/discourse/views/cloaked.js.es6 @@ -0,0 +1,138 @@ +export default Ember.View.extend({ + attributeBindings: ['style'], + _containedView: null, + _scheduled: null, + + init: function() { + this._super(); + this._scheduled = false; + this._childViews = []; + }, + + setContainedView(cv) { + if (this._childViews[0]) { + this._childViews[0].destroy(); + this._childViews[0] = cv; + } + + if (cv) { + cv.set('_parentView', this); + cv.set('templateData', this.get('templateData')); + this._childViews[0] = cv; + } else { + this._childViews.clear(); + } + + if (this._scheduled) return; + this._scheduled = true; + this.set('_containedView', cv); + Ember.run.schedule('render', this, this.updateChildView); + }, + + render(buffer) { + const element = buffer.element(); + const dom = buffer.dom; + + this._childViewsMorph = dom.appendMorph(element); + }, + + updateChildView() { + this._scheduled = false; + if (!this._elementCreated || this.isDestroying || this.isDestroyed) { return; } + + const childView = this._containedView; + if (childView && !childView._elementCreated) { + this._renderer.renderTree(childView, this, 0); + } + }, + + /** + Triggers the set up for rendering a view that is cloaked. + + @method uncloak + */ + uncloak() { + const state = this._state || this.state; + if (state !== 'inDOM' && state !== 'preRender') { return; } + + if (!this._containedView) { + const model = this.get('content'), + container = this.get('container'); + + let controller; + + // Wire up the itemController if necessary + const controllerName = this.get('cloaksController'); + if (controllerName) { + const controllerFullName = 'controller:' + controllerName; + let factory = container.lookupFactory(controllerFullName); + + // let ember generate controller if needed + if (!factory) { + factory = Ember.generateControllerFactory(container, controllerName, model); + + // inform developer about typo + Ember.Logger.warn('ember-cloaking: can\'t lookup controller by name "' + controllerFullName + '".'); + Ember.Logger.warn('ember-cloaking: using ' + factory.toString() + '.'); + } + + const parentController = this.get('controller'); + controller = factory.create({ model, parentController, target: parentController }); + } + + const createArgs = {}, + target = controller || model; + + if (this.get('preservesContext')) { + createArgs.content = target; + } else { + createArgs.context = target; + } + if (controller) { createArgs.controller = controller; } + this.setProperties({ + style: null, + loading: false + }); + + this.setContainedView(this.createChildView(this.get('cloaks'), createArgs)); + } + }, + + /** + Removes the view from the DOM and tears down all observers. + + @method cloak + */ + cloak() { + const self = this; + + if (this._containedView && (this._state || this.state) === 'inDOM') { + const style = 'height: ' + this.$().height() + 'px;'; + this.set('style', style); + this.$().prop('style', style); + + + // We need to remove the container after the height of the element has taken + // effect. + Ember.run.schedule('afterRender', function() { + self.setContainedView(null); + }); + } + }, + + _setHeights: function(){ + if (!this._containedView) { + // setting default height + // but do not touch if height already defined + if(!this.$().height()){ + let defaultHeight = 100; + if(this.get('defaultHeight')) { + defaultHeight = this.get('defaultHeight'); + } + + this.$().css('height', defaultHeight); + } + } + }.on('didInsertElement') +}); + diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 239616e051..5ab6235850 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -36,7 +36,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { }.observes('loading'), postMade: function() { - return this.present('controller.createdPost') ? 'created-post' : null; + return this.present('model.createdPost') ? 'created-post' : null; }.property('model.createdPost'), refreshPreview: Discourse.debounce(function() { @@ -160,7 +160,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { // If we are editing a post, we'll refresh its contents once. This is a feature that // allows a user to refresh its contents once. - if (post && post.blank('refreshedPost')) { + if (post && !post.get('refreshedPost')) { refresh = true; post.set('refreshedPost', true); } @@ -240,7 +240,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { this.editor = editor = Discourse.Markdown.createEditor({ lookupAvatarByPostNumber(postNumber) { - const posts = self.get('controller.controllers.topic.postStream.posts'); + const posts = self.get('controller.controllers.topic.model.postStream.posts'); if (posts) { const quotedPost = posts.findProperty("post_number", postNumber); if (quotedPost) { @@ -564,10 +564,11 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { replyValidation: function() { const replyLength = this.get('model.replyLength'), missingChars = this.get('model.missingReplyCharacters'); + let reason; - if( replyLength < 1 ){ + if (replyLength < 1) { reason = I18n.t('composer.error.post_missing'); - } else if( missingChars > 0 ) { + } else if (missingChars > 0) { reason = I18n.t('composer.error.post_length', {min: this.get('model.minimumPostLength')}); let tl = Discourse.User.currentProp("trust_level"); if (tl === 0 || tl === 1) { @@ -575,8 +576,8 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { } } - if( reason ) { - return Discourse.InputValidation.create({ failed: true, reason: reason }); + if (reason) { + return Discourse.InputValidation.create({ failed: true, reason }); } }.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'), diff --git a/app/assets/javascripts/discourse/views/flag.js.es6 b/app/assets/javascripts/discourse/views/flag.js.es6 index 583452fca4..388735c7c5 100644 --- a/app/assets/javascripts/discourse/views/flag.js.es6 +++ b/app/assets/javascripts/discourse/views/flag.js.es6 @@ -7,14 +7,21 @@ export default ModalBodyView.extend({ return this.get('controller.flagTopic') ? I18n.t('flagging_topic.title') : I18n.t('flagging.title'); }.property('controller.flagTopic'), + _selectRadio: function() { + this.$("input[type='radio']").prop('checked', false); + + const nameKey = this.get('controller.selected.name_key'); + if (!nameKey) { return; } + + this.$('#radio_' + nameKey).prop('checked', 'true'); + }, + selectedChanged: function() { - Em.run.next(() => { - this.$("input[type='radio']").prop('checked', false); + Ember.run.next(this, this._selectRadio); + }.observes('controller.selected.name_key'), - const nameKey = this.get('controller.selected.name_key'); - if (!nameKey) { return; } - - this.$('#radio_' + nameKey).prop('checked', 'true'); - }); - }.observes('controller.selected.name_key') + // See: https://github.com/emberjs/ember.js/issues/10869 + _selectedHack: function() { + this.removeObserver('controller.selected.name_key'); + }.on('willDestroyElement') }); diff --git a/app/assets/javascripts/discourse/views/input-tip.js.es6 b/app/assets/javascripts/discourse/views/input-tip.js.es6 deleted file mode 100644 index 6caf2dad67..0000000000 --- a/app/assets/javascripts/discourse/views/input-tip.js.es6 +++ /dev/null @@ -1,17 +0,0 @@ -import StringBuffer from 'discourse/mixins/string-buffer'; - -export default Discourse.View.extend(StringBuffer, { - classNameBindings: [':tip', 'good', 'bad'], - rerenderTriggers: ['validation'], - - bad: Em.computed.alias('validation.failed'), - good: Em.computed.not('bad'), - - renderString: function(buffer) { - var reason = this.get('validation.reason'); - if (reason) { - var icon = this.get('good') ? 'fa-check' : 'fa-times'; - return buffer.push(" " + reason); - } - } -}); diff --git a/app/assets/javascripts/discourse/views/invite-reply-button.js.es6 b/app/assets/javascripts/discourse/views/invite-reply-button.js.es6 index bea163892a..5e3279d5d0 100644 --- a/app/assets/javascripts/discourse/views/invite-reply-button.js.es6 +++ b/app/assets/javascripts/discourse/views/invite-reply-button.js.es6 @@ -4,7 +4,7 @@ export default ButtonView.extend({ textKey: 'topic.invite_reply.title', helpKey: 'topic.invite_reply.help', attributeBindings: ['disabled'], - disabled: Em.computed.or('controller.archived', 'controller.closed', 'controller.deleted'), + disabled: Em.computed.or('controller.model.archived', 'controller.model.closed', 'controller.model.deleted'), renderIcon(buffer) { buffer.push(""); diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index 6f767825a3..67ddd19126 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -6,11 +6,12 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, { classNameBindings: ['postTypeClass', 'selected', 'post.hidden:post-hidden', - 'post.deleted', + 'post.deleted:deleted', 'post.topicOwner:topic-owner', 'groupNameClass', 'post.wiki:wiki'], - postBinding: 'content', + + post: Ember.computed.alias('content'), historyHeat: function() { var updatedAt = this.get('post.updated_at'); diff --git a/app/assets/javascripts/discourse/views/selected-posts.js.es6 b/app/assets/javascripts/discourse/views/selected-posts.js.es6 new file mode 100644 index 0000000000..d173337d75 --- /dev/null +++ b/app/assets/javascripts/discourse/views/selected-posts.js.es6 @@ -0,0 +1,9 @@ +export default Discourse.View.extend({ + elementId: 'selected-posts', + classNameBindings: ['customVisibility'], + templateName: "selected-posts", + + customVisibility: function() { + if (!this.get('controller.multiSelect')) return 'hidden'; + }.property('controller.multiSelect') +}); diff --git a/app/assets/javascripts/discourse/views/selected_posts_view.js b/app/assets/javascripts/discourse/views/selected_posts_view.js deleted file mode 100644 index 97c38065a7..0000000000 --- a/app/assets/javascripts/discourse/views/selected_posts_view.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - This view is used to handle the interface for multi selecting of posts. - - @class SelectedPostsView - @extends Discourse.View - @namespace Discourse - @module Discourse -**/ -Discourse.SelectedPostsView = Discourse.View.extend({ - elementId: 'selected-posts', - templateName: 'selected_posts', - topicBinding: 'controller.content', - classNameBindings: ['customVisibility'], - - customVisibility: (function() { - if (!this.get('controller.multiSelect')) return 'hidden'; - }).property('controller.multiSelect') - -}); - - diff --git a/app/assets/javascripts/discourse/views/share.js.es6 b/app/assets/javascripts/discourse/views/share.js.es6 index 0b4b1f7562..8d9db1dc57 100644 --- a/app/assets/javascripts/discourse/views/share.js.es6 +++ b/app/assets/javascripts/discourse/views/share.js.es6 @@ -40,15 +40,15 @@ export default Discourse.View.extend({ }.observes('controller.link'), didInsertElement: function() { - var shareView = this, + var self = this, $html = $('html'); $html.on('mousedown.outside-share-link', function(e) { // Use mousedown instead of click so this event is handled before routing occurs when a // link is clicked (which is a click event) while the share dialog is showing. - if (shareView.$().has(e.target).length !== 0) { return; } + if (self.$().has(e.target).length !== 0) { return; } - shareView.get('controller').send('close'); + self.get('controller').send('close'); return true; }); @@ -91,16 +91,16 @@ export default Discourse.View.extend({ $shareLink.css({left: "" + x + "px"}); } - shareView.set('controller.link', url); - shareView.set('controller.postNumber', postNumber); - shareView.set('controller.date', date); + self.set('controller.link', url); + self.set('controller.postNumber', postNumber); + self.set('controller.date', date); return false; }); $html.on('keydown.share-view', function(e){ if (e.keyCode === 27) { - shareView.get('controller').send('close'); + self.get('controller').send('close'); } }); }, diff --git a/app/assets/javascripts/discourse/views/split-topic.js.es6 b/app/assets/javascripts/discourse/views/split-topic.js.es6 index 1c5be40e94..5ee0d76554 100644 --- a/app/assets/javascripts/discourse/views/split-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/split-topic.js.es6 @@ -1,6 +1,7 @@ +import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalBodyView from "discourse/views/modal-body"; -export default ModalBodyView.extend(Discourse.SelectedPostsCount, { +export default ModalBodyView.extend(SelectedPostsCount, { templateName: 'modal/split_topic', title: I18n.t('topic.split_topic.title') }); diff --git a/app/assets/javascripts/discourse/views/topic-list-item.js.es6 b/app/assets/javascripts/discourse/views/topic-list-item.js.es6 index 7286c85ca6..799d1315b8 100644 --- a/app/assets/javascripts/discourse/views/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-list-item.js.es6 @@ -4,7 +4,7 @@ export default Discourse.View.extend(StringBuffer, { topic: Em.computed.alias("content"), rerenderTriggers: ['controller.bulkSelectEnabled', 'topic.pinned'], tagName: 'tr', - rawTemplate: 'list/topic_list_item.raw', + rawTemplate: 'list/topic-list-item.raw', classNameBindings: ['controller.checked', ':topic-list-item', 'unboundClassNames', diff --git a/app/assets/javascripts/discourse/views/topic-progress.js.es6 b/app/assets/javascripts/discourse/views/topic-progress.js.es6 index f69e61df19..baff56dcfd 100644 --- a/app/assets/javascripts/discourse/views/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-progress.js.es6 @@ -29,7 +29,7 @@ export default Ember.View.extend({ _updateBar: function() { Em.run.scheduleOnce('afterRender', this, '_updateProgressBar'); - }.observes('controller.streamPercentage', 'postStream.stream.@each'), + }.observes('controller.streamPercentage', 'controller.model.postStream.stream.@each'), _updateProgressBar: function() { // speeds up stuff, bypass jquery slowness and extra checks diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index 46f46b1807..aaadcfce59 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -7,7 +7,8 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link'; const TopicView = Discourse.View.extend(AddCategoryClass, AddArchetypeClass, Discourse.Scrolling, { templateName: 'topic', topicBinding: 'controller.model', - userFiltersBinding: 'controller.userFilters', + + userFilters: Ember.computed.alias('controller.model.userFilters'), classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype', 'topic.is_warning', @@ -19,7 +20,7 @@ const TopicView = Discourse.View.extend(AddCategoryClass, AddArchetypeClass, Dis categoryFullSlug: Em.computed.alias('topic.category.fullSlug'), - postStream: Em.computed.alias('controller.postStream'), + postStream: Em.computed.alias('controller.model.postStream'), archetype: Em.computed.alias('topic.archetype'), diff --git a/app/assets/javascripts/discourse/views/user-notification-history.js.es6 b/app/assets/javascripts/discourse/views/user-notifications.js.es6 similarity index 61% rename from app/assets/javascripts/discourse/views/user-notification-history.js.es6 rename to app/assets/javascripts/discourse/views/user-notifications.js.es6 index 90dfc8e401..c674102044 100644 --- a/app/assets/javascripts/discourse/views/user-notification-history.js.es6 +++ b/app/assets/javascripts/discourse/views/user-notifications.js.es6 @@ -2,6 +2,5 @@ import LoadMore from "discourse/mixins/load-more"; export default Ember.View.extend(LoadMore, { eyelineSelector: '.user-stream .notification', - classNames: ['user-stream', 'notification-history'], - templateName: 'user/notifications' + classNames: ['user-stream', 'notification-history'] }); diff --git a/app/assets/javascripts/discourse/views/user-topics-list.js.es6 b/app/assets/javascripts/discourse/views/user-topics-list.js.es6 index 8a97f78ff1..3f21ea2459 100644 --- a/app/assets/javascripts/discourse/views/user-topics-list.js.es6 +++ b/app/assets/javascripts/discourse/views/user-topics-list.js.es6 @@ -3,5 +3,4 @@ import LoadMore from "discourse/mixins/load-more"; export default Discourse.View.extend(LoadMore, { classNames: ['paginated-topics-list'], eyelineSelector: '.paginated-topics-list .topic-list tr', - templateName: 'list/user_topics_list' }); diff --git a/app/assets/javascripts/discourse/views/view.js.es6 b/app/assets/javascripts/discourse/views/view.js.es6 index f939764ebb..03f49906f8 100644 --- a/app/assets/javascripts/discourse/views/view.js.es6 +++ b/app/assets/javascripts/discourse/views/view.js.es6 @@ -1,17 +1,3 @@ import Presence from 'discourse/mixins/presence'; -const View = Ember.View.extend(Presence, {}); - -View.reopenClass({ - registerHelper(helperName, helperClass) { - Ember.Handlebars.registerHelper(helperName, function(options) { - var hash = options.hash, - types = options.hashTypes; - - Discourse.Utilities.normalizeHash(hash, types); - return Ember.Handlebars.helpers.view.call(this, helperClass, options); - }); - } -}); - -export default View; +export default Ember.View.extend(Presence); diff --git a/app/assets/javascripts/ember-addons/list-item-view-mixin.js.es6 b/app/assets/javascripts/ember-addons/list-item-view-mixin.js.es6 new file mode 100644 index 0000000000..a9faf184a7 --- /dev/null +++ b/app/assets/javascripts/ember-addons/list-item-view-mixin.js.es6 @@ -0,0 +1,45 @@ +import Ember from 'ember'; + +function samePosition(a, b) { + return a && b && a.x === b.x && a.y === b.y; +} + +function positionElement() { + var element, position, _position; + + Ember.instrument('view.updateContext.positionElement', this, function() { + element = this.element; + position = this.position; + _position = this._position; + + if (!position || !element) { + return; + } + + // // TODO: avoid needing this by avoiding unnecessary + // // calls to this method in the first place + if (samePosition(position, _position)) { + return; + } + + Ember.run.schedule('render', this, this._parentView.applyTransform, this, position.x, position.y); + this._position = position; + }, this); +} + +export default Ember.Mixin.create({ + classNames: ['ember-list-item-view'], + style: Ember.String.htmlSafe(''), + attributeBindings: ['style'], + _position: null, + _positionElement: positionElement, + + positionElementWhenInserted: Ember.on('init', function(){ + this.one('didInsertElement', positionElement); + }), + + updatePosition: function(position) { + this.position = position; + this._positionElement(); + } +}); diff --git a/app/assets/javascripts/ember-addons/list-item-view.js.es6 b/app/assets/javascripts/ember-addons/list-item-view.js.es6 new file mode 100644 index 0000000000..bb24adc854 --- /dev/null +++ b/app/assets/javascripts/ember-addons/list-item-view.js.es6 @@ -0,0 +1,57 @@ +import Ember from 'ember'; +import ListItemViewMixin from './list-item-view-mixin'; + +var get = Ember.get, set = Ember.set; + +/** + The `Ember.ListItemView` view class renders a + [div](https://developer.mozilla.org/en/HTML/Element/div) HTML element + with `ember-list-item-view` class. It allows you to specify a custom item + handlebars template for `Ember.ListView`. + + Example: + + ```handlebars + + ``` + + ```javascript + App.ListView = Ember.ListView.extend({ + height: 500, + rowHeight: 20, + itemViewClass: Ember.ListItemView.extend({templateName: "row_item"}) + }); + ``` + + @extends Ember.View + @class ListItemView + @namespace Ember +*/ +export default Ember.View.extend(ListItemViewMixin, { + updateContext: function(newContext) { + var context = get(this, 'context'); + + Ember.instrument('view.updateContext.render', this, function() { + if (context !== newContext) { + set(this, 'context', newContext); + if (newContext && newContext.isController) { + set(this, 'controller', newContext); + } + } + }, this); + }, + + rerender: function () { + if (this.isDestroying || this.isDestroyed) { + return; + } + + return this._super.apply(this, arguments); + }, + + _contextDidChange: Ember.observer(function () { + Ember.run.once(this, this.rerender); + }, 'context', 'controller') +}); diff --git a/app/assets/javascripts/ember-addons/list-view-helper.js.es6 b/app/assets/javascripts/ember-addons/list-view-helper.js.es6 new file mode 100644 index 0000000000..70053a194b --- /dev/null +++ b/app/assets/javascripts/ember-addons/list-view-helper.js.es6 @@ -0,0 +1,94 @@ +import Ember from 'ember'; + +// TODO - remove this! +var el = document.body || document.createElement('div'); +var style = el.style; +var set = Ember.set; + +function getElementStyle (prop) { + var uppercaseProp = prop.charAt(0).toUpperCase() + prop.slice(1); + + var props = [ + prop, + 'webkit' + prop, + 'webkit' + uppercaseProp, + 'Moz' + uppercaseProp, + 'moz' + uppercaseProp, + 'ms' + uppercaseProp, + 'ms' + prop + ]; + + for (var i=0; i < props.length; i++) { + var property = props[i]; + + if (property in style) { + return property; + } + } + + return null; +} + +function getCSSStyle (attr) { + var styleName = getElementStyle(attr); + var prefix = styleName.toLowerCase().replace(attr, ''); + + var dic = { + webkit: '-webkit-' + attr, + moz: '-moz-' + attr, + ms: '-ms-' + attr + }; + + if (prefix && dic[prefix]) { + return dic[prefix]; + } + + return styleName; +} + +var styleAttributeName = getElementStyle('transform'); +var transformProp = getCSSStyle('transform'); +var perspectiveProp = getElementStyle('perspective'); +var supports2D = !!transformProp; +var supports3D = !!perspectiveProp; + +function setStyle (optionalStyleString) { + return function (obj, x, y) { + var isElement = obj instanceof Element; + + if (optionalStyleString && (supports2D || supports3D)) { + var style = Ember.String.fmt(optionalStyleString, x, y); + + if (isElement) { + obj.style[styleAttributeName] = Ember.String.htmlSafe(style); + } else { + set(obj, 'style', Ember.String.htmlSafe(transformProp + ': ' + style)); + } + } else { + if (isElement) { + obj.style.top = y; + obj.style.left = x; + } + } + }; +} + +export default { + transformProp: transformProp, + applyTransform: (function () { + if (supports2D) { + return setStyle('translate(%@px, %@px)'); + } + + return setStyle(); + })(), + apply3DTransform: (function () { + if (supports3D) { + return setStyle('translate3d(%@px, %@px, 0)'); + } else if (supports2D) { + return setStyle('translate(%@px, %@px)'); + } + + return setStyle(); + })() +}; diff --git a/app/assets/javascripts/ember-addons/list-view-mixin.js.es6 b/app/assets/javascripts/ember-addons/list-view-mixin.js.es6 new file mode 100644 index 0000000000..bcc0a44449 --- /dev/null +++ b/app/assets/javascripts/ember-addons/list-view-mixin.js.es6 @@ -0,0 +1,886 @@ +// TODO: remove unused: false +/* jshint unused: false*/ +import Ember from 'ember'; +import ReusableListItemView from './reusable-list-item-view'; + +var get = Ember.get; +var set = Ember.set; +var min = Math.min; +var max = Math.max; +var floor = Math.floor; +var ceil = Math.ceil; +var forEach = Ember.ArrayPolyfills.forEach; + +function addContentArrayObserver() { + var content = get(this, 'content'); + if (content) { + content.addArrayObserver(this); + } +} + +function removeAndDestroy(object) { + this.removeObject(object); + object.destroy(); +} + +function syncChildViews() { + Ember.run.once(this, '_syncChildViews'); +} + +function sortByContentIndex (viewOne, viewTwo) { + return get(viewOne, 'contentIndex') - get(viewTwo, 'contentIndex'); +} + +function removeEmptyView() { + var emptyView = get(this, 'emptyView'); + if (emptyView && emptyView instanceof Ember.View) { + emptyView.removeFromParent(); + if (this.totalHeightDidChange !== undefined) { + this.totalHeightDidChange(); + } + } +} + +function addEmptyView() { + var emptyView = get(this, 'emptyView'); + + if (!emptyView) { + return; + } + + if ('string' === typeof emptyView) { + emptyView = get(emptyView) || emptyView; + } + + emptyView = this.createChildView(emptyView); + set(this, 'emptyView', emptyView); + + if (Ember.CoreView.detect(emptyView)) { + this._createdEmptyView = emptyView; + } + + this.unshiftObject(emptyView); +} + +function enableProfilingOutput() { + function before(name, time/*, payload*/) { + console.time(name); + } + + function after (name, time/*, payload*/) { + console.timeEnd(name); + } + + if (Ember.ENABLE_PROFILING) { + Ember.subscribe('view._scrollContentTo', { + before: before, + after: after + }); + Ember.subscribe('view.updateContext', { + before: before, + after: after + }); + } +} + +/** + @class Ember.ListViewMixin + @namespace Ember +*/ +export default Ember.Mixin.create({ + itemViewClass: ReusableListItemView, + emptyViewClass: Ember.View, + classNames: ['ember-list-view'], + attributeBindings: ['style'], + classNameBindings: ['_isGrid:ember-list-view-grid:ember-list-view-list'], + scrollTop: 0, + bottomPadding: 0, // TODO: maybe this can go away + _lastEndingIndex: 0, + paddingCount: 1, + _cachedPos: 0, + + _isGrid: Ember.computed.gt('columnCount', 1).readOnly(), + + /** + @private + + Setup a mixin. + - adding observer to content array + - creating child views based on height and length of the content array + + @method init + */ + init: function() { + this._super(); + this._cachedHeights = [0]; + this.on('didInsertElement', this._syncListContainerWidth); + this.columnCountDidChange(); + this._syncChildViews(); + this._addContentArrayObserver(); + }, + + _addContentArrayObserver: Ember.beforeObserver(function() { + addContentArrayObserver.call(this); + }, 'content'), + + /** + Called on your view when it should push strings of HTML into a + `Ember.RenderBuffer`. + + Adds a [div](https://developer.mozilla.org/en-US/docs/HTML/Element/div) + with a required `ember-list-container` class. + + @method render + @param {Ember.RenderBuffer} buffer The render buffer + */ + render: function (buffer) { + var element = buffer.element(); + var dom = buffer.dom; + var container = dom.createElement('div'); + container.className = 'ember-list-container'; + element.appendChild(container); + + this._childViewsMorph = dom.appendMorph(container, container, null); + + return container; + }, + + createChildViewsMorph: function (element) { + this._childViewsMorph = this._renderer._dom.createMorph(element.lastChild, element.lastChild, null); + return element; + }, + + willInsertElement: function() { + if (!this.get('height') || !this.get('rowHeight')) { + throw new Error('A ListView must be created with a height and a rowHeight.'); + } + this._super(); + }, + + /** + @private + + Sets inline styles of the view: + - height + - width + - position + - overflow + - -webkit-overflow + - overflow-scrolling + + Called while attributes binding. + + @property {Ember.ComputedProperty} style + */ + style: Ember.computed('height', 'width', function() { + var height, width, style, css; + + height = get(this, 'height'); + width = get(this, 'width'); + css = get(this, 'css'); + + style = ''; + + if (height) { + style += 'height:' + height + 'px;'; + } + + if (width) { + style += 'width:' + width + 'px;'; + } + + for ( var rule in css ) { + if (css.hasOwnProperty(rule)) { + style += rule + ':' + css[rule] + ';'; + } + } + + return Ember.String.htmlSafe(style); + }), + + /** + @private + + Performs visual scrolling. Is overridden in Ember.ListView. + + @method scrollTo + */ + scrollTo: function(y) { + throw new Error('must override to perform the visual scroll and effectively delegate to _scrollContentTo'); + }, + + /** + @private + + Internal method used to force scroll position + + @method scrollTo + */ + _scrollTo: Ember.K, + + /** + @private + @method _scrollContentTo + */ + _scrollContentTo: function(y) { + var startingIndex, endingIndex, + contentIndex, visibleEndingIndex, maxContentIndex, + contentIndexEnd, contentLength, scrollTop, content; + + scrollTop = max(0, y); + + if (this.scrollTop === scrollTop) { + return; + } + + // allow a visual overscroll, but don't scroll the content. As we are doing needless + // recycyling, and adding unexpected nodes to the DOM. + var maxScrollTop = max(0, get(this, 'totalHeight') - get(this, 'height')); + scrollTop = min(scrollTop, maxScrollTop); + + content = get(this, 'content'); + contentLength = get(content, 'length'); + startingIndex = this._startingIndex(contentLength); + + Ember.instrument('view._scrollContentTo', { + scrollTop: scrollTop, + content: content, + startingIndex: startingIndex, + endingIndex: min(max(contentLength - 1, 0), startingIndex + this._numChildViewsForViewport()) + }, function () { + this.scrollTop = scrollTop; + + maxContentIndex = max(contentLength - 1, 0); + + startingIndex = this._startingIndex(); + visibleEndingIndex = startingIndex + this._numChildViewsForViewport(); + + endingIndex = min(maxContentIndex, visibleEndingIndex); + + if (startingIndex === this._lastStartingIndex && + endingIndex === this._lastEndingIndex) { + + this.trigger('scrollYChanged', y); + return; + } else { + + Ember.run(this, function() { + this._reuseChildren(); + + this._lastStartingIndex = startingIndex; + this._lastEndingIndex = endingIndex; + this.trigger('scrollYChanged', y); + }); + } + }, this); + + }, + + /** + @private + + Computes the height for a `Ember.ListView` scrollable container div. + You must specify `rowHeight` parameter for the height to be computed properly. + + @property {Ember.ComputedProperty} totalHeight + */ + totalHeight: Ember.computed('content.length', + 'rowHeight', + 'columnCount', + 'bottomPadding', function() { + if (typeof this.heightForIndex === 'function') { + return this._totalHeightWithHeightForIndex(); + } else { + return this._totalHeightWithStaticRowHeight(); + } + }), + + _doRowHeightDidChange: function() { + this._cachedHeights = [0]; + this._cachedPos = 0; + this._syncChildViews(); + }, + + _rowHeightDidChange: Ember.observer('rowHeight', function() { + Ember.run.once(this, this._doRowHeightDidChange); + }), + + _totalHeightWithHeightForIndex: function() { + var length = this.get('content.length'); + return this._cachedHeightLookup(length); + }, + + _totalHeightWithStaticRowHeight: function() { + var contentLength, rowHeight, columnCount, bottomPadding; + + contentLength = get(this, 'content.length'); + rowHeight = get(this, 'rowHeight'); + columnCount = get(this, 'columnCount'); + bottomPadding = get(this, 'bottomPadding'); + + return ((ceil(contentLength / columnCount)) * rowHeight) + bottomPadding; + }, + + /** + @private + @method _prepareChildForReuse + */ + _prepareChildForReuse: function(childView) { + childView.prepareForReuse(); + }, + + createChildView: function (_view) { + return this._super(_view, this._itemViewProps || {}); + }, + + /** + @private + @method _reuseChildForContentIndex + */ + _reuseChildForContentIndex: function(childView, contentIndex) { + var content, context, newContext, childsCurrentContentIndex, position, enableProfiling, oldChildView; + + var contentViewClass = this.itemViewForIndex(contentIndex); + + if (childView.constructor !== contentViewClass) { + // rather then associative arrays, lets move childView + contentEntry maping to a Map + var i = this._childViews.indexOf(childView); + childView.destroy(); + childView = this.createChildView(contentViewClass); + this.insertAt(i, childView); + } + + content = get(this, 'content'); + enableProfiling = get(this, 'enableProfiling'); + position = this.positionForIndex(contentIndex); + childView.updatePosition(position); + + set(childView, 'contentIndex', contentIndex); + + if (enableProfiling) { + Ember.instrument('view._reuseChildForContentIndex', position, function() { + + }, this); + } + + newContext = content.objectAt(contentIndex); + childView.updateContext(newContext); + }, + + /** + @private + @method positionForIndex + */ + positionForIndex: function(index) { + if (typeof this.heightForIndex !== 'function') { + return this._singleHeightPosForIndex(index); + } + else { + return this._multiHeightPosForIndex(index); + } + }, + + _singleHeightPosForIndex: function(index) { + var elementWidth, width, columnCount, rowHeight, y, x; + + elementWidth = get(this, 'elementWidth') || 1; + width = get(this, 'width') || 1; + columnCount = get(this, 'columnCount'); + rowHeight = get(this, 'rowHeight'); + + y = (rowHeight * floor(index/columnCount)); + x = (index % columnCount) * elementWidth; + + return { + y: y, + x: x + }; + }, + + // 0 maps to 0, 1 maps to heightForIndex(i) + _multiHeightPosForIndex: function(index) { + var elementWidth, width, columnCount, rowHeight, y, x; + + elementWidth = get(this, 'elementWidth') || 1; + width = get(this, 'width') || 1; + columnCount = get(this, 'columnCount'); + + x = (index % columnCount) * elementWidth; + y = this._cachedHeightLookup(index); + + return { + x: x, + y: y + }; + }, + + _cachedHeightLookup: function(index) { + for (var i = this._cachedPos; i < index; i++) { + this._cachedHeights[i + 1] = this._cachedHeights[i] + this.heightForIndex(i); + } + this._cachedPos = i; + return this._cachedHeights[index]; + }, + + /** + @private + @method _childViewCount + */ + _childViewCount: function() { + var contentLength, childViewCountForHeight; + + contentLength = get(this, 'content.length'); + childViewCountForHeight = this._numChildViewsForViewport(); + + return min(contentLength, childViewCountForHeight); + }, + + /** + @private + + Returns a number of columns in the Ember.ListView (for grid layout). + + If you want to have a multi column layout, you need to specify both + `width` and `elementWidth`. + + If no `elementWidth` is specified, it returns `1`. Otherwise, it will + try to fit as many columns as possible for a given `width`. + + @property {Ember.ComputedProperty} columnCount + */ + columnCount: Ember.computed('width', 'elementWidth', function() { + var elementWidth, width, count; + + elementWidth = get(this, 'elementWidth'); + width = get(this, 'width'); + + if (elementWidth && width > elementWidth) { + count = floor(width / elementWidth); + } else { + count = 1; + } + + return count; + }), + + /** + @private + + Fires every time column count is changed. + + @event columnCountDidChange + */ + columnCountDidChange: Ember.observer(function() { + var ratio, currentScrollTop, proposedScrollTop, maxScrollTop, + scrollTop, lastColumnCount, newColumnCount, element; + + lastColumnCount = this._lastColumnCount; + + currentScrollTop = this.scrollTop; + newColumnCount = get(this, 'columnCount'); + maxScrollTop = get(this, 'maxScrollTop'); + element = this.element; + + this._lastColumnCount = newColumnCount; + + if (lastColumnCount) { + ratio = (lastColumnCount / newColumnCount); + proposedScrollTop = currentScrollTop * ratio; + scrollTop = min(maxScrollTop, proposedScrollTop); + + this._scrollTo(scrollTop); + this.scrollTop = scrollTop; + } + + if (arguments.length > 0) { + // invoked by observer + Ember.run.schedule('afterRender', this, this._syncListContainerWidth); + } + }, 'columnCount'), + + /** + @private + + Computes max possible scrollTop value given the visible viewport + and scrollable container div height. + + @property {Ember.ComputedProperty} maxScrollTop + */ + maxScrollTop: Ember.computed('height', 'totalHeight', function(){ + var totalHeight, viewportHeight; + + totalHeight = get(this, 'totalHeight'); + viewportHeight = get(this, 'height'); + + return max(0, totalHeight - viewportHeight); + }), + + /** + @private + + Determines whether the emptyView is the current childView. + + @method _isChildEmptyView + */ + _isChildEmptyView: function() { + var emptyView = get(this, 'emptyView'); + + return emptyView && emptyView instanceof Ember.View && + this._childViews.length === 1 && this._childViews.indexOf(emptyView) === 0; + }, + + /** + @private + + Computes the number of views that would fit in the viewport area. + You must specify `height` and `rowHeight` parameters for the number of + views to be computed properly. + + @method _numChildViewsForViewport + */ + _numChildViewsForViewport: function() { + + if (this.heightForIndex) { + return this._numChildViewsForViewportWithMultiHeight(); + } else { + return this._numChildViewsForViewportWithoutMultiHeight(); + } + }, + + _numChildViewsForViewportWithoutMultiHeight: function() { + var height, rowHeight, paddingCount, columnCount; + + height = get(this, 'height'); + rowHeight = get(this, 'rowHeight'); + paddingCount = get(this, 'paddingCount'); + columnCount = get(this, 'columnCount'); + + return (ceil(height / rowHeight) * columnCount) + (paddingCount * columnCount); + }, + + _numChildViewsForViewportWithMultiHeight: function() { + var rowHeight, paddingCount, columnCount; + var scrollTop = this.scrollTop; + var viewportHeight = this.get('height'); + var length = this.get('content.length'); + var heightfromTop = 0; + var padding = get(this, 'paddingCount'); + + var startingIndex = this._calculatedStartingIndex(); + var currentHeight = 0; + + var offsetHeight = this._cachedHeightLookup(startingIndex); + for (var i = 0; i < length; i++) { + if (this._cachedHeightLookup(startingIndex + i + 1) - offsetHeight > viewportHeight) { + break; + } + } + + return i + padding + 1; + }, + + + /** + @private + + Computes the starting index of the item views array. + Takes `scrollTop` property of the element into account. + + Is used in `_syncChildViews`. + + @method _startingIndex + */ + _startingIndex: function(_contentLength) { + var scrollTop, rowHeight, columnCount, calculatedStartingIndex, + contentLength; + + if (_contentLength === undefined) { + contentLength = get(this, 'content.length'); + } else { + contentLength = _contentLength; + } + + scrollTop = this.scrollTop; + rowHeight = get(this, 'rowHeight'); + columnCount = get(this, 'columnCount'); + + if (this.heightForIndex) { + calculatedStartingIndex = this._calculatedStartingIndex(); + } else { + calculatedStartingIndex = floor(scrollTop / rowHeight) * columnCount; + } + + var viewsNeededForViewport = this._numChildViewsForViewport(); + var paddingCount = (1 * columnCount); + var largestStartingIndex = max(contentLength - viewsNeededForViewport, 0); + + return min(calculatedStartingIndex, largestStartingIndex); + }, + + _calculatedStartingIndex: function() { + var rowHeight, paddingCount, columnCount; + var scrollTop = this.scrollTop; + var viewportHeight = this.get('height'); + var length = this.get('content.length'); + var heightfromTop = 0; + var padding = get(this, 'paddingCount'); + + for (var i = 0; i < length; i++) { + if (this._cachedHeightLookup(i + 1) >= scrollTop) { + break; + } + } + + return i; + }, + + /** + @private + @event contentWillChange + */ + contentWillChange: Ember.beforeObserver(function() { + var content = get(this, 'content'); + + if (content) { + content.removeArrayObserver(this); + } + }, 'content'), + + /**), + @private + @event contentDidChange + */ + contentDidChange: Ember.observer(function() { + addContentArrayObserver.call(this); + syncChildViews.call(this); + }, 'content'), + + /** + @private + @property {Function} needsSyncChildViews + */ + needsSyncChildViews: Ember.observer(syncChildViews, 'height', 'width', 'columnCount'), + + /** + @private + + Returns a new item view. Takes `contentIndex` to set the context + of the returned view properly. + + @param {Number} contentIndex item index in the content array + @method _addItemView + */ + _addItemView: function (contentIndex) { + var itemViewClass, childView; + + itemViewClass = this.itemViewForIndex(contentIndex); + childView = this.createChildView(itemViewClass); + this.pushObject(childView); + }, + + /** + @public + + Returns a view class for the provided contentIndex. If the view is + different then the one currently present it will remove the existing view + and replace it with an instance of the class provided + + @param {Number} contentIndex item index in the content array + @method _addItemView + @returns {Ember.View} ember view class for this index + */ + itemViewForIndex: function(contentIndex) { + return get(this, 'itemViewClass'); + }, + + /** + @public + + Returns a view class for the provided contentIndex. If the view is + different then the one currently present it will remove the existing view + and replace it with an instance of the class provided + + @param {Number} contentIndex item index in the content array + @method _addItemView + @returns {Ember.View} ember view class for this index + */ + heightForIndex: null, + + /** + @private + + Intelligently manages the number of childviews. + + @method _syncChildViews + **/ + _syncChildViews: function () { + var childViews, childViewCount, + numberOfChildViews, numberOfChildViewsNeeded, + contentIndex, startingIndex, endingIndex, + contentLength, emptyView, count, delta; + + if (this.isDestroyed || this.isDestroying) { + return; + } + + contentLength = get(this, 'content.length'); + emptyView = get(this, 'emptyView'); + + childViewCount = this._childViewCount(); + childViews = this.positionOrderedChildViews(); + + if (this._isChildEmptyView()) { + removeEmptyView.call(this); + } + + startingIndex = this._startingIndex(); + endingIndex = startingIndex + childViewCount; + + numberOfChildViewsNeeded = childViewCount; + numberOfChildViews = childViews.length; + + delta = numberOfChildViewsNeeded - numberOfChildViews; + + if (delta === 0) { + // no change + } else if (delta > 0) { + // more views are needed + contentIndex = this._lastEndingIndex; + + for (count = 0; count < delta; count++, contentIndex++) { + this._addItemView(contentIndex); + } + } else { + // less views are needed + forEach.call( + childViews.splice(numberOfChildViewsNeeded, numberOfChildViews), + removeAndDestroy, + this + ); + } + + this._reuseChildren(); + + this._lastStartingIndex = startingIndex; + this._lastEndingIndex = this._lastEndingIndex + delta; + + if (contentLength === 0 || contentLength === undefined) { + addEmptyView.call(this); + } + }, + + /** + @private + + Applies an inline width style to the list container. + + @method _syncListContainerWidth + **/ + _syncListContainerWidth: function() { + var elementWidth, columnCount, containerWidth, element; + + elementWidth = get(this, 'elementWidth'); + columnCount = get(this, 'columnCount'); + containerWidth = elementWidth * columnCount; + element = this.$('.ember-list-container'); + + if (containerWidth && element) { + element.css('width', containerWidth); + } + }, + + /** + @private + @method _reuseChildren + */ + _reuseChildren: function(){ + var contentLength, childViews, childViewsLength, + startingIndex, endingIndex, childView, attrs, + contentIndex, visibleEndingIndex, maxContentIndex, + contentIndexEnd, scrollTop; + + scrollTop = this.scrollTop; + contentLength = get(this, 'content.length'); + maxContentIndex = max(contentLength - 1, 0); + childViews = this.getReusableChildViews(); + childViewsLength = childViews.length; + + startingIndex = this._startingIndex(); + visibleEndingIndex = startingIndex + this._numChildViewsForViewport(); + + endingIndex = min(maxContentIndex, visibleEndingIndex); + + contentIndexEnd = min(visibleEndingIndex, startingIndex + childViewsLength); + + for (contentIndex = startingIndex; contentIndex < contentIndexEnd; contentIndex++) { + childView = childViews[contentIndex % childViewsLength]; + this._reuseChildForContentIndex(childView, contentIndex); + } + }, + + /** + @private + @method getReusableChildViews + */ + getReusableChildViews: function() { + return this._childViews; + }, + + /** + @private + @method positionOrderedChildViews + */ + positionOrderedChildViews: function() { + return this.getReusableChildViews().sort(sortByContentIndex); + }, + + arrayWillChange: Ember.K, + + /** + @private + @event arrayDidChange + */ + // TODO: refactor + arrayDidChange: function(content, start, removedCount, addedCount) { + var index, contentIndex, state; + + if (this._isChildEmptyView()) { + removeEmptyView.call(this); + } + + // Support old and new Ember versions + state = this._state || this.state; + + if (state === 'inDOM') { + // ignore if all changes are out of the visible change + if (start >= this._lastStartingIndex || start < this._lastEndingIndex) { + index = 0; + // ignore all changes not in the visible range + // this can re-position many, rather then causing a cascade of re-renders + forEach.call( + this.positionOrderedChildViews(), + function(childView) { + contentIndex = this._lastStartingIndex + index; + this._reuseChildForContentIndex(childView, contentIndex); + index++; + }, + this + ); + } + + syncChildViews.call(this); + } + }, + + destroy: function () { + if (!this._super()) { + return; + } + + if (this._createdEmptyView) { + this._createdEmptyView.destroy(); + } + + return this; + } +}); diff --git a/app/assets/javascripts/ember-addons/list-view.js.es6 b/app/assets/javascripts/ember-addons/list-view.js.es6 new file mode 100644 index 0000000000..d931babeb0 --- /dev/null +++ b/app/assets/javascripts/ember-addons/list-view.js.es6 @@ -0,0 +1,167 @@ +import Ember from 'ember'; +import ListViewHelper from './list-view-helper'; +import ListViewMixin from './list-view-mixin'; + +var get = Ember.get; + +/** + The `Ember.ListView` view class renders a + [div](https://developer.mozilla.org/en/HTML/Element/div) HTML element, + with `ember-list-view` class. + + The context of each item element within the `Ember.ListView` are populated + from the objects in the `ListView`'s `content` property. + + ### `content` as an Array of Objects + + The simplest version of an `Ember.ListView` takes an array of object as its + `content` property. The object will be used as the `context` each item element + inside the rendered `div`. + + Example: + + ```javascript + App.ContributorsRoute = Ember.Route.extend({ + model: function () { + return [ + { name: 'Stefan Penner' }, + { name: 'Alex Navasardyan' }, + { name: 'Ray Cohen'} + ]; + } + }); + ``` + + ```handlebars + {{#ember-list items=contributors height=500 rowHeight=50}} + {{name}} + {{/ember-list}} + ``` + + Would result in the following HTML: + + ```html +
+
+
+ Stefan Penner +
+
+ Alex Navasardyan +
+
+ Ray Cohen +
+
+
+ ``` + + By default `Ember.ListView` provides support for `height`, + `rowHeight`, `width`, `elementWidth`, `scrollTop` parameters. + + Note, that `height` and `rowHeight` are required parameters. + + ```handlebars + {{#ember-list items=this height=500 rowHeight=50}} + {{name}} + {{/ember-list}} + ``` + + If you would like to have multiple columns in your view layout, you can + set `width` and `elementWidth` parameters respectively. + + ```handlebars + {{#ember-list items=this height=500 rowHeight=50 width=500 elementWidth=80}} + {{name}} + {{/ember-list}} + ``` + + ### Extending `Ember.ListView` + + Example: + + ```handlebars + {{view 'list-view' content=content}} + + + ``` + + ```javascript + App.ListView = Ember.ListView.extend({ + height: 500, + width: 500, + elementWidth: 80, + rowHeight: 20, + itemViewClass: Ember.ListItemView.extend({templateName: "row_item"}) + }); + ``` + + @extends Ember.ContainerView + @class ListView + @namespace Ember +*/ +export default Ember.ContainerView.extend(ListViewMixin, { + css: { + position: 'relative', + overflow: 'auto', + '-webkit-overflow-scrolling': 'touch', + 'overflow-scrolling': 'touch' + }, + + applyTransform: ListViewHelper.applyTransform, + + _scrollTo: function(scrollTop) { + var element = this.element; + + if (element) { element.scrollTop = scrollTop; } + }, + + didInsertElement: function() { + var that = this; + + this._updateScrollableHeight(); + + this._scroll = function(e) { that.scroll(e); }; + + Ember.$(this.element).on('scroll', this._scroll); + }, + + willDestroyElement: function() { + Ember.$(this.element).off('scroll', this._scroll); + }, + + scroll: function(e) { + this.scrollTo(e.target.scrollTop); + }, + + scrollTo: function(y) { + this._scrollTo(y); + this._scrollContentTo(y); + }, + + totalHeightDidChange: Ember.observer(function () { + Ember.run.scheduleOnce('afterRender', this, this._updateScrollableHeight); + }, 'totalHeight'), + + _updateScrollableHeight: function () { + var height, state; + + // Support old and new Ember versions + state = this._state || this.state; + + if (state === 'inDOM') { + // if the list is currently displaying the emptyView, remove the height + if (this._isChildEmptyView()) { + height = ''; + } else { + height = get(this, 'totalHeight'); + } + + this.$('.ember-list-container').css({ + height: height + }); + } + } +}); diff --git a/app/assets/javascripts/ember-addons/reusable-list-item-view.js.es6 b/app/assets/javascripts/ember-addons/reusable-list-item-view.js.es6 new file mode 100644 index 0000000000..e2ca3a0f4c --- /dev/null +++ b/app/assets/javascripts/ember-addons/reusable-list-item-view.js.es6 @@ -0,0 +1,38 @@ +import Ember from 'ember'; +import ListItemViewMixin from './list-item-view-mixin'; + +var get = Ember.get, set = Ember.set; + +export default Ember.View.extend(ListItemViewMixin, { + prepareForReuse: Ember.K, + + init: function () { + this._super(); + var context = Ember.ObjectProxy.create(); + this.set('context', context); + this._proxyContext = context; + }, + + isVisible: Ember.computed('context.content', function () { + return !!this.get('context.content'); + }), + + updateContext: function (newContext) { + var context = get(this._proxyContext, 'content'); + + // Support old and new Ember versions + var state = this._state || this.state; + + if (context !== newContext) { + if (state === 'inDOM') { + this.prepareForReuse(newContext); + } + + set(this._proxyContext, 'content', newContext); + + if (newContext && newContext.isController) { + set(this, 'controller', newContext); + } + } + } +}); diff --git a/app/assets/javascripts/ember_include.js.erb b/app/assets/javascripts/ember_include.js.erb index f124141c39..029ce40795 100644 --- a/app/assets/javascripts/ember_include.js.erb +++ b/app/assets/javascripts/ember_include.js.erb @@ -1,7 +1,8 @@ <% if Rails.env.development? || Rails.env.test? - require_asset ("development/ember.js") + require_asset ("ember-template-compiler.js") + require_asset ("ember.custom.debug.js") else - require_asset ("production/ember.js") + require_asset ("ember.prod.js") end %> diff --git a/app/assets/javascripts/jquery_include.js.erb b/app/assets/javascripts/jquery_include.js.erb index 6f509136a6..0423ecabc1 100644 --- a/app/assets/javascripts/jquery_include.js.erb +++ b/app/assets/javascripts/jquery_include.js.erb @@ -1,7 +1,7 @@ <% if Rails.env.development? || Rails.env.test? - require_asset ("development/jquery-2.1.1.js") + require_asset ("jquery.debug.js") else - require_asset ("production/jquery-2.1.1.min.js") + require_asset ("jquery.prod.js") end %> diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 6d856ec381..cf5bbda7b1 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -42,7 +42,8 @@ //= require ./discourse/views/container //= require ./discourse/views/modal-body //= require ./discourse/views/flag -//= require ./discourse/views/combo-box +//= require ./discourse/views/cloaked +//= require ./discourse/components/combo-box //= require ./discourse/views/button //= require ./discourse/components/dropdown-button //= require ./discourse/components/notifications-button @@ -67,6 +68,7 @@ //= require ./discourse/lib/emoji/emoji //= require ./discourse/lib/sharing //= require discourse/lib/desktop-notifications +//= require ./discourse/router //= require_tree ./discourse/dialects //= require_tree ./discourse/controllers diff --git a/app/assets/javascripts/main_include_admin.js b/app/assets/javascripts/main_include_admin.js index 6ba3cb51e8..72d94e54d4 100644 --- a/app/assets/javascripts/main_include_admin.js +++ b/app/assets/javascripts/main_include_admin.js @@ -1,4 +1,4 @@ -//= require list-view +//= require_tree ./ember-addons //= require admin/models/user-field //= require admin/models/site-setting //= require admin/controllers/admin-email-skipped diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 12667f3545..a76153ed7f 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -36,7 +36,6 @@ //= require rsvp.js //= require show-html.js //= require lock-on.js -//= require ember-cloaking //= require break_string //= require buffered-proxy //= require jquery.autoellipsis-1.0.10.min.js diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 227e13a353..c01351a734 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -16,20 +16,19 @@ .topic-list { width: 100%; - border-collapse: separate; - border-spacing: 0; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-collapse: collapse; background: rgba($secondary, .8); > tbody > tr { - - &.archived a { - opacity: 0.6; - } &.has-excerpt .star { vertical-align: top; margin-top: 2px; } + border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + } + + > tbody > tr:first-of-type { + border-top: 3px solid dark-light-diff($primary, $secondary, 90%, -75%); } th, diff --git a/app/assets/stylesheets/common/base/rtl.scss b/app/assets/stylesheets/common/base/rtl.scss index 9a6dfeb7ed..804af0bc4b 100644 --- a/app/assets/stylesheets/common/base/rtl.scss +++ b/app/assets/stylesheets/common/base/rtl.scss @@ -31,3 +31,10 @@ border-left-color: transparent !important; border-right-color: $secondary !important; } +.rtl code { + direction: ltr !important; + text-aligh: left !important; +} +.rtl .pull-left { + float:right !important; +} diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 930fe7cdc4..3f53b0d2cc 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -55,12 +55,6 @@ } > tbody > tr { - &:nth-child(odd) { - background-color: darken($secondary, 3%); - } - &:nth-child(even) { - background-color: $secondary; - } &.highlighted { background-color: dark-light-diff($tertiary, $secondary, 90%, -41%); } @@ -184,6 +178,7 @@ position: relative; } > tbody > tr { + border-bottom: none; &:nth-child(odd) { background-color: darken($secondary, 3%); } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 901bdb96e3..01c75c452c 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -462,16 +462,6 @@ a.star { #suggested-topics { clear: left; padding: 20px 0 15px 0; - .topic-list { - > tbody > tr { - &:nth-child(odd) { - background-color: darken($secondary, 3%); - } - &:nth-child(even) { - background-color: $secondary; - } - } - } table { table-layout: fixed; margin-top: 10px; diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index 06ac6b7f91..7707ec9056 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -113,7 +113,6 @@ a:hover.reply-new { width: 0; bottom: 0; z-index: 500; - outline: 1px solid transparent; &.docked { position: absolute; bottom: -70px; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 1ce508b649..144308885b 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -582,14 +582,6 @@ } .paginated-topics-list { - .topic-list > tbody > tr:nth-child(odd) { - background-color: darken($secondary, 3%); - } - - .topic-list > tbody > tr:nth-child(even) { - background-color: $secondary; - } - .user-content { width: 100%; margin-top: 0; diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index 58636be4cf..91bf389d17 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -17,6 +17,7 @@ @import "mobile/upload"; @import "mobile/user"; @import "mobile/history"; +@import "mobile/directory"; /* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ diff --git a/app/assets/stylesheets/mobile/directory.scss b/app/assets/stylesheets/mobile/directory.scss new file mode 100644 index 0000000000..0442df9d3c --- /dev/null +++ b/app/assets/stylesheets/mobile/directory.scss @@ -0,0 +1,49 @@ +.directory { + .period-chooser button { + margin: 0; + } + .period-chooser { + li { + text-align: left; + } + } +} + +.user-controls { + padding: 1em; +} + +.total-rows { + padding: 0.25em 0.5em; +} + +.user { + border-top: 1px solid dark-light-diff($primary, $secondary, 80%, -20%); + padding: 1em; + + + &.me { + background-color: dark-light-diff($highlight, $secondary, 70%, -80%); + + .username a, .name, .title, .number, .time-read, .user-stat .label { + color: scale-color($highlight, $lightness: -50%); + } + } + .user-stat { + margin-left: 55px; + font-size: 13px; + + .value { + font-weight: bold; + } + .label { + margin-left: 0.2em; + color: dark-light-diff($primary, $secondary, 50%, -50%); + } + + i.fa-heart { + color: $love; + } + } + margin-bottom: 1em; +} diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index c37eabeea6..15920f147f 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -30,16 +30,10 @@ .topic-list { > tbody > tr { - &:nth-child(even) { - background-color: darken($secondary, 3%); - } - &:nth-child(odd) { - background-color: $secondary; - } &.highlighted { background-color: scale-color($tertiary, $lightness: 85%); } - } + } th, td { diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 313bcfcd7c..856218de87 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -58,7 +58,6 @@ bottom: 0; z-index: 500; margin-right: 145px; - outline: 1px solid transparent; } #topic-progress-expanded { diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index b723606ffe..f73c9acafd 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -579,18 +579,10 @@ .paginated-topics-list { margin-top: 20px; - .topic-list > tbody > tr:nth-child(odd) { - background-color: darken($secondary, 3%); - } - - .topic-list > tbody > tr:nth-child(even) { - background-color: $secondary; - } - - .user-content { - width: 100%; - margin-top: 0; - } + .user-content { + width: 100%; + margin-top: 0; + } } .user-archive { diff --git a/app/controllers/admin/api_controller.rb b/app/controllers/admin/api_controller.rb index 57d9bce4e9..9fd25eecfb 100644 --- a/app/controllers/admin/api_controller.rb +++ b/app/controllers/admin/api_controller.rb @@ -6,7 +6,7 @@ class Admin::ApiController < Admin::AdminController def regenerate_key api_key = ApiKey.find_by(id: params[:id]) - raise Discourse::NotFound.new if api_key.blank? + raise Discourse::NotFound if api_key.blank? api_key.regenerate!(current_user) render_serialized(api_key, ApiKeySerializer) @@ -14,7 +14,7 @@ class Admin::ApiController < Admin::AdminController def revoke_key api_key = ApiKey.find_by(id: params[:id]) - raise Discourse::NotFound.new if api_key.blank? + raise Discourse::NotFound if api_key.blank? api_key.destroy render nothing: true diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index fe6408ccd8..cd9b7c8541 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -5,7 +5,7 @@ class Admin::ReportsController < Admin::AdminController def show report_type = params[:type] - raise Discourse::NotFound.new unless report_type =~ /^[a-z0-9\_]+$/ + raise Discourse::NotFound unless report_type =~ /^[a-z0-9\_]+$/ start_date = 1.month.ago start_date = Time.parse(params[:start_date]) if params[:start_date].present? @@ -14,7 +14,7 @@ class Admin::ReportsController < Admin::AdminController end_date = Time.parse(params[:end_date]) if params[:end_date].present? report = Report.find(report_type, {start_date: start_date, end_date: end_date}) - raise Discourse::NotFound.new if report.blank? + raise Discourse::NotFound if report.blank? render_json_dump(report: report) end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index c1194709f5..977d1e28d7 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -38,7 +38,7 @@ class Admin::UsersController < Admin::AdminController def show @user = User.find_by(username_lower: params[:id]) - raise Discourse::NotFound.new unless @user + raise Discourse::NotFound unless @user render_serialized(@user, AdminDetailedUserSerializer, root: false) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1c9bdbbd8e..6775cdf539 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -261,7 +261,7 @@ class ApplicationController < ActionController::Base elsif params[:external_id] SingleSignOnRecord.find_by(external_id: params[:external_id]).try(:user) end - raise Discourse::NotFound.new if user.blank? + raise Discourse::NotFound if user.blank? guardian.ensure_can_see!(user) user diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index 4eaaa4c378..033d47a18a 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -21,7 +21,7 @@ class EmbedController < ApplicationController @second_post_url = "#{@topic_view.topic.url}/2" if @topic_view @posts_left = 0 if @topic_view && @topic_view.posts.size == SiteSetting.embed_post_limit - @posts_left = @topic_view.topic.posts_count - SiteSetting.embed_post_limit + @posts_left = @topic_view.topic.posts_count - SiteSetting.embed_post_limit - 1 end else Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 2cc4669084..e6e30a54c1 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -225,11 +225,11 @@ class ListController < ApplicationController parent_category_id = nil if parent_slug_or_id.present? parent_category_id = Category.query_parent_category(parent_slug_or_id) - raise Discourse::NotFound.new if parent_category_id.blank? + raise Discourse::NotFound if parent_category_id.blank? end @category = Category.query_category(slug_or_id, parent_category_id) - raise Discourse::NotFound.new if !@category + raise Discourse::NotFound if !@category @description_meta = @category.description guardian.ensure_can_see!(@category) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 7325fc8fac..6a42a5bc67 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,56 +1,51 @@ +require_dependency 'notification_serializer' + class NotificationsController < ApplicationController before_filter :ensure_logged_in - def recent - notifications = Notification.recent_report(current_user, 10) + def index + user = current_user + if params[:recent].present? + notifications = Notification.recent_report(current_user, 10) - if notifications.present? - # ordering can be off due to PMs - max_id = notifications.map(&:id).max - current_user.saw_notification_id(max_id) unless params.has_key?(:silent) + if notifications.present? + # ordering can be off due to PMs + max_id = notifications.map(&:id).max + current_user.saw_notification_id(max_id) unless params.has_key?(:silent) + end + current_user.reload + current_user.publish_notifications_state + + render_serialized(notifications, NotificationSerializer, root: :notifications) + else + offset = params[:offset].to_i + user = User.find_by_username(params[:username].to_s) if params[:username] + + guardian.ensure_can_see_notifications!(user) + + notifications = Notification.where(user_id: user.id) + .visible + .includes(:topic) + .order(created_at: :desc) + + total_rows = notifications.dup.count + notifications = notifications.offset(offset).limit(60) + render_json_dump(notifications: serialize_data(notifications, NotificationSerializer), + total_rows_notifications: total_rows, + load_more_notifications: notifications_path(username: user.username, offset: offset + 60)) end - current_user.reload - current_user.publish_notifications_state - render_serialized(notifications, NotificationSerializer) end - def history - params.permit(:before, :user) - params[:before] ||= 1.day.from_now - - user = current_user - user = User.find_by_username(params[:user].to_s) if params[:user] - - unless guardian.can_see_notifications?(user) - return render json: {errors: [I18n.t('js.errors.reasons.forbidden')]}, status: 403 - end - - notifications = Notification.where(user_id: user.id) - .visible - .includes(:topic) - .limit(60) - .where('created_at < ?', params[:before]) - .order(created_at: :desc) - - render_serialized(notifications, NotificationSerializer) - end - - def reset_new - params.permit(:user) - - user = current_user - if params[:user] - user = User.find_by_username(params[:user].to_s) - end - - Notification.where(user_id: user.id).includes(:topic).where(read: false).update_all(read: true) + def mark_read + Notification.where(user_id: current_user.id).includes(:topic).where(read: false).update_all(read: true) current_user.saw_notification_id(Notification.recent_report(current_user, 1).max) current_user.reload current_user.publish_notifications_state - render nothing: true + render json: success_json end + end diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index 8ab55b2e26..397ce503fb 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -85,7 +85,7 @@ class UserBadgesController < ApplicationController else badge = Badge.find_by(name: params[:badge_name], enabled: true) end - raise Discourse::NotFound.new if badge.blank? + raise Discourse::NotFound if badge.blank? badge end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3858dd11ca..dc168a565d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -157,7 +157,7 @@ class UsersController < ApplicationController redirect_to path("/users/#{current_user.username}/#{params[:path]}") return end - raise Discourse::NotFound.new + raise Discourse::NotFound end def invited @@ -436,7 +436,7 @@ class UsersController < ApplicationController end def account_created - @message = session['user_created_message'] + @message = session['user_created_message'] || I18n.t('activation.missing_session') expires_now render layout: 'no_ember' end diff --git a/app/jobs/scheduled/create_missing_avatars.rb b/app/jobs/scheduled/create_missing_avatars.rb index 42a2b01dc8..8526b41155 100644 --- a/app/jobs/scheduled/create_missing_avatars.rb +++ b/app/jobs/scheduled/create_missing_avatars.rb @@ -1,13 +1,15 @@ module Jobs class CreateMissingAvatars < Jobs::Scheduled every 1.hour + def execute(args) - # backfill in batches 5000 an hour - UserAvatar.where(last_gravatar_download_attempt: nil).includes(:user) - .order("users.last_posted_at desc") - .limit(5000).each do |u| + # backfill in batches of 5000 an hour + UserAvatar.includes(:user) + .where(last_gravatar_download_attempt: nil) + .order("users.last_posted_at DESC") + .limit(5000) + .each do |u| u.user.refresh_avatar - u.user.save end end end diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index af2e01ad99..7ae1b8810a 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -142,7 +142,7 @@ class UserNotifications < ActionMailer::Base title: post.topic.title, post: post, username: post.user.username, - from_alias: (SiteSetting.enable_names && SiteSetting.display_name_on_posts && !post.user.name.empty?) ? post.user.name : post.user.username, + from_alias: (SiteSetting.enable_names && SiteSetting.display_name_on_posts && post.user.name.present?) ? post.user.name : post.user.username, allow_reply_by_email: true, use_site_subject: true, add_re_to_subject: true, diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index 666749126f..30f2da1b4b 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -49,7 +49,7 @@ class DirectoryItem < ActiveRecord::Base SUM(CASE WHEN ua.action_type = :reply_type THEN 1 ELSE 0 END) FROM users AS u LEFT OUTER JOIN user_actions AS ua ON ua.user_id = u.id - LEFT OUTER JOIN topics AS t ON ua.target_topic_id = t.id + LEFT OUTER JOIN topics AS t ON ua.target_topic_id = t.id AND t.archetype = 'regular' LEFT OUTER JOIN posts AS p ON ua.target_post_id = p.id LEFT OUTER JOIN categories AS c ON t.category_id = c.id WHERE u.active @@ -57,7 +57,6 @@ class DirectoryItem < ActiveRecord::Base AND COALESCE(ua.created_at, :since) >= :since AND t.deleted_at IS NULL AND COALESCE(t.visible, true) - AND COALESCE(t.archetype, 'regular') = 'regular' AND p.deleted_at IS NULL AND (NOT (COALESCE(p.hidden, false))) AND COALESCE(p.post_type, :regular_post_type) != :moderator_action diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 4db846f93b..db2387923a 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -120,7 +120,7 @@ class DiscourseSingleSignOn < SingleSignOn end if SiteSetting.sso_overrides_name && user.name != name - user.name = name || User.suggest_name(username || email) + user.name = name || User.suggest_name(username.blank? ? email : username) end if SiteSetting.sso_overrides_avatar && avatar_url.present? && ( diff --git a/app/models/incoming_link.rb b/app/models/incoming_link.rb index 9064b0d0d0..fd3e9e8e8d 100644 --- a/app/models/incoming_link.rb +++ b/app/models/incoming_link.rb @@ -14,7 +14,9 @@ class IncomingLink < ActiveRecord::Base user_id, host, referer = nil current_user = opts[:current_user] - if username = opts[:username] + username = opts[:username] + username = nil unless String === username + if username u = User.select(:id).find_by(username_lower: username.downcase) user_id = u.id if u end diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index b556229957..cbfb901ab2 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -6,72 +6,74 @@ class OptimizedImage < ActiveRecord::Base def self.create_for(upload, width, height, opts={}) return unless width > 0 && height > 0 - # do we already have that thumbnail? - thumbnail = find_by(upload_id: upload.id, width: width, height: height) + DistributedMutex.synchronize("optimized_image_#{upload.id}_#{width}_#{height}") do + # do we already have that thumbnail? + thumbnail = find_by(upload_id: upload.id, width: width, height: height) - # make sure the previous thumbnail has not failed - if thumbnail && thumbnail.url.blank? - thumbnail.destroy - thumbnail = nil - end - - # return the previous thumbnail if any - return thumbnail unless thumbnail.nil? - - # create the thumbnail otherwise - external_copy = Discourse.store.download(upload) if Discourse.store.external? - original_path = if Discourse.store.external? - external_copy.try(:path) - else - Discourse.store.path_for(upload) - end - - if original_path.blank? - Rails.logger.error("Could not find file in the store located at url: #{upload.url}") - else - # create a temp file with the same extension as the original - extension = File.extname(original_path) - temp_file = Tempfile.new(["discourse-thumbnail", extension]) - temp_path = temp_file.path - - if extension =~ /\.svg$/i - FileUtils.cp(original_path, temp_path) - resized = true - else - resized = resize(original_path, temp_path, width, height, opts) + # make sure the previous thumbnail has not failed + if thumbnail && thumbnail.url.blank? + thumbnail.destroy + thumbnail = nil end - if resized - thumbnail = OptimizedImage.create!( - upload_id: upload.id, - sha1: Digest::SHA1.file(temp_path).hexdigest, - extension: extension, - width: width, - height: height, - url: "", - ) - # store the optimized image and update its url - url = Discourse.store.store_optimized_image(temp_file, thumbnail) - if url.present? - thumbnail.url = url - thumbnail.save + # return the previous thumbnail if any + return thumbnail unless thumbnail.nil? + + # create the thumbnail otherwise + external_copy = Discourse.store.download(upload) if Discourse.store.external? + original_path = if Discourse.store.external? + external_copy.try(:path) + else + Discourse.store.path_for(upload) + end + + if original_path.blank? + Rails.logger.error("Could not find file in the store located at url: #{upload.url}") + else + # create a temp file with the same extension as the original + extension = File.extname(original_path) + temp_file = Tempfile.new(["discourse-thumbnail", extension]) + temp_path = temp_file.path + + if extension =~ /\.svg$/i + FileUtils.cp(original_path, temp_path) + resized = true else - Rails.logger.error("Failed to store avatar #{size} for #{upload.url} from #{source}") + resized = resize(original_path, temp_path, width, height, opts) end - else - Rails.logger.error("Failed to create optimized image #{width}x#{height} for #{upload.url}") + + if resized + thumbnail = OptimizedImage.create!( + upload_id: upload.id, + sha1: Digest::SHA1.file(temp_path).hexdigest, + extension: extension, + width: width, + height: height, + url: "", + ) + # store the optimized image and update its url + url = Discourse.store.store_optimized_image(temp_file, thumbnail) + if url.present? + thumbnail.url = url + thumbnail.save + else + Rails.logger.error("Failed to store avatar #{size} for #{upload.url} from #{source}") + end + else + Rails.logger.error("Failed to create optimized image #{width}x#{height} for #{upload.url}") + end + + # close && remove temp file + temp_file.close! end - # close && remove temp file - temp_file.close! - end + # make sure we remove the cached copy from external stores + if Discourse.store.external? + external_copy.try(:close!) rescue nil + end - # make sure we remove the cached copy from external stores - if Discourse.store.external? - external_copy.try(:close!) rescue nil + thumbnail end - - thumbnail end def destroy diff --git a/app/models/upload.rb b/app/models/upload.rb index f9507aca8e..d604dd0b85 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -52,43 +52,45 @@ class Upload < ActiveRecord::Base def self.create_for(user_id, file, filename, filesize, options = {}) sha1 = Digest::SHA1.file(file).hexdigest - # do we already have that upload? - upload = find_by(sha1: sha1) + DistributedMutex.synchronize("upload_#{sha1}") do + # do we already have that upload? + upload = find_by(sha1: sha1) - # make sure the previous upload has not failed - if upload && upload.url.blank? - upload.destroy - upload = nil + # make sure the previous upload has not failed + if upload && upload.url.blank? + upload.destroy + upload = nil + end + + # return the previous upload if any + return upload unless upload.nil? + + # create the upload otherwise + upload = Upload.new + upload.user_id = user_id + upload.original_filename = filename + upload.filesize = filesize + upload.sha1 = sha1 + upload.url = "" + upload.origin = options[:origin][0...1000] if options[:origin] + + # deal with width & height for images + upload = resize_image(filename, file, upload) if FileHelper.is_image?(filename) + + return upload unless upload.save + + # store the file and update its url + url = Discourse.store.store_upload(file, upload, options[:content_type]) + if url.present? + upload.url = url + upload.save + else + upload.errors.add(:url, I18n.t("upload.store_failure", { upload_id: upload.id, user_id: user_id })) + end + + # return the uploaded file + upload end - - # return the previous upload if any - return upload unless upload.nil? - - # create the upload otherwise - upload = Upload.new - upload.user_id = user_id - upload.original_filename = filename - upload.filesize = filesize - upload.sha1 = sha1 - upload.url = "" - upload.origin = options[:origin][0...1000] if options[:origin] - - # deal with width & height for images - upload = resize_image(filename, file, upload) if FileHelper.is_image?(filename) - - return upload unless upload.save - - # store the file and update its url - url = Discourse.store.store_upload(file, upload, options[:content_type]) - if url.present? - upload.url = url - upload.save - else - upload.errors.add(:url, I18n.t("upload.store_failure", { upload_id: upload.id, user_id: user_id })) - end - - # return the uploaded file - upload end def self.resize_image(filename, file, upload) diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 0e979991c0..0d4c661d10 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -142,6 +142,7 @@ SQL SELECT a.id, t.title, a.action_type, a.created_at, t.id topic_id, + t.closed AS topic_closed, t.archived AS topic_archived, a.user_id AS target_user_id, au.name AS target_name, au.username AS target_username, coalesce(p.post_number, 1) post_number, p.id as post_id, p.reply_to_post_number, diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 73431a9610..0a14ec745c 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -10,27 +10,31 @@ class UserAvatar < ActiveRecord::Base end def update_gravatar! - # special logic for our system user, we do not want the discourse email there - email_hash = user.id == -1 ? User.email_hash("info@discourse.org") : user.email_hash + DistributedMutex.synchronize("update_gravatar_#{user.id}") do + begin + # special logic for our system user + email_hash = user.id == Discourse::SYSTEM_USER_ID ? User.email_hash("info@discourse.org") : user.email_hash - self.last_gravatar_download_attempt = Time.new - gravatar_url = "http://www.gravatar.com/avatar/#{email_hash}.png?s=500&d=404" - tempfile = FileHelper.download(gravatar_url, SiteSetting.max_image_size_kb.kilobytes, "gravatar") + self.last_gravatar_download_attempt = Time.new - upload = Upload.create_for(user.id, tempfile, 'gravatar.png', tempfile.size, { origin: gravatar_url }) + gravatar_url = "http://www.gravatar.com/avatar/#{email_hash}.png?s=500&d=404" + tempfile = FileHelper.download(gravatar_url, SiteSetting.max_image_size_kb.kilobytes, "gravatar") + upload = Upload.create_for(user.id, tempfile, 'gravatar.png', tempfile.size, { origin: gravatar_url }) - if gravatar_upload_id != upload.id - gravatar_upload.try(:destroy!) - self.gravatar_upload = upload - save! + if gravatar_upload_id != upload.id + gravatar_upload.try(:destroy!) + self.gravatar_upload = upload + save! + end + rescue OpenURI::HTTPError + save! + rescue SocketError + # skip saving, we are not connected to the net + Rails.logger.warn "Failed to download gravatar, socket error - user id #{user.id}" + ensure + tempfile.try(:close!) + end end - rescue OpenURI::HTTPError - save! - rescue SocketError - # skip saving, we are not connected to the net - Rails.logger.warn "Failed to download gravatar, socket error - user id #{user.id}" - ensure - tempfile.close! if tempfile && tempfile.respond_to?(:close!) end end diff --git a/app/serializers/notification_serializer.rb b/app/serializers/notification_serializer.rb index 50d0b1ef7b..28d92f19c5 100644 --- a/app/serializers/notification_serializer.rb +++ b/app/serializers/notification_serializer.rb @@ -1,6 +1,7 @@ class NotificationSerializer < ApplicationSerializer - attributes :notification_type, + attributes :id, + :notification_type, :read, :created_at, :post_number, diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 503aabe626..ccf02e031c 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -26,6 +26,8 @@ class UserActionSerializer < ApplicationSerializer :edit_reason, :category_id, :uploaded_avatar_id, + :closed, + :archived, :acting_uploaded_avatar_id def excerpt @@ -77,4 +79,12 @@ class UserActionSerializer < ApplicationSerializer object.action_type == UserAction::EDIT end + def closed + object.topic_closed + end + + def archived + object.topic_archived + end + end diff --git a/app/services/anonymous_shadow_creator.rb b/app/services/anonymous_shadow_creator.rb index 3441455d8c..f2abd9e1b1 100644 --- a/app/services/anonymous_shadow_creator.rb +++ b/app/services/anonymous_shadow_creator.rb @@ -33,7 +33,7 @@ class AnonymousShadowCreator User.transaction do shadow = User.create!( password: SecureRandom.hex, - email: "#{SecureRandom.hex}@#{SecureRandom.hex}.com", + email: "#{SecureRandom.hex}@anon.#{Discourse.current_hostname}", name: "", username: UserNameSuggester.suggest(I18n.t(:anonymous).downcase), active: true, diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 7beae7ecb8..a075563052 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -36,8 +36,6 @@ PreloadStore.get("customEmoji").forEach(function(emoji) { Discourse.Dialect.registerEmoji(emoji.name, emoji.url); }); - Discourse.Router = Ember.Router.extend({ location: 'discourse-location' }); - Discourse.Route.mapRoutes(); Discourse.start(); Discourse.set('assetVersion','<%= Discourse.assets_digest %>'); Discourse.Session.currentProp("disableCustomCSS", <%= loading_admin? %>); diff --git a/app/views/list/list.rss.erb b/app/views/list/list.rss.erb index 63684a5e82..deb5e80f02 100644 --- a/app/views/list/list.rss.erb +++ b/app/views/list/list.rss.erb @@ -11,7 +11,7 @@ <%= @topic_list.topics.first.created_at.rfc2822 %> <% @topic_list.topics.each do |topic| %> - <% topic_url = topic.relative_url -%> + <% topic_url = topic.url -%> <%= topic.title %> <%= "no-reply@example.com (@#{topic.user.username}#{" #{topic.user.name}" if (topic.user.name.present? && SiteSetting.enable_names?)})" -%> diff --git a/app/views/topics/show.rss.erb b/app/views/topics/show.rss.erb index dbfda1d2e9..03b5471ecb 100644 --- a/app/views/topics/show.rss.erb +++ b/app/views/topics/show.rss.erb @@ -1,7 +1,7 @@ - <% topic_url = @topic_view.relative_url %> + <% topic_url = @topic_view.absolute_url %> <% lang = SiteSetting.find_by_name('default_locale').try(:value) %> <% site_email = SiteSetting.find_by_name('contact_email').try(:value) %> <%= @topic_view.title %> @@ -31,7 +31,7 @@ <%= post_url %> <%= post.created_at.rfc2822 %> post-<%= post.topic_id %>-<%= post.post_number %> - <%= @topic_view.title %> + <%= @topic_view.title %> <% end %> diff --git a/config/cloud/cloud66/scripts/curl.sh b/config/cloud/cloud66/scripts/curl.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/drop_create.sh b/config/cloud/cloud66/scripts/drop_create.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/env_vars.sh b/config/cloud/cloud66/scripts/env_vars.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/import_dev.sh b/config/cloud/cloud66/scripts/import_dev.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/import_prod.sh b/config/cloud/cloud66/scripts/import_prod.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/kill_db.sh b/config/cloud/cloud66/scripts/kill_db.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/migrate.sh b/config/cloud/cloud66/scripts/migrate.sh old mode 100644 new mode 100755 diff --git a/config/cloud/cloud66/scripts/permissions.sh b/config/cloud/cloud66/scripts/permissions.sh old mode 100644 new mode 100755 diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index c1d862dba9..013e1b3c36 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -20,14 +20,9 @@ if Rails.env.production? # /(?m).*?Line: (?:\D|0).*?Column: (?:\D|0)/, - # suppress trackback spam bots - Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { REQUEST_URI: /\/trackback\/$/ }), - # suppress trackback spam bots submitting to random URLs - # test for the presence of these params: url, title, excerpt, blog_name - Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { params: { url: /./, title: /./, excerpt: /./, blog_name: /./} }), - - # API calls, TODO fix this in rails - Logster::IgnorePattern.new("Can't verify CSRF token authenticity", { REQUEST_URI: /api_key/ }) + # CSRF errors are not providing enough data + # suppress unconditionally for now + /^Can't verify CSRF token authenticity$/ ] end diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index e983876416..7887bcf923 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -42,7 +42,7 @@ ar: two: "> ثانيتان" few: "> %{count}ث" many: "> %{count}ث" - other: "> %{count}ث" + other: "%{count} ثانية" x_seconds: zero: "%{count} ثانية" one: "%{count} ثانية" @@ -1776,6 +1776,25 @@ ar: block_explanation: "المستخدم الموقوف لايستطيع أن يشارك" trust_level_change_failed: "هناك مشكلة في تغيير مستوى ثقة المستخدم " grant_admin_failed: "هناك مشكلة في الحصول على صلاحية المدير" + tl3_requirements: + posts_read_all_time: "المشاركات المقروءة (جميع الاوقات)" + flagged_posts: "المشاركات المبلغ عنها " + flagged_by_users: "المستخدمين الذين بلغوا" + likes_given: "الإعجابات المعطاة" + likes_received: "الإعجابات المستلمة" + likes_received_days: "الإعجابات المستلمة : الايام الغير عادية" + likes_received_users: "الإعجابات المستلمة : المستخدمين المميزين" + qualifies: "مستوى الثقة الممنوحة للمستوى " + does_not_qualify: "غير مستحق للمستوى" + will_be_promoted: "سيتم الترقية عنه قريبا" + will_be_demoted: "سيتم التخفيض قريبا" + locked_will_not_be_promoted: "مستوى الثفة هذا لن يتم الترقية له نهائيا" + locked_will_not_be_demoted: "مستوى الثفة هذا لن يتم الخفض له نهائيا" + sso: + external_username: "اسم المستخدم" + external_name: "الاسم" + external_email: "البريد الإلكتروني" + external_avatar_url: "رابط الملف الشخصي" site_settings: none: 'لا شيء' badges: diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 3f2b4f7d50..a45084974f 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -211,7 +211,7 @@ de: one: "Dieses Thema hat 1 Beitrag, der genehmigt werden muss" many: "Dieses Thema hat {{count}} Beiträge, die genehmigt werden müssen" confirm: "Änderungen speichern" - delete_prompt: "Bist du sicher, dass du %{username} löschen möchtest? Damit werden alle ihre/seine Beiträge entfernt und ihre/seine E-Mail- und IP-Adresse geblockt." + delete_prompt: "Bist du sicher, dass du %{username} löschen möchtest? Es werden alle Beiträge des Benutzers gelöscht und dessen E-Mail- und IP-Adresse geblockt." approval: title: "Beitrag muss genehmigt werden" description: "Wir haben deinen neuen Beitrag erhalten. Dieser muss allerdings zunächst durch einen Moderator freigeschaltet werden. Bitte habe etwas Geduld. " @@ -334,6 +334,7 @@ de: dismiss_notifications: "Alle als gelesen markieren" dismiss_notifications_tooltip: "Alle ungelesenen Benachrichtigungen als gelesen markieren" disable_jump_reply: "Springe nicht zu meinem Beitrag, nachdem ich geantwortet habe" + dynamic_favicon: "Zeige die Anzahl der neuen und geänderte Themen im Browser-Symbol" edit_history_public: "Andere Benutzer dürfen in Beiträgen meine Überarbeitungen sehen." external_links_in_new_tab: "Öffne alle externen Links in einem neuen Tab" enable_quoting: "Aktiviere Zitatantwort mit dem hervorgehobenen Text" @@ -556,8 +557,9 @@ de: logout: "Du wurdest abgemeldet." refresh: "Aktualisieren" read_only_mode: - enabled: "Nur-Lesen-Modus ist aktiviert. Du kannst die Website weiter nutzen, wobei jedoch einige Funktionen nicht funktionieren werden." + enabled: "Der Nur-Lesen-Modus ist aktiviert. Du kannst die Website weiter durchsuchen und lesen. Einige Funktionen werden jedoch wahrscheinlich nicht funktionieren." login_disabled: "Die Anmeldung ist deaktiviert während sich die Website im Nur-Lesen-Modus befindet." + too_few_topics_notice: "Erstelle mindestens 5 öffentliche Themen und %{posts} öffentliche Antworten, um die Diskussion ins Rollen zu bringen. Neue Benutzer können ihre Vertrauensstufe nur erhöhen, wenn es für sie etwas zu Lesen gibt. Diese Meldung erscheint nur für Mitarbeiter." learn_more: "mehr erfahren..." year: 'Jahr' year_desc: 'Themen, die in den letzten 365 Tagen erstellt wurden' @@ -742,6 +744,13 @@ de: moved_post: "

{{username}} hat {{description}} verschoben

" linked: "

{{username}} {{description}}

" granted_badge: "

Abzeichen '{{description}}' erhalten

" + popup: + mentioned: '{{username}} hat dich in "{{topic}}" - {{site_title}} erwähnt' + quoted: '{{username}} hat dich in "{{topic}}" - {{site_title}} zitiert' + replied: '{{username}} hat dir in "{{topic}}" - {{site_title}} geantwortet' + posted: '{{username}} hat in "{{topic}}" - {{site_title}} einen Beitrag verfasst' + private_message: '{{username}} hat dir in "{{topic}}" - {{site_title}} eine Nachricht geschickt' + linked: '{{username}} hat in "{{topic}}" - {{site_title}} einen Beitrag von dir verlinkt' upload_selector: title: "Ein Bild hinzufügen" title_with_attachments: "Ein Bild oder eine Datei hinzufügen" @@ -2181,7 +2190,7 @@ de: add: "Neues Emoji hinzufügen" name: "Name" image: "Bild" - delete_confirm: "Möchtest du wirklich das %{name}: Emoji löschen?" + delete_confirm: "Möchtest du wirklich das :%{name}: Emoji löschen?" lightbox: download: "herunterladen" search_help: diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index b961a02554..3290747e35 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -333,6 +333,7 @@ es: dismiss_notifications: "Marcador todos como leídos" dismiss_notifications_tooltip: "Marcar todas las notificaciones no leídas como leídas" disable_jump_reply: "No dirigirme a mi post cuando responda" + dynamic_favicon: "Mostrar contador de temas nuevos/actualizados en el favicon" edit_history_public: "Dejar que otros usuarios puedan ver las revisiones de mis posts" external_links_in_new_tab: "Abrir todos los enlaces externos en una nueva pestaña" enable_quoting: "Activar respuesta citando el texto resaltado" @@ -493,7 +494,7 @@ es: pending: "Invitaciones Pendientes" topics_entered: "Temas Vistos" posts_read_count: "Posts leídos" - expired: "Esta invitación ha vencido." + expired: "Esta invitación ha caducado." rescind: "Remover" rescinded: "Invitación eliminada" reinvite: "Reenviar Invitación" @@ -503,7 +504,7 @@ es: account_age_days: "Antigüedad de la cuenta en días" create: "Enviar una Invitación" bulk_invite: - none: "No has invitado a nadie todavía. Puede enviar invitaciones individuales o invitar a un grupo de personas a la vez subiendo un archivo para invitaciones en masa." + none: "No has invitado a nadie todavía. Puedes enviar invitaciones individuales o invitar a un grupo de personas a la vez subiendo un archivo para invitaciones en masa." text: "Archivo de Invitación en Masa" uploading: "Subiendo..." success: "Archivo subido correctamente, se te notificará con un mensaje cuando se complete el proceso." @@ -557,6 +558,7 @@ es: read_only_mode: enabled: "Modo solo-lectura activado. Puedes continuar navegando por el sitio pero las interacciones podrían no funcionar." login_disabled: "Iniciar sesión está desactivado mientras el foro esté en modo solo lectura." + too_few_topics_notice: "Crea al menos 5 temas y %{posts} posts públicos para considerar iniciada la discusión. Los nuevos usuarios no podrán ganar niveles de confianza a menos que haya contenido para que lean. Este mensaje sólo les aparecerá a los miembros del staff." learn_more: "saber más..." year: 'año' year_desc: 'temas creados en los últimos 365 días' @@ -741,6 +743,13 @@ es: moved_post: "

{{username}} movió {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

Se te ha concedido '{{description}}'

" + popup: + mentioned: '{{username}} te mencionó en "{{topic}}" - {{site_title}}' + quoted: '{{username}} te citó en "{{topic}}" - {{site_title}}' + replied: '{{username}} te respondió en "{{topic}}" - {{site_title}}' + posted: '{{username}} publicó en "{{topic}}" - {{site_title}}' + private_message: '{{username}} te envió un mensaje privado en "{{topic}}" - {{site_title}}' + linked: '{{username}} enlazó tu publicación desde "{{topic}}" - {{site_title}}' upload_selector: title: "Añadir imagen" title_with_attachments: "Añadir una imagen o archivo" @@ -998,7 +1007,7 @@ es: email_placeholder: 'nombre@ejemplo.com' success_email: "Hemos enviado un email con tu invitación a {{emailOrUsername}}. Te notificaremos cuando se acepte. Puedes revisar la pestaña invitaciones en tu perfil de usuario para consultar el estado de tus invitaciones." success_username: "Hemos invitado a ese usuario a participar en este tema." - error: "Lo sentimos, no pudimos invitar a esa persona. Tal vez ya han sido invitados? (La tasa de invitaciones son limitadas)" + error: "Lo sentimos, no pudimos invitar a esa persona. Tal vez ya haya sido invitada. (La tasa de invitaciones es limitada)" login_reply: 'Inicia Sesión para Responder' filters: n_posts: @@ -2289,34 +2298,34 @@ es: name: Buen post description: Recibió 10 "me gusta" en un post. Este distintivo puede ser concedido varias veces good_post: - name: Muy buen post + name: Gran post description: Recibió 25 "me gusta" en un post. Este distintivo puede ser concedido varias veces great_post: - name: Gran Post + name: Excelente post description: Recibió 50 "me gusta" en un post. Este distintivo puede ser concedido varias veces nice_topic: - name: Lindo Tema + name: Buen tema description: Recibió 10 "me gusta" en un tema. Este distintivo puede ser concedido varias veces good_topic: - name: Buen Tema + name: Gran tema description: Recibió 25 "me gusta" en un tema. Este distintivo puede ser concedido varias veces great_topic: - name: Gran Tema + name: Excelente tema description: Recibió 50 "me gusta" en un tema. Este distintivo puede ser concedido varias veces nice_share: - name: Linda Contribución + name: Buena contribución description: Compartió un post con 25 visitantes únicos good_share: - name: Buena Contribución + name: Gran contribución description: Compartió un post con 300 visitantes únicos great_share: - name: Gran Contribución + name: Excelente contribución description: Compartió un post con 1000 visitantes únicos first_like: name: Primer "me gusta" description: Le dio a "me gusta" a un post first_flag: - name: Primer Reporte + name: Primer reporte description: Reportó un post first_share: name: Primer Compartido diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 889b5ba07b..ca7f28344f 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -307,6 +307,7 @@ fa_IR: dismiss_notifications: "علامت گذاری همه به عنوان خوانده شده" dismiss_notifications_tooltip: "علامت گذاری همه اطلاعیه های خوانده نشده به عنوان خوانده شده" disable_jump_reply: "بعد از پاسخ من به پست من پرش نکن" + dynamic_favicon: "Show new / updated topic count on browser icon" edit_history_public: "اجازه بده کاربران دیگر اصلاحات نوشته مرا ببینند" external_links_in_new_tab: "همهٔ پیوندهای برون‌رو را در یک تب جدید باز کن" enable_quoting: "فعال کردن نقل قول گرفتن از متن انتخاب شده" @@ -527,6 +528,7 @@ fa_IR: read_only_mode: enabled: "حالت فقط خواندن را فعال است. می توانید به جستجو در وب سایت ادامه دهید ولی ممکن است تعاملات کار نکند." login_disabled: "ورود به سیستم غیر فعال شده همزمان با اینکه سایت در حال فقط خواندنی است." + too_few_topics_notice: "Create at least 5 public topics and %{posts} public replies to get discussion started. New users cannot earn trust levels unless there's content for them to read. This message appears only to staff." learn_more: "بیشتر بدانید..." year: 'سال' year_desc: 'موضوعاتی که در 365 روز گذشته باز شده‌اند' @@ -710,6 +712,13 @@ fa_IR: moved_post: "

{{username}} moved {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

Earned '{{description}}'

" + popup: + mentioned: '{{username}} mentioned you in "{{topic}}" - {{site_title}}' + quoted: '{{username}} quoted you in "{{topic}}" - {{site_title}}' + replied: '{{username}} replied to you in "{{topic}}" - {{site_title}}' + posted: '{{username}} posted in "{{topic}}" - {{site_title}}' + private_message: '{{username}} sent you a private message in "{{topic}}" - {{site_title}}' + linked: '{{username}} linked to your post from "{{topic}}" - {{site_title}}' upload_selector: title: "افزودن یک عکس" title_with_attachments: "افزودن یک تصویر یا پرونده" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index ab340185d6..9ac15a7aec 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -333,6 +333,7 @@ fi: dismiss_notifications: "Merkitse kaikki luetuiksi" dismiss_notifications_tooltip: "Merkitse kaikki lukemattomat ilmoitukset luetuiksi" disable_jump_reply: "Älä siirry uuteen viestiini lähetettyäni sen" + dynamic_favicon: "Näytä uusien / päivittyneiden ketjujen määrä selaimen ikonissa" edit_history_public: "Anna muiden nähdä viestieni revisiot" external_links_in_new_tab: "Avaa sivuston ulkopuoliset linkit uudessa välilehdessä" enable_quoting: "Ota käyttöön viestin lainaaminen tekstiä valitsemalla" @@ -557,6 +558,7 @@ fi: read_only_mode: enabled: "Olet Vain luku -tilassa. Voit jatkaa selaamista, muttet välttämättä pysty vaikuttamaan sisältöön." login_disabled: "Kirjautuminen ei ole käytössä sivuston ollessa vain luku -tilassa." + too_few_topics_notice: "Luo vähintään 5 julkista ketjua ja %{posts} julkista vastausta saadaksesi keskustelun käyntiin. Uudet käyttäjät eivät voi saavuttaa korkeampia luottamustasoja, jos heillä ei ole tarpeeksi luettavaa sisältöä. Tämä viesti näytetään vain henkilökunnalle." learn_more: "opi lisää..." year: 'vuosi' year_desc: 'viimeisen 365 päivän aikana luodut ketjut' @@ -741,6 +743,13 @@ fi: moved_post: "

{{username}} siirsi {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

Ansaitsit '{{description}}'

" + popup: + mentioned: '{{username}} mainitsi sinut ketjussa "{{topic}}" - {{site_title}}' + quoted: '{{username}} lainasi sinua ketjussa "{{topic}}" - {{site_title}}' + replied: '{{username}} vastasi sinulle ketjussa "{{topic}}" - {{site_title}}' + posted: '{{username}} vastasi ketjuun "{{topic}}" - {{site_title}}' + private_message: '{{username}} lähetti sinulle yksityisviestin ketjussa "{{topic}}" - {{site_title}}' + linked: '{{username}} linkitti viestiisi aiheesta "{{topic}}" - {{site_title}}' upload_selector: title: "Lisää kuva" title_with_attachments: "Lisää kuva tai tidosto" @@ -1949,7 +1958,7 @@ fi: show_public_profile: "Näytä julkinen profiili" impersonate: 'Esiinny käyttäjänä' ip_lookup: "IP haku" - log_out: "Kirjaudu ulos" + log_out: "Kirjaa ulos" logged_out: "Käyttäjä on kirjautunut ulos kaikilla laitteilla" revoke_admin: 'Peru ylläpitäjän oikeudet' grant_admin: 'Myönnä ylläpitäjän oikeudet' @@ -1981,7 +1990,7 @@ fi: anonymize_failed: "Käyttäjätilin anonymisointi ei onnistunut." delete: "Poista käyttäjä" delete_forbidden_because_staff: "Ylläpitäjiä ja valvojia ei voi poistaa." - delete_posts_forbidden_because_staff: "Ylläpitäjien ja moderaattoreiden kaikkia viestejä ei voi poistaa." + delete_posts_forbidden_because_staff: "Ylläpitäjien ja valvojien kaikkia viestejä ei voi poistaa." delete_forbidden: one: "Käyttäjiä ei voi poistaa jos heillä on kirjoitettuja viestejä. Poista ensin viestit ennen käyttäjätilin poistamista. (Vanhempia viestejä, kuin %{count} päivä ei voi poistaa)" other: "Käyttäjiä ei voi poistaa jos heillä on kirjoitettuja viestejä. Poista ensin viestit ennen käyttäjätilin poistamista. (Vanhempia viestejä, kuin %{count} päivää ei voi poistaa)" @@ -2010,7 +2019,7 @@ fi: block_explanation: "Estetty käyttäjä ei voi luoda viestejä tai ketjuja." trust_level_change_failed: "Käyttäjän luottamustason vaihtamisessa tapahtui virhe." grant_admin_failed: "Ylläpitäjän oikeuksian antaminen ei onnistunut." - grant_moderation_failed: "Moderaattorin oikeuksien antaminen ei onnistunut." + grant_moderation_failed: "Valvojan oikeuksien myöntäminen ei onnistunut." suspend_modal_title: "Hyllytä käyttäjä" trust_level_2_users: "Käyttäjät luottamustasolla 2" trust_level_3_requirements: "Luottamustaso 3 vaatimukset" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 78870c0101..36a5ef3e28 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -107,6 +107,9 @@ he: show_more: "הראה עוד" show_help: "עזרה" links: "קישורים" + links_lowercase: + one: "קישור" + other: "קישורים" faq: "שאלות נפוצות" guidelines: "כללי התנהלות" privacy_policy: "מדיניות פרטיות" @@ -193,6 +196,28 @@ he: title: search: "חפש נושא לפי שם, כתובת או מזהה:" placeholder: "הקלד את כותרת הנושא כאן" + queue: + topic: "נושא:" + approve: 'לאשר' + reject: 'לדחות' + delete_user: 'מחק משתמש' + title: "זקוק לאישור" + none: "לא נותרו הודעות לבדיקה" + edit: "ערוך" + cancel: "ביטול" + view_pending: "הצג הודעות ממתינות" + has_pending_posts: + one: " בנושא זה ישנה הודעה אחת הממתינה לאישור" + many: "בנושא זה ישנם {{count}} הודעות הממתינות לאישור" + confirm: "שמור שינויים" + delete_prompt: "אתה בטוח שאתה רוצה למחוק את המשתמש %{username}? פעולה זו תמחק את כל ההודעות , תחסום את הדואר האלקטרוני וכתובת ה-IP של המשתמש." + approval: + title: "ההודעה זקוקה לאישור" + description: "קיבלנו את הודעתך אך נדרש אישור של מנחה לפני שההודעה תוצג, אנא המתן בסבלנות." + pending_posts: + one: "יש לך הודעה אחת ממתינה לאישור" + other: "יש לך {{count}} הודעות ממתינות." + ok: "אשר" user_action: user_posted_topic: "{{user}} פרסם את הנושא" you_posted_topic: "את/ה פרסמת את הנושא" @@ -254,6 +279,7 @@ he: '11': "עריכות" '12': "פריטים שנשלחו" '13': "דואר נכנס" + '14': "ממתין" categories: all: "כל הקטגוריות" all_subcategories: "הכל" @@ -307,6 +333,7 @@ he: dismiss_notifications: "סימון הכל כנקרא" dismiss_notifications_tooltip: "סימון כל ההתראות שלא נקראו כהתראות שנקראו" disable_jump_reply: "אל תקפצו לפרסומים שלי לאחר שאני משיב/ה" + dynamic_favicon: "הצג את מספר נושאים חדשים/מעודכנים על האייקון של הדפדפן" edit_history_public: "אפשרו למשתמשים אחרים לראות את תיקוני הפרסומים שלי" external_links_in_new_tab: "פתח את כל הקישורים החיצוניים בעמוד חדש" enable_quoting: "אפשרו תגובת ציטוט לטקסט מסומן" @@ -449,6 +476,7 @@ he: auto_track_topics: "מעקב אוטומטי נושאים אליהם נכנסתי" auto_track_options: never: "אף פעם" + immediately: "מיידי" after_n_seconds: one: "אחרי שנייה אחת" other: "אחרי {{count}} שניות" @@ -528,7 +556,9 @@ he: logout: "נותקת מהמערכת" refresh: "רענן" read_only_mode: + enabled: "מופעל מצב קריאה בלבד. אפשר להמשיך לגלוש באתר, אך חלק מהפעולות עלולות לא לעבוד." login_disabled: "התחברות אינה מתאפשרת כשהאתר במצב קריאה בלבד." + too_few_topics_notice: "צור לפחות 5 נושאים ציבוריים ו-%{posts} תגובות ציבוריות להתחיל את הדיון. משתמשים חדשים לא יכולים להשיג דרגות אמון אלא אם יש להם תוכן לקרוא. הודעה זו מופיעה רק לצוות." learn_more: "למד עוד..." year: 'שנה' year_desc: 'נושאים שפורסמו ב-365 הימים האחרונים' @@ -541,6 +571,10 @@ he: mute: השתק unmute: בטל השתקה last_post: הודעה אחרונה + last_reply_lowercase: תגובה אחרונה + replies_lowercase: + one: תגובה + other: תגובות summary: enabled_description: "אתם צופים בסיכום נושא זה: הפרסומים המעניינים ביותר כפי שסומנו על ידי הקהילה." description: "ישנן {{count}} תגובות" @@ -634,6 +668,7 @@ he: title_too_long: "על הכותרת להיות באורך {{max}} לכל היותר." post_missing: "ההודעה אינה יכולה להיות ריקה." post_length: "על ההודעה להיות באורך {{min}} תווים לפחות." + try_like: 'האם ניסית את כפתור ה-' category_missing: "עליך לבחור קטגוריה." save_edit: "שמירת עריכה" reply_original: "תגובה לנושא המקורי" @@ -707,6 +742,13 @@ he: moved_post: "

{{username}} הזיז/ה {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

הרוויח/ה '{{description}}'

" + popup: + mentioned: '{{username}} הזכיר אוך ב"{{topic}}" - {{site_title}}' + quoted: '{{username}} ציטט אותך ב"{{topic}}" - {{site_title}}' + replied: '{{username}} הגיב לך ב"{{topic}}" - {{site_title}}' + posted: '{{username}} הגיב ב"{{topic}}" - {{site_title}}' + private_message: '{{username}} שלח לך הודעה פרטית ב"{{topic}}" - {{site_title}}' + linked: '{{username}} קישר להודעה שלך מ"{{topic}}" - {{site_title}}' upload_selector: title: "הוספת תמונה" title_with_attachments: "הוספת תמונה או קובץ" @@ -942,12 +984,21 @@ he: email_or_username: "כתובת דואר אלקטרוני או שם משתמש של המוזמן" email_or_username_placeholder: "כתובת דואר אלקטרוני או שם משתמש" action: "הזמנה" + success: "הזמנו את המשתמש להשתתף בשיחה." error: "סליחה, הייתה שגיאה בהזמנת משתמש זה." group_name: "שם הקבוצה" invite_reply: title: 'הזמנה' + username_placeholder: "שם משתמש" + action: 'שלח הזמנה' + help: 'הזמן אנשים אחרים לנושא זה דרך דואר אלקטרוני או התראות' to_forum: "נשלח מייל קצר המאפשר לחברך להצטרף באופן מיידי באמצעות לחיצה על קישור, ללא צורך בהתחברות למערכת הפורומים." + sso_enabled: "הכנס את שם המשתמש של האדם שברצונך להזמין לנושא זה." + to_topic_blank: "הכנס את שם המשתמש או כתובת דואר האלקטרוני של האדם שברצונך להזמין לנושא זה." + to_topic_username: "הכנסת את שם המשתמש של האדם שברצונך להזמין. אנו נשלח התראה למשתמש זה עם קישור המזמין אותו לנושא זה." + to_username: "הכנס את שם המשתמש של האדם שברצונך להזמין. אנו נשלח התראה למשתמש זה עם קישור המזמין אותו לנושא זה." email_placeholder: 'name@example.com' + success_username: "הזמנו את המשתמש להשתתף בנושא." login_reply: 'התחברו כדי להשיב' filters: n_posts: @@ -991,8 +1042,10 @@ he: one: בחרת הודעה אחת. other: בחרת {{count}} הודעות. post: + reply: "השב ל {{link}} {{replyAvatar}} {{username}}" reply_topic: "תגובה ל {{link}}" quote_reply: "תגובה עם ציטוט" + edit: "עורך את {{link}} {{replyAvatar}} {{username}}" edit_reason: "סיבה: " post_number: "הודעה {{number}}" last_edited_on: "הודעה נערכה לאחרונה ב" @@ -1086,6 +1139,7 @@ he: inappropriate: "{{icons}} סומן בלתי ראוי" notify_moderators: "{{icons}} הודיעו למנהלים" notify_moderators_with_url: "{{icons}} הודיעו למנהלים" + notify_user: "{{icons}} שלח הודעה" bookmark: "{{icons}} סימנו כמועדף" like: "{{icons}} נתנו לייק" vote: "{{icons}} הצביעו עבור זה" @@ -1133,6 +1187,9 @@ he: notify_moderators: one: "אדם אחד דגלל את זה לניהול" other: "{{count}} אנשים סמנו את זה לניהול" + notify_user: + one: "אדם אחד שלח הודעה למשתמש זה" + other: "{{count}} שלחו הודעה למשתמש זה" bookmark: one: "אדם אחד סימן הודעה זו כמועדפת" other: "{{count}} אנשים סימנו הודעה זו כמועדפת" @@ -1357,6 +1414,16 @@ he: top: title: "מובילים" help: "הנושאים הפעילים ביותר בשנה, חודש, שבוע או יום האחרונים" + all: + title: "תמיד" + yearly: + title: "שנתי" + monthly: + title: "חודשי" + weekly: + title: "שבועי" + daily: + title: "יומי" this_year: "השנה" this_month: "החודש" this_week: "השבוע" @@ -1530,15 +1597,18 @@ he: read_only: enable: title: "אפשר מצב קריאה בלבד" + label: "אפשר מצב \"קריאה בלבד\"" confirm: "אתה בטוח שברצונך לאפשר את מצב קריאה בלבד??" disable: title: "בטל מצב קריאה בלבד" + label: "בטל מצב \"קריאה בלבד\"" logs: none: "עדיין אין לוגים..." columns: filename: "שם קובץ" size: "גודל" upload: + label: "העלה" uploading: "מעלה..." success: "'{{filename}}' הועלה בהצלחה." error: "הייתה שגיאה במהלך העלאת '{{filename}}': {{message}}" @@ -1546,19 +1616,23 @@ he: is_running: "פעולה רצה כרגע..." failed: "ה{{operation}} נכשלה. אנא בדוק את הלוגים." cancel: + label: "ביטול" title: "בטל את הפעולה הנוכחית" confirm: "אתה בטוח שברצונך לבטל את הפעולה הנוכחית?" backup: + label: "גבה" title: "צור גיבוי" confirm: "האם תרצו להתחיל גיבוי חדש?" without_uploads: "כן (ללא הכללת קבצים)" download: + label: "הורד" title: "הורד את הגיבוי" destroy: title: "הסר את הגיבוי" confirm: "אתה בטוח שברצונך להשמיד את הגיבוי הזה?" restore: is_disabled: "שחזור אינו מאופשר לפי הגדרות האתר." + label: "שחזר" title: "שחזר את הגיבוי" confirm: "אתה בטוח שברצונך לשחזר את הגיבוי הזה?" rollback: diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 3858975407..dd4c18d008 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -742,6 +742,8 @@ it: moved_post: "

{{username}} ha spostato {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

Guadagnato '{{description}}'

" + popup: + mentioned: '{{username}} ti ha menzionato in "{{topic}}" - {{site_title}}' upload_selector: title: "Aggiungi un'immagine" title_with_attachments: "Aggiungi un'immagine o un file" @@ -1335,12 +1337,21 @@ it: Questo argomento ha {count, plural, one {1 risposta} other {# risposte}} {ratio, select, low {con un alto rapporto "mi piace" / messaggi} med {con un altissimo rapporto "mi piace" / messaggi} high {con un estremamente alto rapporto "mi piace" / messaggi} other {}} original_post: "Messaggio Originale" views: "Visualizzazioni" + views_lowercase: + one: "vista" + other: "viste" replies: "Risposte" views_long: "questo argomento è stato visualizzato {{number}} volte" activity: "Attività" likes: "Mi piace" + likes_lowercase: + one: "mi piace" + other: "mi piace" likes_long: "ci sono {{number}} \"Mi piace\" in questo argomento" users: "Utenti" + users_lowercase: + one: "utente" + other: "utenti" category_title: "Categoria" history: "Storia" changed_by: "da {{author}}" @@ -1401,6 +1412,17 @@ it: top: title: "Di Punta" help: "gli argomenti più attivi nell'ultimo anno, mese, settimana o giorno" + all: + title: "Tutti" + yearly: + title: "Annuale" + monthly: + title: "Mensile" + weekly: + title: "Settimanale" + daily: + title: "Giornaliero" + all_time: "Tutti" this_year: "Quest'anno" this_month: "Questo mese" this_week: "Questa settimana" @@ -1624,6 +1646,7 @@ it: confirm: "Sicuro di voler ripristinare una precedente versione funzionante del database?" export_csv: user_archive_confirm: "Sei sicuro di voler scaricare i tuoi messaggi?" + success: "Esportazione iniziata, verrai avvertito con un messaggio al termine del processo." failed: "Esportazione fallita. Controlla i log." rate_limit_error: "I messaggi possono essere scaricati una volta al giorno, prova ancora domani." button_text: "Esporta" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 521aa3c527..bd85b52b8a 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -191,7 +191,7 @@ ja: confirm: "変更を保存" approval: title: "この投稿は承認が必要" - description: "投稿された新規のポストは受付されましたがモデレイターによる承認が必要です。もう少々お待ち下さい。" + description: "投稿された新規のポストは受付されましたがモデレーターによる承認が必要です。もう少々お待ち下さい。" ok: "実行" user_action: user_posted_topic: "{{user}}トピック を作成" @@ -834,10 +834,10 @@ ja: notifications: reasons: '3_6': 'このカテゴリを参加中のため通知されます' - '3_5': 'このトピックを参加中のため通知されます' - '3_2': 'このトピックを参加中のため通知されます。' + '3_5': 'このトピックに参加中のため通知されます' + '3_2': 'このトピックに参加中のため通知されます。' '3_1': 'このトピックを作成したため通知されます。' - '3': 'このトピックを参加中のため通知されます。' + '3': 'このトピックに参加中のため通知されます。' '2_8': 'このカテゴリをトラック中のため通知されます。' '2_4': 'このトピックに回答したため通知されます。' '2_2': 'このトピックをトラック中のため通知されます。' @@ -849,11 +849,13 @@ ja: '0': 'このトピックに関して一切通知を受けません。' watching_pm: title: "参加中" + description: "このメッセージに対して新しい投稿があった場合、登録されたメールアドレスと、コミュニティ内の通知ボックスに通知が届き、トピック一覧に新しい投稿数がつきます。" watching: title: "参加中" description: "このトピックに対して新しい投稿があった場合、登録されたメールアドレスと、NVコミュニティ内の通知ボックスに通知が届き、トピック一覧に新しい投稿数がつきます。" tracking_pm: title: "トラック中" + description: "未読件数と新しい投稿がメッセージの横に表示されます。他ユーザから@ユーザ名でタグ付けされた場合、またはあなたの投稿に回答がついた場合に通知されます。" tracking: title: "トラック中" description: "未読件数と新しい投稿がプライベートメッセージされます。他ユーザーからタグ付けをされた場合、またはあなたの投稿に回答が付いた場合に通知されます" @@ -864,6 +866,7 @@ ja: title: "通常" muted_pm: title: "ミュートされました" + description: "このメッセージについての通知を受け取りません。" muted: title: "ミュート" description: "このトピックについての通知を受け取りません。また、未読タブにも通知されません。" @@ -873,6 +876,7 @@ ja: open: "トピックを開く" close: "トピックを終了する" multi_select: "投稿の選択。。。" + auto_close: "自動終了する。。。" pin: "トピックをピンで留める。。。" unpin: "トピックのピンを取り除く。。。" unarchive: "トピックのアーカイブ解除" @@ -1029,8 +1033,8 @@ ja: yes_value: "はい、回答も一緒に削除する" no_value: "いいえ、ポストのみ削除する" admin: "ポスト管理" - wiki: "ウィキーポストにする" - unwiki: "ウィキーポストから外す" + wiki: "wikiポストにする" + unwiki: "wikiポストから外す" convert_to_moderator: "スタッフ色を追加" revert_to_regular: "スタッフ色を削除" rebake: "HTML再建築" @@ -1237,7 +1241,7 @@ ja: help: "このトピックはピン留めされていません。 既定の順番に表示されます。" pinned_globally: title: "全サイト的にピン留めされました" - help: "このトピックは全サイト的にピン留めされました;全てのリストのトップに表示されます。" + help: "このトピックは全サイト的にピン留めされました。全てのリストのトップに表示されます。" pinned: title: "ピン留め" help: "このトピックはピン留めされています。常にカテゴリのトップに表示されます" @@ -1374,6 +1378,7 @@ ja: admins: '管理者:' blocked: 'ブロック中:' suspended: '停止中:' + private_messages_short: "メッセージ" private_messages_title: "メッセージ" space_free: "{{size}} free" uploads: "アップロード" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 278d9881c1..45a7e71c86 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -185,8 +185,14 @@ ko: edit: "편집" cancel: "취소" view_pending: "대기중인 포스트" + has_pending_posts: + one: "이 토픽은 1개의 승인 대기중인 게시글이 있습니다." + many: "이 토픽은 {{count}}개의 승인 대기중인 게시글이 있습니다." confirm: "변경사항 저장" approval: + title: "게시글 승인 필요" + pending_posts: + other: "{{count}}개 미결 게시글이 있습니다." ok: "OK" user_action: user_posted_topic: "{{user}}가 게시한 토픽" @@ -420,6 +426,7 @@ ko: every_three_days: "매 3일마다" weekly: "매주" every_two_weeks: "격주" + email_direct: "누군가 나를 인용했을 때, 내 글에 답글을 달았을때, 내 이름을 언급했을때 혹은 토픽에 나를 초대했을 떄 이메일 보내기" email_always: "싸이트에 방문 중 일 때 이메일 알림을 보내지 마세요." other_settings: "추가 사항" categories_settings: "카테고리" @@ -580,6 +587,7 @@ ko: requires_invite: "죄송합니다. 초대를 받은 사람만 이용하실 수 있습니다." not_activated: "당신은 아직 로그인 할 수 없습니다. 계정을 만들었을때 {{sentTo}} 주소로 인증 이메일을 보냈습니다. 계정을 활성화하려면 해당 이메일의 지침을 따르십시오." not_allowed_from_ip_address: "이 IP 주소에서 로그인 할 수 없습니다." + admin_not_allowed_from_ip_address: "You can't log in as admin from that IP address." resend_activation_email: "다시 인증 이메일을 보내려면 여기를 클릭하세요." sent_activation_email_again: " {{currentEmail}} 주소로 인증 이메일을 보냈습니다. 이메일이 도착하기까지 몇 분 정도 걸릴 수 있습니다. 또한 스팸 메일을 확인하십시오." google: @@ -675,9 +683,11 @@ ko: units: "(# 시간)" examples: '시간에 해당하는 숫자를 입력하세요. (24)' notifications: + title: "@name 언급, 게시글과 토픽에 대한 답글, 개인 메시지 등에 대한 알림" none: "현재 알림을 불러올 수 없습니다." more: "이전 알림을 볼 수 있습니다." total_flagged: "관심 표시된 총 게시글" + mentioned: "

{{username}} {{description}}

" quoted: "

{{username}} {{description}}

" replied: "

{{username}} {{description}}

" posted: "

{{username}} {{description}}

" @@ -689,6 +699,11 @@ ko: moved_post: "

{{username}} moved {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

'{{description}}' 뱃지를 받았습니다.

" + popup: + mentioned: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 당신을 언급했습니다' + quoted: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 당신을 인용했습니다' + replied: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 당신에게 답글을 달았습니다' + posted: '"{{topic}}" - {{site_title}}에서 {{username}}님이 글을 게시하였습니다' upload_selector: title: "이미지 추가하기" title_with_attachments: "이미지 또는 파일 추가하기" @@ -711,6 +726,7 @@ ko: user: "@{{username}}의 글 검색" category: "\"{{category}}\" 카테고리 검색" topic: "이 토픽을 검색" + private_messages: "메시지 검색" site_map: "다른 토픽이나 카테고리로 이동" go_back: '돌아가기' not_logged_in_user: 'user page with summary of current activity and preferences' @@ -730,6 +746,7 @@ ko: close_topics: "토픽 닫기" archive_topics: "토픽 보관하기" notification_level: "알림 설정 변경" + choose_new_category: "토픽의 새로운 카테고리를 선택" selected: other: "{{count}}개의 토픽이 선택되었습니다." none: @@ -742,6 +759,7 @@ ko: bookmarks: "아직 북마크한 토픽이 없습니다." category: "{{category}}에 토픽이 없습니다." top: "Top 토픽이 없습니다." + search: "검색 결과가 없습니다." educate: new: '

새로운 토픽은 여기에서 볼 수 있습니다.

기본 설정으로 2일 이내에 생성된 토픽은 새로운 것으로 간주되며 new 표시가 나타납니다.

환경설정에서 설정을 변경 할 수 있습니다.

' unread: '

읽지 않은 토픽은 여기에서 볼 수 있습니다.

기본 설정으로 다음과 같은 경우 토픽은 읽지 않은 것으로 간주되며 읽지 않은 개수 1 가 표시됩니다.

  • 토픽을 만든 경우
  • 토픽에 리플을 단 경우
  • 토픽을 4분 이상 읽은 경우

또는 각 토픽 아래에 있는 알림 설정에서 해당 토픽을 추적하거나 지켜보도록 설정한 경우

환경설정에서 설정을 변경 할 수 있습니다.

' @@ -755,6 +773,7 @@ ko: category: "더 이상 {{category}}에 토픽이 없습니다" top: "더 이상 인기 토픽이 없습니다." bookmarks: "더이상 북마크한 토픽이 없습니다." + search: "더이상 검색 결과가 없습니다." topic: filter_to: "이 토픽에서 {{username}}님의 {{post_count}}건의 게시물만 보기" create: '새 토픽 만들기' @@ -904,6 +923,7 @@ ko: automatically_add_to_groups_optional: "이 초대는 다음 그룹에 대한 접근 권한을 포함합니다: (선택, 관리자만 가능)" automatically_add_to_groups_required: "이 초대는 다음 그룹에 대한 접근 권한을 포함합니다: (필수, 관리자만 가능)" invite_private: + title: '초대 메시지' email_or_username: "초대하려는 이메일 또는 아이디" email_or_username_placeholder: "이메일 또는 아이디" action: "초대" @@ -1384,6 +1404,7 @@ ko: agree_title: "이 신고가 올바르고 타당한지 확인하세요." agree_flag_modal_title: "동의 및 ..." agree_flag_hide_post: "동의 (포스트 숨기기 + 개인 메시지 보내기)" + agree_flag_hide_post_title: "Hide this post and automatically send the user a message urging them to edit it" agree_flag_restore_post: "동의하기(게시글 복원)" agree_flag_restore_post_title: "게시글을 복원하기" agree_flag: "신고에 동의함" @@ -1450,9 +1471,12 @@ ko: name: "이름" add: "추가" add_members: "사용자 추가하기" + custom: "Custom" automatic: "자동화" automatic_membership_email_domains: "이 목록의 있는 항목과 사용자들이 등록한 이메일 도메인이 일치할때 이 그룹에 포함" automatic_membership_retroactive: "이미 등록된 사용자에게 같은 이메일 도메인 규칙 적용하기" + default_title: "Default title for all users in this group" + primary_group: "Automatically set as primary group" api: generate_master: "마스터 API 키 생성" none: "지금 활성화된 API 키가 없습니다." @@ -1529,6 +1553,7 @@ ko: confirm: "정말로 이전 작업 상태로 데이터베이스를 롤백하시겠습니까?" export_csv: user_archive_confirm: "정말로 당신의 포스트를 다운로드 하시겠습니까?" + success: "Export initiated, you will be notified via message when the process is complete." failed: "내보내기가 실패했습니다. 로그를 확인해주세요" rate_limit_error: "글은 하루에 한번 다운로드 받을 수 있습니다. 내일 다시 시도해주십시요." button_text: "내보니기" @@ -1701,6 +1726,7 @@ ko: delete_topic: "토픽 삭제" delete_post: "게시글 삭제" impersonate: "대역" + anonymize_user: "anonymize user" screened_emails: title: "블락된 이메일들" description: "누군가가 새로운 계정을 만들면 아래 이메일 주소는 체크되고 등록은 블락됩니다, 또는 다른 조치가 취해집니다." @@ -1718,6 +1744,7 @@ ko: delete_confirm: "%{ip_address}를 규칙에 의해 삭제할까요?" roll_up_confirm: "화면에 표시되는 IP 주소를 subnet으로 바꾸시겠습니까?" rolled_up_some_subnets: "다음의 subnet들의 IP 주소들을 차단하였습니다: %{subnets}." + rolled_up_no_subnet: "There was nothing to roll up." actions: block: "블락" do_nothing: "허용" @@ -1727,6 +1754,9 @@ ko: ip_address: "IP 주소" add: "추가" filter: "검색" + roll_up: + text: "Roll up" + title: "Creates new subnet ban entries if there are at least 'min_ban_entries_for_roll_up' entries." logster: title: "에러 로그" impersonate: @@ -1747,6 +1777,7 @@ ko: staff: '스태프' suspended: '접근 금지 사용자' blocked: '블락된 사용자' + suspect: 'Suspect' approved: "승인?" approved_selected: other: "승인한 사용자 ({{count}}명)" @@ -1766,6 +1797,7 @@ ko: moderators: '운영자' blocked: '블락된 사용자들' suspended: '접근 금지된 사용자들' + suspect: 'Suspect Users' reject_successful: other: "성공적으로 ${count}명의 사용자를 거절하였습니다." reject_failures: @@ -1824,8 +1856,13 @@ ko: approve_success: "인증 이메일이 발송되었습니다." approve_bulk_success: "성공! 모든 선택된 사용자는 인증되었고 통보되었습니다." time_read: "읽은 시간" + anonymize: "익명 사용자" + anonymize_confirm: "Are you SURE you want to anonymize this account? This will change the username and email, and reset all profile information." + anonymize_yes: "Yes, anonymize this account" + anonymize_failed: "There was a problem anonymizing the account." delete: "사용자 삭제" delete_forbidden_because_staff: "관리자 및 운영자 계정은 삭제할 수 없습니다." + delete_posts_forbidden_because_staff: "Can't delete all posts of admins and moderators." delete_forbidden: other: "사용자가 작성한 글이 있으면 사용자를 삭제 할 수 없습니다. 사용자를 삭제 하기 전에 사용자가 작성한 글을 모두 삭제해야 합니다. (%{count}일 이전에 작성한 글은 삭제할 수 없습니다.)" cant_delete_all_posts: @@ -1850,6 +1887,8 @@ ko: suspended_explanation: "접근 금지된 유저는 로그인 할 수 없습니다." block_explanation: "블락 사용자는 게시글을 작성하거나 토픽을 작성할 수 없습니다." trust_level_change_failed: "신뢰도 변경에 문제가 있습니다." + grant_admin_failed: "There was a problem granting admin privileges." + grant_moderation_failed: "There was a problem granting moderation privileges." suspend_modal_title: "Suspend User" trust_level_2_users: "신뢰도 2 사용자들" trust_level_3_requirements: "사용자 신뢰도 3 이상이 필요" @@ -1963,11 +2002,13 @@ ko: modal_title: 뱃지 그룹으로 나누기 granted_by: 배지 부여자 granted_at: 배지 수여일 + reason_help: ( 토픽이나 게시글 링크) save: 저장 delete: 삭제 delete_confirm: 정말로 이 뱃지를 삭제하시겠습니까? revoke: 회수 reason: 원인 + expand: 확장 … revoke_confirm: 정말로 이 뱃지를 회수하시겠습니까? edit_badges: 뱃지 수정 grant_badge: 뱃지 부여 @@ -2015,6 +2056,7 @@ ko: with_time: %{username} at %{time} emoji: title: "Emoji" + help: "모든 사용자가 사용가능한 새로운 이미지를 추가. (프로 팁: 여러개의 파일을 드래그 & 드롭으로 한번에)" add: "새로운 Emoji 추가" name: "이름" image: "이미지" @@ -2053,6 +2095,7 @@ ko: dismiss_topics: 'x, t 토픽 무시하기' actions: title: 'Actions' + bookmark_topic: 'f 토글 북마크 토픽' pin_unpin_topic: 'shift+p 핀고정/핀해제' share_topic: 'shift+s 토픽 공유' share_post: 's 게시글 공유' @@ -2071,6 +2114,8 @@ ko: mark_watching: 'm, w 토픽 알람 : 주시하기' badges: title: 뱃지 + allow_title: "can be used as a title" + multiple_grant: "can be awarded multiple times" badge_count: other: "뱃지 %{count}개" more_badges: @@ -2114,6 +2159,7 @@ ko: description: 사용자의 프로필 정보를 작성함 anniversary: name: 기념일 + description: Active member for a year, posted at least once nice_post: name: 괜찮은 글 description: 작성한 글이 좋아요를 10개 받았습니다. 이 뱃지는 중복 수여 가능합니다. diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index d784343011..a25fc0d560 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -91,7 +91,7 @@ nb_NO: google+: 'del denne lenken på Google+' email: 'del denne lenken i en e-post' topic_admin_menu: "admin-handlinger for emne" - emails_are_disabled: "All outgunned email har blitt deaktivert globalt av en administrator. Ingen email varslinger vil bli sent. " + emails_are_disabled: "All utgående e-post har blitt deaktivert globalt av en administrator. Ingen e-postvarslinger vil bli sendt." edit: 'rediger tittelen og kategorien til dette emnet' not_implemented: "Den funksjonen har ikke blitt implementert ennå, beklager!" no_value: "Nei" @@ -389,11 +389,11 @@ nb_NO: email: title: "E-post" instructions: "Blir aldri vist offentlig" - ok: "Vi sender deg en email for å bekrefte" - invalid: "Vennligst gi en gyldig email-addresse" - authenticated: "Din email har blitt autentisert av {{provider}}" + ok: "Vi sender deg en e-post for å bekrefte" + invalid: "Vennligst oppgi en gyldig e-postadresse" + authenticated: "Din e-post har blitt autentisert av {{provider}}" frequency: - zero: "Vi sender deg straks en email hvis du ikke har lest tingen vi sendte deg en email om." + zero: "Vi sender deg straks en e-post hvis du ikke har lest saken vi sendte deg en e-post om." one: "Du får bare e-post hvis du ikke er blitt sett det siste minuttet." other: "Du får bare e-post hvis du ikke har blitt sett de siste {{count}} minuttene." name: @@ -407,14 +407,14 @@ nb_NO: instructions: "Unikt, kort og uten mellomrom." short_instructions: "Folk kan nevne deg som @{{username}}." available: "Ditt brukernavn er tilgjengelig." - global_match: "Email stemmer med det registrerte brukernavnet" + global_match: "E-post stemmer med det registrerte brukernavnet" global_mismatch: "Allerede registrert. Prøv {{suggestion}}?" not_available: "Ikke tilgjengelig. Prøv {{suggestion}}?" too_short: "Ditt brukernavn er for kort." too_long: "Ditt brukernavn er for langt." checking: "Sjekker brukernavnets tilgjengelighet..." - enter_email: 'Brukernavn funnet; oppgi samsvarende email' - prefilled: "Email stemmer med dette registrerte brukernavnet" + enter_email: 'Brukernavn funnet; oppgi samsvarende e-post' + prefilled: "E-post stemmer med dette registrerte brukernavnet" locale: title: "Språk for grensesnitt" instructions: "Språk for grensesnitt. Endringen vil tre i kraft når du oppdaterer siden." @@ -437,8 +437,8 @@ nb_NO: every_three_days: "hver tredje dag" weekly: "ukentlig" every_two_weeks: "annenhver uke" - email_direct: "Motta en email når noen siterer deg, svarer på dine innlegg, nevner mitt brukernavn eller inviterer meg til et emne" - email_private_messages: "Motta en email når noen sender deg en melding" + email_direct: "Motta en e-post når noen siterer deg, svarer på dine innlegg, nevner ditt brukernavn eller inviterer deg til et emne" + email_private_messages: "Motta en e-post når noen sender deg en melding" email_always: "Ikke stans e-postvarsler mens jeg er aktiv på nettstedet." other_settings: "Annet" categories_settings: "Kategorier" @@ -572,7 +572,7 @@ nb_NO: created: 'Opprettet' created_lowercase: 'opprettet' trust_level: 'Tillitsnivå' - search_hint: 'brukernavn, email eller IP adresse' + search_hint: 'brukernavn, e-post eller IP-adresse' create_account: title: "Opprett ny konto" failed: "Noe gikk galt, kanskje denne e-postadressen allerede er registrert. Prøv lenke for glemt passord" @@ -582,7 +582,7 @@ nb_NO: invite: "Skriv inn ditt brukernavn eller din e-postadresse, så sender vi deg en e-post for å nullstille ditt passord." reset: "Nullstill passord" complete_username: "Hvis en konto med brukernavn %{username} finnes vil du motta en e-post om kort tid med instruksjoner om hvordan du kan nullstille passordet." - complete_email: "Hvis en konto med e-postadressen %{email} eksisterer i systemet vil du om kort tid motta en epost med instruksjoner om hvordan du kan nullstille passordet." + complete_email: "Hvis en konto med e-postadressen %{email} eksisterer i systemet vil du om kort tid motta en e-post med instruksjoner om hvordan du kan nullstille passordet." complete_username_found: "Vi fant en konto med brukernavn %{username}. Du mottar om litt en e-post med instruksjoner for hovrdan du nullstiller passordet." complete_email_found: "Vi fant en konto med e-postadressen %{email}. Du mottar om litt en e-post med instruksjoner for hvordan du nullstiller passordet." complete_username_not_found: "Ingen konto har med brukernavnet %{username} er registrert" @@ -715,6 +715,13 @@ nb_NO: moved_post: "

{{username}} moved {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

Ble tildelt '{{description}}'

" + popup: + mentioned: '{{username}} nevnte deg i "{{topic}}" - {{site_title}}' + quoted: '{{username}} siterte deg i "{{topic}}" - {{site_title}}' + replied: '{{username}} svarte deg i "{{topic}}" - {{site_title}}' + posted: '{{username}} skrev i "{{topic}}" - {{site_title}}' + private_message: '{{username}} sendte deg en privat melding: "{{topic}}" - {{site_title}}' + linked: '{{username}} lenket til ditt innlegg i "{{topic}}" - {{site_title}}' upload_selector: title: "Legg til Bilde" title_with_attachments: "Legg til et bilde eller en fil" @@ -949,7 +956,7 @@ nb_NO: invite_reply: title: 'Inviter' to_forum: "Vi sender en kortfattet e-post som gjør det mulig for en venn å umiddelbart registreres ved å klikke på en lenke. Ingen innlogging er nødvendig." - email_placeholder: 'email' + email_placeholder: 'navn@example.com' login_reply: 'Logg Inn for å svare' filters: n_posts: @@ -1309,7 +1316,7 @@ nb_NO: history: "Historie" changed_by: "av {{author}}" raw_email: - title: "Rå Email" + title: "Rå e-post" not_available: "Ikke tilgjengelig!" categories_list: "Kategoriliste" filters: @@ -1675,7 +1682,7 @@ nb_NO: sent_test: "sendt!" delivery_method: "Leveringsmetode" preview_digest: "Forhåndsvis Oppsummering" - preview_digest_desc: "Forhåndsvis innholdet i den ukentlige sammendrags emailen sendt til innaktive melemer." + preview_digest_desc: "Forhåndsvis innholdet i det ukentlige e-postsammendraget sendt til inaktive medlemmer." refresh: "Refresh" format: "Format" html: "html" @@ -1741,7 +1748,7 @@ nb_NO: impersonate: "overta brukerkonto" anonymize_user: "anonymiser bruker" screened_emails: - title: "Kontrollerte Emails" + title: "Kontrollerte e-poster" description: "Når noen forsøker å lage en ny konto, vil de følgende e-postadressene bli sjekket, og registreringen vil bli blokkert, eller en annen handling vil bli utført." email: "E-postadresse" actions: @@ -1781,7 +1788,7 @@ nb_NO: not_found: "Beklager, det brukernavner eksisterer ikke i systemet vårt." id_not_found: "Beklager, denne brukerID eksisterer ikke i vårt system." active: "Aktiv" - show_emails: "Vis email" + show_emails: "Vis e-poster" nav: new: "Ny" active: "Aktiv" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 2b7259a72b..cd1165f031 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1427,11 +1427,11 @@ pl_PL: posts_lowercase: "wpisy" posts_long: "jest {{number}} wpisów w tym temacie" original_post: "Oryginalny wpis" - views: "Wyświetlenia" + views: "Odsłony" views_lowercase: - one: "wyświetlenie" - few: "wyświetlenia" - other: "wyświetleń" + one: "odsłona" + few: "odsłony" + other: "odsłon" replies: "Odpowiedzi" views_long: "ten temat był oglądany {number}} razy" activity: "Aktywność" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index fea206e6e7..bd4a03541c 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -728,6 +728,7 @@ ru: title_placeholder: "Название: суть обсуждения коротким предложением" edit_reason_placeholder: "почему вы хотите изменить?" show_edit_reason: "(добавить причину редактирования)" + reply_placeholder: "Для форматирования текста используйте Markdown и BBCode. Перетяните или вставьте изображение, чтобы загрузить его на сервер." view_new_post: "Посмотреть созданное вами сообщение." saving: "Сохранение..." saved: "Сохранено!" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 5e98e979b4..0005beb267 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -418,13 +418,13 @@ sq: other: "We'll only email you if we haven't seen you in the last {{count}} minutes." name: title: "Emri" - instructions: "Your full name (optional)" + instructions: "Emri i Plotë (fakultativ)" instructions_required: "Emri i plotë" too_short: "Emri juaj është shumë i shkurtër" - ok: "Your name looks good" + ok: "Emri duket në rregull" username: title: "Pseudonimi" - instructions: "Unique, no spaces, short" + instructions: "Unik, pa hapësira, i shkurtër" short_instructions: "People can mention you as @{{username}}" available: "Emri është i disponueshëm" global_match: "Email matches the registered username" @@ -475,13 +475,13 @@ sq: auto_track_topics: "Automatically track topics I enter" auto_track_options: never: "asnjëherë" - immediately: "immediately" + immediately: "menjëherë" after_n_seconds: one: "pas 1 sekonde" other: "pas {{count}} sekonda" after_n_minutes: - one: "after 1 minute" - other: "after {{count}} minutes" + one: "pas 1 minute" + other: "pas {{count}} minutash" invited: search: "shkruaj për të kërkuar ftesat..." title: "Ftesa" @@ -512,7 +512,7 @@ sq: title: "Fjalëkalimi" too_short: "Fjalëkalimi është shumë i shkurër." common: "Ky fjalëkalim është shumë i përdorur." - same_as_username: "Your password is the same as your username." + same_as_username: "Fjalëkalimi është i njëjtë me pseudonimin." same_as_email: "Your password is the same as your email." ok: "Fjalëkalimi është i pranueshëm." instructions: "Të paktën %{count} karaktere." @@ -582,7 +582,7 @@ sq: deleted_filter: enabled_description: "This topic contains deleted posts, which have been hidden. " disabled_description: "Deleted posts in the topic are shown." - enable: "Hide Deleted Posts" + enable: "Fsheh Postimet e Eliminuara" disable: "Show Deleted Posts" private_message_info: title: "Mesazh" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 00c7a66778..47c6c8a835 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -533,7 +533,7 @@ tr_TR: last_post: Son gönderi last_reply_lowercase: son cevap replies_lowercase: - other: cevaplar + other: cevap summary: enabled_description: "Bu konunun özetini görüntülemektesiniz: topluluğun en çok ilgisini çeken gönderiler" description: "{{count}} sayıdacevap var." @@ -1283,7 +1283,7 @@ tr_TR: views: "Görüntüleme" views_lowercase: other: "görüntüleme" - replies: "Cevaplar" + replies: "Cevap" views_long: "bu konu {{number}} defa görüntülendi" activity: "Aktivite" likes: "Beğeni" @@ -1292,7 +1292,7 @@ tr_TR: likes_long: "bu konuda {{number}} beğeni var" users: "Kullanıcı" users_lowercase: - other: "kullanıcılar" + other: "kullanıcı" category_title: "Kategori" history: "Geçmiş" changed_by: "Yazan {{author}}" @@ -1368,7 +1368,7 @@ tr_TR: this_month: "Bu ay" this_week: "Bu hafta" today: "Bugün" - other_periods: "daha fazla popüler konuya bakın" + other_periods: "daha fazla konuya bak" browser_update: 'Malesef, tarayıcınız bu site için çok eski. Lütfen tarayıcınızı güncelleyin.' permission_types: full: "Oluştur / Cevapla / Bak" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 906b512e19..3dca5c8179 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -21,6 +21,7 @@ uk: mb: МБ tb: ТБ dates: + time: "HH:mm" tiny: half_a_minute: "< 1 хв" date_year: "MMM 'YY" @@ -141,6 +142,9 @@ uk: title: search: "Шукати тему за назвою, url або id:" placeholder: "введіть назву теми" + queue: + topic: "Тема:" + delete_user: 'Видалити користувача' user_action: user_posted_topic: "{{user}} написав(ла) тему" you_posted_topic: "Ви написали тему" @@ -1321,6 +1325,8 @@ uk: post_revision: "Коли користувач редагує або створює допис" trust_level_change: "Коли користувач змінює рівень довіри" user_change: "Коли користувач змінений або створений" + preview: + sample: "Зразок:" lightbox: download: "звантажити" keyboard_shortcuts_help: diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 1084c23d00..5110d4e742 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -308,6 +308,7 @@ zh_CN: dismiss_notifications: "标记所有为已读" dismiss_notifications_tooltip: "标记所有未读通知为已读" disable_jump_reply: "不要在回复后跳转到我的新帖子" + dynamic_favicon: "在浏览器图标上显示新/更新的主题数量" edit_history_public: "让其他用户查看我的帖子的以前版本" external_links_in_new_tab: "始终在新的标签页打开外部链接" enable_quoting: "在高亮选择文字时启用引用回复" @@ -528,6 +529,7 @@ zh_CN: read_only_mode: enabled: "只读模式已启用。你可以继续浏览这个站点但是无法进行交互操作。" login_disabled: "只读模式下不允许登录。" + too_few_topics_notice: "创建至少 5 个公共主题和 %{posts} 公开的回复活跃论坛。新用户无法在无内容可读的情况下提高用户等级。这个消息只显示给职员。" learn_more: "了解更多..." year: '年' year_desc: '365 天以前创建的主题' @@ -711,6 +713,13 @@ zh_CN: moved_post: "

{{username}} 移动了 {{description}}

" linked: "

{{username}} {{description}}

" granted_badge: "

获得“{{description}}”

" + popup: + mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' + quoted: '{{username}}在“{{topic}}”引用了你的帖子 - {{site_title}}' + replied: '{{username}}在“{{topic}}”回复了你 - {{site_title}}' + posted: '{{username}}在“{{topic}}”中发布了帖子 - {{site_title}}' + private_message: '{{username}}在“{{topic}}”中给你发送了一个消息 - {{site_title}}' + linked: '{{username}}在“{{topic}}”中链接了你的帖子 - {{site_title}}' upload_selector: title: "插入图片" title_with_attachments: "上传图片或文件" @@ -768,7 +777,7 @@ zh_CN: top: "没有最佳主题。" search: "没有搜索结果。" educate: - new: '

你讲发表新的主题。

默认情况下,近两天创建的主题是近期主题,并会显示一个 的标识。

你可以在你的 设置中改变这一行为。

' + new: '

近期的主题将在这里显示。

默认情况下,近两天创建的主题是近期主题,并会显示一个 的标识。

你可以在你的 设置中改变这一行为。

' unread: '

这里是你的未读主题。

默认情况下,下述主题将被认为是未读的,并会显示未读数目: 1 如果你:

  • 创建了该主题
  • 回复了该主题
  • 阅读该主题超过 4 分钟

或者你在主题底部的通知控制中选择了追踪或监视。

你可以改变你的用户设置.

' bottom: latest: "没有更多主题可看了。" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 659d8201ae..266051ca66 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -475,7 +475,7 @@ en: continue_button: "Continue to %{site_name}" welcome_to: "Welcome to %{site_name}!" approval_required: "A moderator must manually approve your new account before you can access this forum. You'll get an email when your account is approved!" - + missing_session: "We can not detect if your account was created, please ensure you have cookies enabled." post_action_types: off_topic: title: 'Off-Topic' diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index ca2bdb7ed8..ab55fd9058 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -377,6 +377,7 @@ es: one: "hace casi 1 año" other: "hace casi %{count} años" password_reset: + no_token: "Lo sentimos, el enlace de cambio de contraseña es demasiado antiguo. Inicia sesión de nuevo y selecciona 'Olvidé mi contraseña' para obtener un nuevo enlace." choose_new: "Por favor elige un nuevo password" choose: "Por favor, elige una contraseña" update: 'Actualizar contraseña' @@ -396,6 +397,7 @@ es: continue_button: "Continuar a %{site_name}" welcome_to: "Bienvenido a %{site_name}!" approval_required: "Un moderador debe aprobar manualmente tu nueva cuenta antes de que puedas acceder a este foro. ¡Recibirás un email cuando tu cuenta sea aprobada!" + missing_session: "No hemos podido detectar si tu cuenta fue creada. Por favor, asegúrate de tener activadas las cookies en tu navegador." post_action_types: off_topic: title: 'Off-Topic' @@ -722,6 +724,7 @@ es: flag_sockpuppets: "Si un nuevo usuario responde a un tema desde la misma dirección de IP que el nuevo usuario que inició el tema, reportar los posts de los dos como spam en potencia." traditional_markdown_linebreaks: "Utiliza saltos de línea tradicionales en Markdown, que requieren dos espacios al final para un salto de línea." post_undo_action_window_mins: "Número de minutos durante los cuales los usuarios pueden deshacer sus acciones recientes en un post (me gusta, reportes, etc)." + must_approve_users: "Los miembros administración deben aprobar todas las nuevas cuentas antes de que se les permita el acceso al sitio. AVISO: ¡habilitar esta opción en un sitio activo revocará el acceso a los usuarios que no sean moderadores o admin!" ga_tracking_code: "Código de Google Analytics, ej: UA-12345678-9; visita http://google.com/analytics" ga_domain_name: "El nombre de dominio especificado en Google Analytics (ga.js). Ejemplo: misitio.com; ver en http://google.com/analytics" ga_universal_tracking_code: "Código de seguimiento de Google Universal Analytics (analytics.js), ejemplo: UA-12345678-9; visita http://google.com/analytics" @@ -970,6 +973,7 @@ es: show_create_topics_notice: "Si el sitio tiene menos de 5 temas abiertos al público, mostrar un aviso pidiendo a los administradores crear más temas." vacuum_db_days: "Correr VACUUM FULL ANALYZE para reclamar espacio en la base de datos después de las migraciones. (Poner en 0 para inhabilitar)" prevent_anons_from_downloading_files: "Impedir que los usuarios anónimos descarguen archivos. ADVERTENCIA: Esto impedirá que funcione cualquier recurso del sitio publicado como adjunto." + slug_generation_method: "Elegir un método de generación de slug. 'encoded' generará cadenas con código porciento. 'none' hara que no se genere slug." enable_emoji: "Habilitar emoji" emoji_set: "¿De qué tipo os gustan los emoji?" enforce_square_emoji: "Forzar una relación de aspecto cuadrada para todos los emojis." @@ -1122,6 +1126,13 @@ es: text_body_template: "%{invitee_name} te ha invitado a unirte a\n\n > **%{site_title}**\n\n > %{site_description}\n\n Si te interesa, haz clic en el siguiente enlace: \n\n %{invite_link}\n\nEsta invitación procede de un usuario de confianza, por lo que no necesitarás autenticarte.\n" invite_password_instructions: subject_template: "Asigna una contraseña para tu cuenta en %{site_name}" + text_body_template: | + Gracias por aceptar tu invitación a %{site_name} -- ¡te damos la bienvenida! + + Haz clic en el siguiente enlace para elegir una contraseña: + %{base_url}/users/password-reset/%{email_token} + + Si no recuerdas tu contraseña, o no tienes todavía, elige "Olvidé mi contraseña" cuando inicies sesión con tu dirección de email. test_mailer: subject_template: "[%{site_name}] Prueba de envío de email" text_body_template: | @@ -1220,6 +1231,20 @@ es: system_messages: post_hidden: subject_template: "Post oculto al haber sido reportado por la comunidad" + text_body_template: | + Hola, + + Este es un mensaje automático de %{site_name} para informarte que tu post se ha ocultado. + + %{base_url}%{url} + + %{flag_reason} + + Varios miembros de la comunidad reportaron este post antes de que fuera ocultado, por lo que por favor, considera cómo podrías revisarlo y reflejar su feedback. **Puedes editar tu post antes de %{edit_delay} minutos, y se mostrará de nuevo automáticamente.** + + Sin embargo, si el post fuera ocultado por la comunidad una segunda vez, permanecerá oculto hasta que sea revisado por un moderador – y en ese caso puede que se tomen medidas, incluyendo la posible suspesión de tu cuenta. + + Para más información, por favor consulta nuestras [directrices](%{base_url}/guidelines). usage_tips: text_body_template: | Este mensaje privado tiene algunos trucos rápidos para que puedas comenzar. ## Sigue deslizando No hay botones de página siguiente o números de página – para leer más, **¡simplemente sigue deslizando hacia abajo!** Mientras llegan nuevos temas, estos aparecerán automáticamente. ## ¿Dónde estoy? - Para buscar, tu página de usuario o el menú, usa los botones **icon en la parte superior derecha**. - Cualquier título de tema te llevará al siguiente mensaje no leído. Usa la hora de última actividad y la cuenta de mensaje para ir al principio o al final. - Mientras estás leyendo un tema, salta al inicio ↑ seleccionando el título del tema. Selecciona la barra de progreso verde en la parte inferior derecha para los controles completos de navegación o usa las teclas home "Inicio" y end; "Fin";. ## ¿Cómo respondo? - Para responder al mensaje general, usa el botón de Respuesta al final de la página. - Para responder a un mensaje en particular, usa el botón Respuesta en ese mensaje. - Para llevar la conversación en una dirección distinta, pero manteniendo un enlace, usa Responde como Tema enlazado a la derecha del mensaje. - Para citar a alguien en tu respuesta, selecciona el texto que quieras citar y presiona cualquier de los botones de Respuesta. Para dar un toque a alguien en tu respuesta, menciona su nombre. Escribe `@` y un cuadro de autocompletado aparecerá. Para [Emoji comunes](http://www.emoji.codes/), simplemente comienza escribiendo `:` o los tradicionales smileys `:)` :smile: ## ¿Qué más puedo hacer? Hay botones de acción al final de cada mensaje. Para hacer saber a cualquiera que te agradó su mensaje, usa el botón **me gusta**. Si tienes algún problema con un mensaje, házselo saber privadamente al autor o bien a nuestro equipo usando el botón **reportar**. También puedes **compartir** el enlace a un mensaje o añadirlo a **favoritos** para referencia futura en tu página de usuario. ## ¿Quién me está hablando? Cuando alguien responde a tu mensaje, cita tu mensaje o menciona tu `@nombre_de_usuario`, un número aparecerá de forma inmediata en la parte superior derecha de la página. Úsalo para acceder a tus **notificaciones**. No te preocupes si te pierdes una respuesta – se te enviará un correo electrónico con las respuestas directas (y mensajes privados) si no estas conectado cuando te lleguen. ## ¿Cuándo son nuevas las conversaciones? Por defecto todas las conversaciones con menos de dos días de antigüedad son consideradas nuevas, y cualquier conversación en la que hayas participado (respondido, creado o leído por un periodo extenso de tiempo) serán automáticamente monitorizadas. Verás indicadores azules y de números al lado de estos temas: Puedes cambiar el estado individual de notificación de cada tema a través del control al final del tema (también puede ajustarse por categoría). Para cambiar la forma en que monitorizas los temas, o definir una nueva, echa un vistazo a tus [preferencias de usuario](%{base_url}/my/preferences). ## ¿Por qué no puedo hacer ciertas cosas? Los nuevos usuarios están de alguna forma limitados por razones de seguridad. Conforme vayas participando, irá ganando la confianza de la comunidad, te convertirás en un ciudadano de pleno derecho y estas limitaciones se te quitarán automáticamente. Con un [nivel de confianza](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) suficientemente alto, incluso ganarás acceso a más habilidades para ayudarnos a administrar nuestra comunidad juntos. diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 4aacbe1000..3594493ee4 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -343,6 +343,7 @@ fa_IR: almost_x_years: other: "حداقل %{count} سال قبل" password_reset: + no_token: "Sorry, that password change link is too old. Log in again and select 'I forgot my password' to get a new link." choose_new: "لطفا یک رمز عبور جدید وارد کنید" choose: "لطفا یک رمز عبور وارد کنید" update: 'به‌روز کردن گذرواژه' @@ -362,6 +363,7 @@ fa_IR: continue_button: "برو به %{site_name}" welcome_to: "به %{site_name} خوش آمدید!" approval_required: "مدیر باید حساب کاربری را بصورت دستی تایید کند برای دسترسی به این انجمن. شما ایمیلی مبنی به تایید حساب کاربریتان دریافت می کنید. " + missing_session: "We can not detect if your account was created, please ensure you have cookies enabled." post_action_types: off_topic: title: 'جستار های قدیمی' @@ -689,6 +691,7 @@ fa_IR: flag_sockpuppets: "اگر کاربری به جستاری با ای پی برابر با کاربری که نوشته را شروع کرده ٬ آنها را به عنوان هرزنامه پرچم گزاری کن." traditional_markdown_linebreaks: "از linebreaks سنتی استفاده کن در مدلهای نشانه گزاری٬‌ چرا که نیاز به دو فضای انتهایی دارد برای linebreak." post_undo_action_window_mins: "تعداد دقایقی که کاربران اجازه دارند اقدامی را که در نوشته انجام داده اند باز گردانند. (پسند٬‌ پرچم گزاری٬‌ چیزهای دیگر)." + must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" ga_tracking_code: " Google analytics (ga.js) کد پیگری کد٬ eg: UA-12345678-9;این را ببین http://google.com/analytics" ga_domain_name: "Google analytics (ga.js) اسم دامنه٬ eg: mysite.com; این را ببین http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) کد پیگری کد٬ UA-12345678-9; این را ببین http://google.com/analytics" @@ -937,6 +940,7 @@ fa_IR: show_create_topics_notice: "اگر سایت کمتر از 5 جستار عمومی دارد٬ اطلاع بده به مدیران تا جستارهای بیشتری بسازند." vacuum_db_days: " VACUUM FULL ANALYZE را اجرا کن برای پس گرفتن فضای DB بعد از مهاجرت کردن ( 0 برای غیر فعال کردن)" prevent_anons_from_downloading_files: "جلوگیری از دانلود کردن فایل پیوست توسط افراد ناشناس. اخطار: این جلوگیری می کنه از هر سایت دارای بدون عکسی. " + slug_generation_method: "Choose a slug generation method. 'encoded' will generate percent encoding string. 'none' will disable slug at all." enable_emoji: "فعالسازی ایموجی" emoji_set: "می‌خواهید ایموجی شما چطور باشد؟" enforce_square_emoji: "تحمیل نسبت ابعاد مربع به تمام شکلک ها emojis . " @@ -1066,6 +1070,13 @@ fa_IR: text_body_template: "%{invitee_name} شما را دعوت کرده تا عضو شوید \n\n> **%{site_title}**\n>> %{site_description}\n\nاگر علاقه مند هستید بر روی پیوند پیش رو کلیک کنید: \n\n%{invite_link}\n\nاین دعوتنامه از طرف یک کاربر قابل اعتماد است٬ پس شما نیازی به ورود ندارید. \n" invite_password_instructions: subject_template: "تنظیم رمز عبور برای حساب کاربری {site_name}%" + text_body_template: | + Thanks for accepting your invitation to %{site_name} -- welcome! + + 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. test_mailer: subject_template: "[%{site_name}] ایمیل تست دهش" text_body_template: | @@ -1134,6 +1145,20 @@ fa_IR: system_messages: post_hidden: subject_template: "پست مخفی شده بدلیل پرچم گزاری انجمن " + text_body_template: | + Hello, + + This is an automated message from %{site_name} to inform you that your post was hidden. + + %{base_url}%{url} + + %{flag_reason} + + Multiple community members flagged this post before it was hidden, so please consider how you might revise your post to reflect their feedback. **You can edit your post after %{edit_delay} minutes, and it will be automatically unhidden.** + + However, if the post is hidden by the community a second time, it will remain hidden until handled by staff – and there may be further action, including the possible suspension of your account. + + For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines). usage_tips: text_body_template: "چند راهنمای کوتاه برای شروع کار: \n\n##همنیطور اسکرول کنید\n\nشماره صفحه یا کلیدی در صفحه وجود ندارد - برای خواندن بیشتر ٬ ** فقط همنیطور اسکرول کنید**! \n\nنوشته های جدید وقتی می آیند٬ بطور خودکار نمایش داده می شوند.\n\n## من کجا هستم ؟! \n\n- برای جستجو٬‌ صفحه کاربریت٬ یا منو از ** آیکون دکمه های بالا سمت راست استفاده کن**.\n\n-عنوان هر جستاری شما را به نوشته خوانده نشده ی بعدی می برد. از زمان آخرین فعالیتت و شمارنده نوشته ها برای وارد شدن به بالا یا پایین استفاده کن. \n\n-همزمان با خواندن جستار٬‌ بپرید به بالا یا uarr ; با انتخاب عنوان جستار. بار سبز رنگ در پایین سمت راست را انتخاب کنید برای کنترل تمام جابجایی ها. یا از خانه و پایانکلید استفاده کنید.\n\n\n\n## چگونه پاسخ دهم ؟ \n\n- - برای پاسخ با تمام جستار ها از کلید پاسخ استفاده کن در پایین ترین قسمت صفحه. \n\n- برای پاسخ به یک نوشته خاص از کلید پاسخ استفاده کن در همان نوشته.\n\n- برای بردن گفتگو در مسیری متفاوت ولی مرتبط بهم از این استفاده کنید٬ پاسخ به عنوان جستار مرتبط در بالا سمت راست نوشته.\n\nبرای اشاره کردن به شخصی در پاسخ٬ متنی را که علاقه دارید انتخاب کنید و بعد کلید پاسخ را فشار دهید.\n\n\n\nبرای پینگ شخصی در پاسخ از اسم آنها نام بربید. `@` را تایپ کنید و autocompleter (کامل کننده خودکار) بالا می آید.\n\n\n\nبرای [ Emoji استاندارد]٬ (http://www.emoji.codes/)٬ فقط تایپ کنید `:` یا از خنده های سنتی استفاده کنید `:)`\n\n## چه کارهای دیگری می توانم انجام دهم ؟ \n\nدکمه های فعالیت در پایین هر نوشته است. \n\n\n\nبرای اطلاع دادن به دیگران از اینکه شما نوشته آنها لذت بردید از کلید **like** استفاده کنید.\ \ اگر در نوشته مشکلی را میبینید٬‌ بطور شخصی به آنها یا مدیران اطلاع دهید با کلید پرچم گزاری **flag**.\n\nهمچنین شما می توانید پیوند را در نوشته به **اشتراک** بگذارید٬ یا **نشانه گزاری** کنید برای مرجع بعد در صفحه کاربریتان. \n\n##چه کسی می تواند با من حرف بزند ؟ \n\nوقتی کسی به شما پاسخ داد در نوشته٬ نوشته شما را جایی ذکر کرد٬‌ یا کسی اسمی از شما برد `@username`, شماره ای به سرعت در بالای صفحه سمت راست نمایان می شود. استفاده کن برای دسترسی به **آگاه سازی ها**.\n\n\n\nنگران نباش اگر پاسخ دادن را فراموش کردی - اگر آنلاین نیستی پاسخ ها در ایمیل یا (و پیام ها ) برایت می آید وقتی آن ها می رسند. \n\n##گفتگو ها چه وقت جدید هستند ؟ \n\nبطور معمول تمام گفتگوهایی با عمر کمتر از دو روز جدید محسوب می شوند٬ و هر گفتگویی که شما در آن شرکت دارید (پاسخ دادن٬ ساختن٬ یا خواندن برای مدت طولانی) بطور خودکار دنبال می شوند.\n\nشما یک آبی جدید و شاخص تعداد را در مقابل آنها می بینید: \n\n\n\nشما می توانید تعداد آگهی سازها را در هر جستار تغییر دهید از طریق کنترل در پاییت جستار ( این می تواند در دسته تنظیم شود). برای تغییر دنبال کردن جستارها یا معنی جدید) نگاه کن به [تنظیمات کاربر](%{base_url}/my/preferences\n\n## چرا یک سری کارها خاص را نمی توانم انجام دهم ؟ \n\nکاربران جدید به تا حدودی محدودند به دلایل امنیتی. با بودنت در اینجا٬ تو اعتماد انجمن را بدست میاری و بعد شهروند اینجا می شی و اون محدودیت ها به مرور زمان بر داشته می شوند. در اندزه کافی [سطح اعتماد] ](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924), شما اعتماد انجمن را بدست می آورید و قادر خواهید بود به ما کمک کنید تا با هم انجمن را مدیریت کنیم.\n" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index cbcec30f1e..c1d00061ca 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -386,6 +386,7 @@ fi: one: "lähes vuosi sitten" other: "lähes %{count} vuotta sitten" password_reset: + no_token: "Pahoittelut, tämä linkki salasanan vaihtamiseksi on liian vanha. Kirjaudu sisään uudestaan ja valitse 'Unohdin salasanani' saadaksesi uuden linkin" choose_new: "Valitse uusi salasana" choose: "Valitse salasana" update: 'Päivitä salasana' @@ -731,6 +732,7 @@ fi: flag_sockpuppets: "Jos uusi käyttäjä vastaa toisen uuden käyttäjän luomaan ketjun samasta IP osoitteesta, liputa molemmat viestit mahdolliseksi roskapostiksi." traditional_markdown_linebreaks: "Käytä perinteisiä rivinvaihtoja Markdownissa, joka vaatii kaksi perättäistä välilyöntiä rivin vaihtoon." post_undo_action_window_mins: "Kuinka monta minuuttia käyttäjällä on aikaa perua viestiin kohdistuva toimi (tykkäys, liputus, etc)." + must_approve_users: "Henkilökunnan täytyy hyväksyä kaikki uudet tilit, ennen uusien käyttäjien päästämistä sivustolle. VAROITUS: tämän asetuksen valitseminen poistaa pääsyn kaikilta jo olemassa olevilta henkilökuntaan kuulumattomilta käyttäjiltä." ga_tracking_code: "Google analytics (ga.js) seurantakoodi, esim.: UA-12345678-9; katso http://google.com/analytics" ga_domain_name: "Google analytics (ga.js) verkkotunnus, esim.: osoite.fi; katso http://google.com/analytics" ga_universal_tracking_code: "Google Universal analytics (analytics.js) seurantakoodi, esim.: UA-12345678-9; katso http://google.com/analytics" @@ -979,6 +981,7 @@ fi: show_create_topics_notice: "Jos palstalla on vähemmän kuin 5 julkista ketjua, huomauta ylläpitäjiä ketjujen luonnista." vacuum_db_days: "Aja VACUUM FULL ANALYZE vapauttaaksesi tilaa tietokantaan migraatioiden jälkeen (aseta 0 ottaaksesi pois käytöstä)" prevent_anons_from_downloading_files: "Estä kirjautumattomia käyttäjiä lataamasta liitetiedostoja. VAROITUS: tämä estää viestin liitettyjen muiden tiedostojen, kuin kuvien käyttämisen sivustolla." + slug_generation_method: "Valitse polkulyhenteen luomisen metodi. 'encoded' käyttää prosenttikoodausta, 'none' poistaa polkulyhenteet käytöstä." enable_emoji: "Ota emoji käyttöön" emoji_set: "Minkälaisa emojia haluaisit käyttää?" enforce_square_emoji: "Pakota neliö kuvasuhteeksi kaikille emojille." @@ -1142,6 +1145,12 @@ fi: Tämän kutsun lähetti luotettu käyttäjä, joten sinun ei tarvitse kirjautua sisään. invite_password_instructions: subject_template: "Aseta salasana %{site_name} -tunnuksellesi" + text_body_template: | + Kiitos kun hyväksyit kutsun sivustolle %{site_name} -- tervetuloa! + + Klikkaa tätä linkkiä valitaksesi uuden salasanan nyt: %{base_url}/users/password-reset/%{email_token} + + Jos et muista salasanaasi, tai et ole vielä asettanut sellaista, valitse "Unohdin salasanani" kirjautuessasi siään sähköpostiosoitteellasi. test_mailer: subject_template: "[%{site_name}] Sähköpostin toimitettavuustesti" text_body_template: | @@ -1243,6 +1252,20 @@ fi: system_messages: post_hidden: subject_template: "Viesti on piilotettu liputuksen johdosta" + text_body_template: | + Hei, + + Tämä automaattinen viesti on lähetetty sivustolta %{site_name} kertoaksemme sinulle, että kirjoittamasi viesti on piilotettu. + + %{base_url}%{url} + + %{flag_reason} + + Usea sivuston jäsen on liputtanut tämän viestien ennen sen piilottamista, joten harkitse miten voisit muokata viestiäsi heidän antamansa palautteen pohjalta. **Voit muokata viestiäsi kun %{edit_delay} minuuttia on kulunut, jolloin viesti tulee taas näkyviin.** + + Kuitenkin, jos viesti piilotetaan toisen kerran, se pysyy piilotettuna, kunnes henkilökunta selvittää tilanteen – jonka jälkeen voi seurata muita seuraamuksia, mukaan lukien tilisi hyllyttäminen. + + Saadaksesi lisätietoja, lue [yhteisön säännöt](%{base_url}/guidelines). usage_tips: text_body_template: | Tässä muutama pikaohje, joiden avulla pääset alkuun. diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 229bc9c622..3cdaf5449b 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -1396,8 +1396,43 @@ it: search_help: "

Suggerimenti

\n

\n

    \n
  • La ricerca nei titoli è prioritaria, perciò, in caso di dubbi, cerca per titolo
  • \n
  • I risultati migliori si ottengono sempre con parole uniche e non comuni
  • \n
  • Quando possibile, circoscrivi l'ambito della ricerca a una categoria, a un utente o a un argomento in particolare
  • \n
\n

\n

Opzioni

\n

\n \n\n \n \n \n\n
order:viewsorder:latest
status:openstatus:closedstatus:archivedstatus:norepliesstatus:singleuser
category:foouser:foo
in:likesin:postedin:watchingin:trackingin:private
in:bookmarks
\n

\n

\narcobaleni category:parchi status:open order:latest ricercherà gli argomenti contenenti la parola \"arcobaleni\" nella categoria \"parchi\" che non siano chiusi o archiviati, ordinati secondo la data dell'ultimo messaggio.

\n" badges: long_descriptions: + autobiographer: | + Blah blah blah + Blah blah blah + first_like: | + Blah blah blah + Blah blah blah + first_link: | + Blah blah blah + Blah blah blah + first_quote: | + Blah blah blah + Blah blah blah + first_share: | + Blah blah blah + Blah blah blah + read_guidelines: | + Blah blah blah + Blah blah blah + reader: | + Blah blah blah + Blah blah blah + editor: | + Blah blah blah + Blah blah blah first_flag: "Le segnalazioni sono importanti per il benessere della tua comunità. Se noti dei messaggi che richiedono l'attenzione dei moderatori, non esitare a segnalarli. Puoi anche usare la finestra di segnalazione per inviare un messaggio agli utenti, una volta che avrai raggiunto il \nlivello di esperienza 1.\n" + nice_share: | + Blah blah blah + Blah blah blah + welcome: | + Blah blah blah + Blah blah blah anniversary: "Bla bla bla \nBla bla bla \n" good_share: "Bla bla bla \nBla bla bla\n" great_share: "Bla bla bla \nBla bla bla \n" nice_post: "Bla bla bla \nBla bla bla \n" + admin_login: + success: "email Inviata" + error: "Errore!" + email_input: "Email Amministratore" + submit_button: "Invia Email" diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index a08842a7b6..ef99277bdc 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -526,7 +526,7 @@ ja: contact_email_invalid: "サイトの連絡先メールアドレスが正しくありません。サイトの設定で更新してください" title_nag: "サイトの名前が正しくありません。サイトの設定で更新してください" site_description_missing: "検索結果に表示される説明文を入力してください。サイトの設定で更新してください" - consumer_email_warning: "サイトはメール送信に Gmail (または他のカスタムメールサービス) を利用するように設定されています。Gmail で送信可能なメール数には制限があります。メールを確実に送信するために mandrill.com などのメールサービスプロバイダーのり用を検討してください。" + consumer_email_warning: "サイトはメール送信に Gmail (または他のカスタムメールサービス) を利用するように設定されています。Gmail で送信可能なメール数には制限があります。メールを確実に送信するために mandrill.com などのメールサービスプロバイダーの利用を検討してください。" notification_email_warning: "通知用メールがあなたのドメインで有効なメールアドレスから送信されていません。メール配信が不安定になり、信頼性が低くなります。\nサイトの設定で更新してください" content_types: education_new_reply: diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 38d9546be9..9b9e2f21f9 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -20,11 +20,13 @@ pl_PL: is_reserved: "jest zarezerwowana" purge_reason: "Automatycznie usunięto jako porzucone, nieaktywne konto" disable_remote_images_download_reason: "Pobieranie zewnętrznych grafik zostało wyłączone z uwagi na niską ilość wolnego miejsca na dysku." + anonymous: "Anonim" errors: format: '%{attribute} %{message}' messages: too_long_validation: "ograniczono do %{max} znaków, podano %{length}." invalid_boolean: "Nieprawidłowy boolean." + taken: "jest już w użyciu" accepted: musi zostać zaakceptowane blank: nie może być puste present: musi być puste @@ -61,6 +63,10 @@ pl_PL: other_than: "musi być różna od %{count}" template: body: 'Wystąpiły problemy z następującymi polami:' + header: + one: 1 błąd uniemożliwił zapisanie %{model} + few: '%{count} błędy uniemożliwiły zapisanie %{model}' + other: '%{count} błędów uniemożliwiło zapisanie %{model}' embed: load_from_remote: "Wystąpił błąd podczas wczytywania tego wpisu." bulk_invite: @@ -133,6 +139,8 @@ pl_PL: hot: "Popularne wątki" too_late_to_edit: "Ten wpis został utworzony zbyt dawno. Nie może być edytowany ani usunięty." excerpt_image: "zdjęcie" + queue: + delete_reason: "Skasowany z poziomu kolejki moderacji." groups: errors: can_not_modify_automatic: "Nie możesz modyfikować automatycznej grupy" @@ -516,18 +524,23 @@ pl_PL: user_to_user_private_messages: title: "Użytkownik-do-użytkownika" xaxis: "Dzień" + yaxis: "Liczba wiadomości" system_private_messages: title: "System" xaxis: "Dzień" + yaxis: "Liczba wiadomości" moderator_warning_private_messages: title: "Ostrzeżenia moderatorów" xaxis: "Dzień" + yaxis: "Liczba wiadomości" notify_moderators_private_messages: title: "Powiadomienia moderatorów" xaxis: "Dzień" + yaxis: "Liczba wiadomości" notify_user_private_messages: title: "Powiadom użytkownika" xaxis: "Dzień" + yaxis: "Liczba wiadomości" top_referrers: title: "Najczęstszy referrerzy" xaxis: "Użytkownik" @@ -616,8 +629,10 @@ pl_PL: description: "Przewodnik i inne informacje dla nowych użytkowników." welcome_user: title: "Przywitanie: Nowy użytkownik" + description: "Wiadomość wysyłana automatycznie do wszystkich nowych użytkowników zaraz po ich rejestracji." welcome_invite: title: "Przywitanie: Zaproszony użytkownik" + description: "Wiadomość wysyłana automatycznie do nowych użytkowników po zaakceptowaniu zaproszenia od innego użytkownika." login_required_welcome_message: title: "Wymagane logowanie: wiadomość powitalna" description: "Wiadomość powitalna wyświetlana niezalogowanym użytkownikom, gdy ustawienie 'login required' jest włączone." @@ -639,9 +654,12 @@ pl_PL: default_locale: "Domyślny język tej instancji Discourse (kod ISO 639-1)" allow_user_locale: "Zezwól użytkownikom na zmianę języka interfejsu we własnych ustawieniach" min_post_length: "Minimalna długość wpisu w znakach" + min_first_post_length: "Minimalna długość treści (liczba znaków) pierwszego wpisu w temacie " + min_private_message_post_length: "Minimalna długość treści wiadomości " max_post_length: "Maksymalna długość wpisu, w znakach" min_topic_title_length: "Minimalna długość tytułu tematu, w znakach" max_topic_title_length: "Maksymalna długość tytułu tematu, w znakach" + min_private_message_title_length: "Minimalna liczba znaków w temacie wiadomości " min_search_term_length: "Minimalna długość wyszukiwanego tekstu, w znakach" allow_uncategorized_topics: "Pozwól na tworzenie tematów bez kategorii." uncategorized_description: "Znajdują się tu wątki którym jeszcze nie przypisano odpowiedniej kategorii." @@ -650,6 +668,7 @@ pl_PL: educate_until_posts: "Wyświetlaj okno edukacyjne po rozpoczęciu pisania dopóki nowy użytkownik nie napisze tylu wpisów." title: "Nazwa tej strony używana w tagu title." site_description: "Opis strony w jednym zdaniu, wykorzystywany w meta tagu description." + contact_url: "URL kontaktowy dla tego serwisu. Używany na podstronie /about." queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken." crawl_images: "Pobieraj grafiki ze zdalnych URLi aby ustawić poprawną wysokość i szerokość w tagu img." download_remote_images_to_local: "Pobieraj zdalne grafiki i twórz ich lokalne kopie aby zapobiegać uszkodzonym/brakującym obrazkom na stronach." @@ -665,6 +684,8 @@ pl_PL: fixed_category_positions: "Zaznacz, aby ręcznie ustawiać kolejność kategorii. Odznacz, aby kategorie były sortowane na podstawie aktywności. " exclude_rel_nofollow_domains: "A pipe-delimited list of domains where nofollow is not added (tld.com will automatically allow sub.tld.com as well)" post_excerpt_maxlength: "Maksymalna długość podsumowania / streszczenia wpisu." + post_onebox_maxlength: "Maksymalna długość (ilość znaków) treści wpisu osadzonego via Onebox" + onebox_domains_whitelist: "Lista domen dla których włączony jest oneboxing; te domeny powinny wspierać OpenGraph lub oEmbed. Można to sprawdzić na http://iframely.com/debug" logo_url: "Logo w lewym górnym rogu strony; jeśli zostawisz to pole puste zamiast logo pojawi się nazwa strony w formie tekstowej" digest_logo_url: "Alternatywne logo używane w biuletynie email. Jeśli puste, zostanie użyta wartość z pola `logo_url`, np: http://example.com/logo.png" logo_small_url: "Małe logo w lewym górnym rogu strony widoczne przy przewijaniu. Jeśli zostawisz to pole puste zamiast logo pojawi się symbol strony domowej." @@ -672,6 +693,12 @@ pl_PL: apple_touch_icon_url: "Ikona używana przez urządzenia Apple. Rekomendowany wymiar to 144px na 144px." notification_email: "Adres z którego wysyłane będą wszystkie istotne emaile systemowe.\nKonieczna jest poprawna konfiguracja rekordów SPF, DKIM oraz zwrotnego PTR użytej domeny." email_custom_headers: "A pipe-delimited list of custom email headers" + summary_score_threshold: "Minimalny wynik wpisu wymagany do umieszczenia go w 'Podsumowaniu tematu'" + summary_posts_required: "Minimalna liczba wpisów w temacie zanim 'Podsumowanie tematu' jest dostępne" + summary_likes_required: "Minimalna liczba polubień w temacie zanim 'Podsumowanie tematu' jest dostępne" + summary_percent_filter: "Gdy użytkownik kliknie na 'Podsumowaniu tematu', pokaż % najlepszych wpisów" + summary_max_results: "Maksymalna liczba wpisów w 'Podsumowaniu tematu'" + enable_private_messages: "Zezwalaj użytkownikom o 1 poziomie zaufania na tworzenie wiadomości i odpowiadanie na nie." enable_long_polling: "Message bus used for notification can use long polling" anon_polling_interval: "How often should anonymous clients poll in milliseconds" auto_track_topics_after: "Liczba milisekund zanim temat jest automatycznie dodany do śledzonych. użytkownik może nadpisać tę wartość w swoich ustawieniach (0 zawsze, -1 nigdy)" @@ -698,6 +725,8 @@ pl_PL: min_password_length: "Minimalna długość hasła." allow_new_registrations: "Zezwól na rejestrację nowych użytkowników. Odznacz opcję żeby uniemożliwić rejestrację nowych kont." enable_yahoo_logins: "Enable Yahoo authentication" + google_oauth2_client_id: "Client ID twojej aplikacji w Google" + google_oauth2_client_secret: "Client Secret twojej aplikacji w Google" enable_twitter_logins: "Enable Twitter authentication, requires twitter_consumer_key and twitter_consumer_secret" twitter_consumer_key: "Consumer key for Twitter authentication, registered at http://dev.twitter.com" twitter_consumer_secret: "Consumer secret for Twitter authentication, registered at http://dev.twitter.com" @@ -710,7 +739,15 @@ pl_PL: maximum_backups: "Maksymalna liczba kopii zapasowych do przechowywania na dysku. Starsze kopie zapasowe zostaną automatycznie usunięte." backup_daily: "Automatycznie utwórz raz dziennie kopie zapasową strony." active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds" + verbose_localization: "Wyświetlaj dodatkowe identyfikatory tłumaczeń w treści etykiet" previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours" + max_likes_per_day: "Maksymalna liczba polubień per użytkownik per dzień" + max_flags_per_day: "Maksymalna liczba oflagowań per użytkownik per dzień." + max_bookmarks_per_day: "Maksymalna liczba zakładek per użytkownik per dzień." + max_edits_per_day: "Maksymalna liczba edycji per użytkownik per dzień." + max_topics_per_day: "Maksymalna liczba tematów jakie użytkownik może stworzyć jednego dnia." + max_private_messages_per_day: "Maksymalna liczba wiadomości jakie użytkownik może wysłać jednego dnia." + max_invites_per_day: "Maksymalna liczba zaproszeń jakie użytkownik może wysłać jednego dnia." suggested_topics: "Liczba sugerowanych tematów widocznych na końcu aktualnego tematu." limit_suggested_to_category: "Sugeruj tematy jedynie z tej samej kategorii." default_invitee_trust_level: "Domyślny poziom zaufania (0-4) dla zaproszonych użytkowników." @@ -732,7 +769,11 @@ pl_PL: delete_all_posts_max: "The maximum number of posts that can be deleted at once with the Delete All Posts button. If a user has more than this many posts, the posts cannot all be deleted at once and the user can't be deleted." email_editable: "Allow users to change their e-mail address after registration." allow_uploaded_avatars: "Zezwól użytkownikom przesyłanie własnych avatarów." + digest_topics: "Maksymalna liczba tematów w podsumowaniu e-mail." + digest_min_excerpt_length: "Minimalny wycinek wpisu (liczba znaków) w podsumowaniu e-mail." default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." + suppress_digest_email_after_days: "Nie wysyłaj podsumowań e-mail użytkownikom, którzy nie odwiedzili serwisu dłużej niż (n) dni." + disable_digest_emails: "Wyłącz wysyłanie podsumowania e-mail wszystkim uzytkownikom. " default_external_links_in_new_tab: "Otwieraj zewnętrzne odnośniki w nowej karcie. Użytkownicy mogą zmienić to ustawienie w swoich preferencjach." allow_profile_backgrounds: "Zezwól użytkownikom na przesyłanie obrazu tła dla profilu." enable_mobile_theme: "Urządzenia mobilne używają dedykowanego mobilnego szablonu. Wyłącz to, jeśli chcesz użyć własnego, pojedynczego i responsywnego szablonu stylów. " @@ -775,6 +816,30 @@ pl_PL: archived_disabled: "Temat został przywrócony z archiwum. Został odblokowany i może ponownie być zmieniany." closed_enabled: "Temat został zamknięty. Dodawanie nowych odpowiedzi nie jest możliwe." closed_disabled: "Temat został otwarty. Dodawanie odpowiedzi jest ponownie możliwe." + autoclosed_enabled_days: + one: "Ten temat został automatycznie zamknięty po 1 dniu. Tworzenie nowych odpowiedzi nie jest możliwe." + few: "Ten temat został automatycznie zamknięty po %{count} dniach. Tworzenie nowych odpowiedzi nie jest możliwe." + other: "Ten temat został automatycznie zamknięty po %{count} dniach. Tworzenie nowych odpowiedzi nie jest już możliwe." + autoclosed_enabled_hours: + one: "Ten temat został automatycznie zamknięty po 1 godzinie. Tworzenie nowych odpowiedzi nie jest już możliwe." + few: "Ten temat został automatycznie zamknięty po %{count} godzinach. Tworzenie nowych odpowiedzi nie jest już możliwe." + other: "Ten temat został automatycznie zamknięty po %{count} godzinach. Tworzenie nowych odpowiedzi nie jest już możliwe." + autoclosed_enabled_minutes: + one: "Ten temat został automatycznie zamknięty po 1 minucie. Tworzenie nowych odpowiedzi nie jest już możliwe." + few: "Ten temat został automatycznie zamknięty po %{count} minutach. Tworzenie nowych odpowiedzi nie jest już możliwe." + other: "Ten temat został automatycznie zamknięty po %{count} minutach. Tworzenie nowych odpowiedzi nie jest już możliwe." + autoclosed_enabled_lastpost_days: + one: "Ten temat został automatycznie zamknięty 1 dzień po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + few: "Ten temat został automatycznie zamknięty %{count} dni po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + other: "Ten temat został automatycznie zamknięty %{count} dni po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + autoclosed_enabled_lastpost_hours: + one: "Ten temat został automatycznie zamknięty 1 godzinę po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + few: "Ten temat został automatycznie zamknięty %{count} godziny po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + other: "Ten temat został automatycznie zamknięty %{count} godzin po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + autoclosed_enabled_lastpost_minutes: + one: "Ten temat został automatycznie zamknięty 1 minutę po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + few: "Ten temat został automatycznie zamknięty %{count} minuty po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." + other: "Ten temat został automatycznie zamknięty %{count} minut po ostatnim wpisie. Tworzenie nowych odpowiedzi nie jest już możliwe." autoclosed_disabled: "Temat został otwarty. Dodawanie odpowiedzi jest ponownie możliwe." pinned_enabled: "Temat został przypięty. Będzie pojawiać się na początku swojej kategorii dopóki nie zostanie odpięty przez obsługę lub prywatnie przez użytkownika." pinned_disabled: "Temat został odpięty. Już nie będzie pojawiać się na początku swojej kategorii." @@ -946,6 +1011,7 @@ pl_PL: --- %{respond_instructions} digest: + subject_template: "[%{site_name}] Podsumowanie" new_activity: "Nowa aktywność w twoich tematach i wpisach:" top_topics: "Popularne wpisy" other_new_topics: "Popularne wątki" diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index d6a534c2bc..61915c1944 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -698,6 +698,7 @@ zh_CN: flag_sockpuppets: "如果一个新用户开始了一个主题,并且同时另一个新用户以同一个 IP 在该主题回复,他们所有的帖子都将被自动标记为垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用传统换行符,即用两个尾随空格来换行" post_undo_action_window_mins: "允许用户在帖子上进行撤销操作(赞、标记等)所需等待的间隔分钟数" + must_approve_users: "新用户在被允许访问站点前需要由职员批准。警告:在运行的站点中启用将解除所有非职员用户的访问权限!" ga_tracking_code: "Google 分析追踪代码(ga.js),例如:UA-12345678-9。参考 http://google.com/analytics" ga_domain_name: "Google 分析域名(ga.js),例如:mysite.com;参考 http://google.com/analytics" ga_universal_tracking_code: "Google 通用分析追踪代码(analytics.js)追踪代码,例如:UA-12345678-9;参考 http://google.com/analytics" @@ -946,6 +947,7 @@ zh_CN: show_create_topics_notice: "如果站点只有少于 5 篇的公开帖子时,显示一条请管理员创建帖子的提示。" vacuum_db_days: "在数据库迁移后使用完整扫描回收数据库空间(设置 0 为禁用)" prevent_anons_from_downloading_files: "禁止匿名用户下载附件。警告:这将禁止他们访问任何发表在帖子中的非图片资源。" + slug_generation_method: "选择一个链接生成方式。“encoded”将生成以百分号编码的链接。“none”将禁用自定义链接,只生成默认链接。" enable_emoji: "启用绘文字(emoji)" emoji_set: "你喜欢哪一种 emoji?" enforce_square_emoji: "强制为所有 emojis 设置正方形比例。" @@ -1101,6 +1103,13 @@ zh_CN: 你是被一个受信任的用户邀请的,所以你可以不用登录。 invite_password_instructions: subject_template: "为 %{site_name} 账户设置密码" + text_body_template: | + 感谢你接受来自感谢您接受来自%{site_name}的邀请——欢迎! + + 点击下面的链接立即选择一个密码: + %{base_url}/users/password-reset/%{email_token} + + 如果你不记得你的密码,或者你没有设置密码,在通过邮件地址登录时选择“我忘记了密码”。 test_mailer: subject_template: "[%{site_name}] 电子邮件发送测试" text_body_template: | @@ -1198,6 +1207,20 @@ zh_CN: system_messages: post_hidden: subject_template: "%{site_name} 提示:由于论坛用户标记,系统隐藏了你的帖子" + text_body_template: | + 你好, + + 这是一封从%{site_name}自动发出的邮件,通知你的帖子已被隐藏。 + + %{base_url}%{url} + + %{flag_reason} + + 你的帖子是因为被多个社群成员标记才被隐藏的,所以请考虑根据他们的反馈修改你的帖子。**你可以在 %{edit_delay} 分钟后开始编辑你的帖子,之后它会被自动显示。** + + 然而,如果帖子再次被社群标记并隐藏,它将被隐藏并等待版主处理——并且可能导致进一步的措施,包括禁止你的用户帐号的可能。 + + 想了解更多,请查看我们的[社群指引](%{base_url}/guidelines)。 usage_tips: text_body_template: | 这条消息包含了一些可以让你快速开始的技巧提示。 diff --git a/config/routes.rb b/config/routes.rb index 2825488662..7fe30be8f6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -329,9 +329,8 @@ Discourse::Application.routes.draw do end end - get "notifications" => "notifications#recent" - get "notifications/history" => "notifications#history" - put "notifications/reset-new" => 'notifications#reset_new' + get 'notifications' => 'notifications#index' + put 'notifications/mark-read' => 'notifications#mark_read' match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: [:get, :post] match "/auth/failure", to: "users/omniauth_callbacks#failure", via: [:get, :post] diff --git a/config/site_settings.yml b/config/site_settings.yml index 30270d0201..8bb55f956a 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -611,7 +611,9 @@ trust: tl3_requires_likes_received: default: 20 min: 0 - tl3_links_no_follow: false + tl3_links_no_follow: + default: false + client: true security: use_https: false diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index 8d47c86821..647b1f1735 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -106,7 +106,7 @@ before_fork do |server, worker| restart = false if out_of_memory? - Rails.logger.warn("Sidekiq is consuming too much memory (using: %0.2fM), restarting" % (max_rss.to_f / 1.megabyte)) + Rails.logger.warn("Sidekiq is consuming too much memory (using: %0.2fM) for '%s', restarting" % [(max_rss.to_f / 1.megabyte), ENV["DISCOURSE_HOSTNAME"]]) restart = true end diff --git a/lib/assets/quote_email.hbs b/lib/assets/quote_email.hbs deleted file mode 100644 index 61cda02968..0000000000 --- a/lib/assets/quote_email.hbs +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -
{{{avatarImg}}} {{username}}
{{{quote}}}
diff --git a/lib/discourse.rb b/lib/discourse.rb index 2a139eb2d7..845aaf4824 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -256,7 +256,7 @@ module Discourse user ||= User.admins.real.order(:id).first end - SYSTEM_USER_ID = -1 unless defined? SYSTEM_USER_ID + SYSTEM_USER_ID ||= -1 def self.system_user User.find_by(id: SYSTEM_USER_ID) diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index 2777603580..6b5658963e 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -136,7 +136,7 @@ module Tilt def generate_source(scope) js_source = ::JSON.generate(data, quirks_mode: true) - js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping']})['code']" + js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring']})['code']" "new module.exports.Compiler(#{js_source}, '#{module_name(scope.root_path, scope.logical_path)}', #{compiler_options}).#{compiler_method}()" end diff --git a/lib/freedom_patches/better_handlebars_errors.rb b/lib/freedom_patches/better_handlebars_errors.rb index 0f41bd2781..197bd7e24e 100644 --- a/lib/freedom_patches/better_handlebars_errors.rb +++ b/lib/freedom_patches/better_handlebars_errors.rb @@ -3,11 +3,11 @@ module Ember class Template < Tilt::Template # Wrap in an IIFE in development mode to get the correct filename - def compile_ember_handlebars(string) + def compile_ember_handlebars(string, ember_template = 'Handlebars') if ::Rails.env.development? - "(function() { try { return Ember.Handlebars.compile(#{indent(string).inspect}); } catch(err) { throw err; } })()" + "(function() { try { return Ember.#{ember_template}.compile(#{indent(string).inspect}); } catch(err) { throw err; } })()" else - "Handlebars.compile(#{indent(string).inspect});" + "Ember.#{ember_template}.compile(#{indent(string).inspect});" end end end diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb index 3b1f789120..ae0419f291 100644 --- a/lib/letter_avatar.rb +++ b/lib/letter_avatar.rb @@ -15,10 +15,9 @@ class LetterAvatar def self.from_username(username) identity = new identity.color = LetterAvatar::COLORS[ - Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length + Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length ] identity.letter = username[0].upcase - identity end end @@ -32,21 +31,23 @@ class LetterAvatar end def generate(username, size, opts = nil) - identity = Identity.from_username(username) + DistributedMutex.synchronize("letter_avatar_#{version}_#{username}_#{size}") do + identity = Identity.from_username(username) - cache = true - cache = false if opts && opts[:cache] == false + cache = true + cache = false if opts && opts[:cache] == false - size = FULLSIZE if size > FULLSIZE - filename = cached_path(identity, size) + size = FULLSIZE if size > FULLSIZE + filename = cached_path(identity, size) - return filename if cache && File.exists?(filename) + return filename if cache && File.exists?(filename) - fullsize = fullsize_path(identity) - generate_fullsize(identity) if !cache || !File.exists?(fullsize) + fullsize = fullsize_path(identity) + generate_fullsize(identity) if !cache || !File.exists?(fullsize) - OptimizedImage.resize(fullsize, filename, size, size) - filename + OptimizedImage.resize(fullsize, filename, size, size) + filename + end end def cached_path(identity, size) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 4ef8150991..de418dc187 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -114,13 +114,6 @@ module PrettyText end end - ctx['quoteTemplate'] = File.read("#{app_root}/app/assets/javascripts/discourse/templates/quote.hbs") - ctx['quoteEmailTemplate'] = File.read("#{app_root}/lib/assets/quote_email.hbs") - ctx.eval("HANDLEBARS_TEMPLATES = { - 'quote': Handlebars.compile(quoteTemplate), - 'quote_email': Handlebars.compile(quoteEmailTemplate), - };") - ctx end diff --git a/lib/remote_ip_improved.rb b/lib/remote_ip_improved.rb deleted file mode 100644 index 8d1b715798..0000000000 --- a/lib/remote_ip_improved.rb +++ /dev/null @@ -1,129 +0,0 @@ -# https://github.com/rails/rails/pull/7234 - -class RemoteIpImproved - class IpSpoofAttackError < StandardError ; end - - # IP addresses that are "trusted proxies" that can be stripped from - # the comma-delimited list in the X-Forwarded-For header. See also: - # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces - # http://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses. - TRUSTED_PROXIES = %r{ - ^127\.0\.0\.1$ | # localhost - ^::1$ | - ^(10 | # private IP 10.x.x.x - 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255 - 192\.168 | # private IP 192.168.x.x - fc00:: # private IP fc00 - )\. - }x - - attr_reader :check_ip, :proxies - - def initialize(app, check_ip_spoofing = true, custom_proxies = nil) - @app = app - @check_ip = check_ip_spoofing - @proxies = case custom_proxies - when Regexp - custom_proxies - when nil - TRUSTED_PROXIES - else - Regexp.union(TRUSTED_PROXIES, custom_proxies) - end - end - - def call(env) - env["action_dispatch.remote_ip"] = GetIp.new(env, self) - @app.call(env) - end - - class GetIp - - # IP v4 and v6 (with compression) validation regexp - # https://gist.github.com/1289635 - VALID_IP = %r{ - (^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4 - (^( - (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated - (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end - (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6 - (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with - (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon - (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle - (([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - (([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - (([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - (([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - (([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 - ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4 - (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining - (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending - )$) - }x - - def initialize(env, middleware) - @env = env - @middleware = middleware - @calculated_ip = false - end - - # Determines originating IP address. REMOTE_ADDR is the standard - # but will be wrong if the user is behind a proxy. Proxies will set - # HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those. - # HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of - # multiple chained proxies. The first address which is in this list - # if it's not a known proxy will be the originating IP. - # Format of HTTP_X_FORWARDED_FOR: - # client_ip, proxy_ip1, proxy_ip2... - # http://en.wikipedia.org/wiki/X-Forwarded-For - def calculate_ip - client_ip = @env['HTTP_CLIENT_IP'] - forwarded_ip = ips_from('HTTP_X_FORWARDED_FOR').first - remote_addrs = ips_from('REMOTE_ADDR') - - check_ip = client_ip && @middleware.check_ip - if check_ip && forwarded_ip != client_ip - # We don't know which came from the proxy, and which from the user - raise IpSpoofAttackError, "IP spoofing attack?!" \ - "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \ - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" - end - - client_ips = remove_proxies [client_ip, forwarded_ip, remote_addrs].flatten - if client_ips.present? - client_ips.first - else - # If there is no client ip we can return first valid proxy ip from REMOTE_ADDR etc - [remote_addrs, client_ip, forwarded_ip].flatten.find { |ip| valid_ip? ip } - end - end - - def to_s - return @ip if @calculated_ip - @calculated_ip = true - @ip = calculate_ip - end - - private - - def ips_from(header) - @env[header] ? @env[header].strip.split(/[,\s]+/) : [] - end - - def valid_ip?(ip) - ip =~ VALID_IP - end - - def not_a_proxy?(ip) - ip !~ @middleware.proxies - end - - def remove_proxies(ips) - ips.select { |ip| valid_ip?(ip) && not_a_proxy?(ip) } - end - - end - -end diff --git a/lib/sass/discourse_stylesheets.rb b/lib/sass/discourse_stylesheets.rb index 7b2b59f6ef..668d9b51be 100644 --- a/lib/sass/discourse_stylesheets.rb +++ b/lib/sass/discourse_stylesheets.rb @@ -33,9 +33,9 @@ class DiscourseStylesheets def self.compile(target = :desktop, opts={}) @lock.synchronize do - FileUtils.rm(MANIFEST_FULL_PATH, force: true) if opts[:force] # Force a recompile, even in production env + FileUtils.rm(MANIFEST_FULL_PATH, force: true) if opts[:force] builder = self.new(target) - builder.compile + builder.compile(opts) builder.stylesheet_filename end end @@ -76,7 +76,20 @@ class DiscourseStylesheets @target = target end - def compile + def compile(opts={}) + unless opts[:force] + if File.exists?(stylesheet_fullpath) + unless StylesheetCache.where(target: @target, digest: digest).exists? + begin + StylesheetCache.add(@target, digest, File.read(stylesheet_fullpath)) + rescue => e + Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}" + end + end + return true + end + end + scss = File.read("#{Rails.root}/app/assets/stylesheets/#{@target}.scss") css = begin DiscourseSassCompiler.compile(scss, @target) @@ -142,9 +155,14 @@ class DiscourseStylesheets # digest encodes the things that trigger a recompile def digest @digest ||= begin - theme = (cs = ColorScheme.enabled) ? "#{cs.id}-#{cs.version}" : 0 - category_updated = Category.last_updated_at - Digest::SHA1.hexdigest("#{RailsMultisite::ConnectionManagement.current_db}-#{theme}-#{DiscourseStylesheets.last_file_updated}-#{category_updated}") + theme = (cs = ColorScheme.enabled) ? "#{cs.id}-#{cs.version}" : false + category_updated = Category.where("background_url IS NOT NULL and background_url != ''").last_updated_at + + if theme || category_updated > 0 + Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{theme}-#{DiscourseStylesheets.last_file_updated}-#{category_updated}" + else + Digest::SHA1.hexdigest "defaults-#{DiscourseStylesheets.last_file_updated}" + end end end end diff --git a/lib/search.rb b/lib/search.rb index 454aa5b7c5..97779db687 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -426,7 +426,7 @@ class Search def self.ts_query(term, locale = nil, joiner = "&") locale = Post.sanitize(locale) if locale - all_terms = term.gsub(/[*:()&!'"]/,'').squish.split + all_terms = term.gsub(/[\p{P}\p{S}]+/, ' ').squish.split query = Post.sanitize(all_terms.map {|t| "#{PG::Connection.escape_string(t)}:*"}.join(" #{joiner} ")) "TO_TSQUERY(#{locale || query_locale}, #{query})" end diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index aa95714cf9..efba76eff7 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -72,16 +72,18 @@ task 'assets:precompile:before' do end task 'assets:precompile:css' => 'environment' do + puts "Start compiling CSS: #{Time.zone.now}" RailsMultisite::ConnectionManagement.each_connection do |db| # Heroku precompiles assets before db migration, so tables may not exist. # css will get precompiled during first request instead in that case. if ActiveRecord::Base.connection.table_exists?(ColorScheme.table_name) puts "Compiling css for #{db}" [:desktop, :mobile].each do |target| - puts DiscourseStylesheets.compile(target, force: true) + puts DiscourseStylesheets.compile(target) end end end + puts "Done compiling CSS: #{Time.zone.now}" end def assets_path diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index f8b8c25b9c..73118c6b15 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -34,7 +34,7 @@ task "uploads:migrate_from_s3" => :environment do puts "Migrating uploads from S3 to local storage" puts - Upload.order(:id).find_each do |upload| + Upload.find_each do |upload| # remove invalid uploads if upload.url.blank? @@ -100,7 +100,7 @@ task "uploads:clean_up" => :environment do ## # uploads & avatars - Upload.order(:id).find_each do |upload| + Upload.find_each do |upload| path = "#{public_directory}#{upload.url}" if !File.exists?(path) upload.destroy rescue nil @@ -111,7 +111,7 @@ task "uploads:clean_up" => :environment do end # optimized images - OptimizedImage.order(:id).find_each do |optimized_image| + OptimizedImage.find_each do |optimized_image| path = "#{public_directory}#{optimized_image.url}" if !File.exists?(path) optimized_image.destroy rescue nil @@ -157,3 +157,123 @@ task "uploads:clean_up" => :environment do end end + + +# list all missing uploads and optimized images +task "uploads:missing" => :environment do + + public_directory = "#{Rails.root}/public" + + RailsMultisite::ConnectionManagement.each_connection do |db| + + if Discourse.store.external? + puts "This task only works for internal storages." + next + end + + + Upload.find_each do |upload| + + # could be a remote image + next unless upload.url =~ /^\/[^\/]/ + + path = "#{public_directory}#{upload.url}" + bad = true + begin + bad = false if File.size(path) != 0 + rescue + # something is messed up + end + puts path if bad + end + + OptimizedImage.find_each do |optimized_image| + + # remote? + next unless optimized_image.url =~ /^\/[^\/]/ + + path = "#{public_directory}#{optimized_image.url}" + + bad = true + begin + bad = false if File.size(path) != 0 + rescue + # something is messed up + end + puts path if bad + end + + end + +end + +# regenerate missing optimized images +task "uploads:regenerate_missing_optimized" => :environment do + ENV["RAILS_DB"] ? regenerate_missing_optimized : regenerate_missing_optimized_all_sites +end + +def regenerate_missing_optimized_all_sites + RailsMultisite::ConnectionManagement.each_connection { regenerate_missing_optimized } +end + +def regenerate_missing_optimized + db = RailsMultisite::ConnectionManagement.current_db + + puts "Regenerating missing optimized images for '#{db}'..." + + if Discourse.store.external? + puts "This task only works for internal storages." + return + end + + public_directory = "#{Rails.root}/public" + missing_uploads = Set.new + + OptimizedImage.includes(:upload) + .where("LENGTH(COALESCE(url, '')) > 0") + .where("width > 0 AND height > 0") + .find_each do |optimized_image| + + upload = optimized_image.upload + + next unless optimized_image.url =~ /^\/[^\/]/ + next unless upload.url =~ /^\/[^\/]/ + + thumbnail = "#{public_directory}#{optimized_image.url}" + original = "#{public_directory}#{upload.url}" + + if !File.exists?(thumbnail) || File.size(thumbnail) <= 0 + # make sure the original image exists locally + if (!File.exists?(original) || File.size(original) <= 0) && upload.origin.present? + # try to fix it by redownloading it + begin + downloaded = FileHelper.download(upload.origin, SiteSetting.max_image_size_kb.kilobytes, "discourse-missing", true) rescue nil + if downloaded && downloaded.size > 0 + FileUtils.mkdir_p(File.dirname(original)) + File.open(original, "wb") { |f| f.write(downloaded.read) } + end + ensure + downloaded.try(:close!) if downloaded.respond_to?(:close!) + end + end + + if File.exists?(original) && File.size(original) > 0 + FileUtils.mkdir_p(File.dirname(thumbnail)) + OptimizedImage.resize(original, thumbnail, optimized_image.width, optimized_image.height) + putc "#" + else + missing_uploads << original + putc "X" + end + else + putc "." + end + end + + puts "", "Done" + + if missing_uploads.size > 0 + puts "Missing uploads:" + missing_uploads.sort.each { |u| puts u } + end +end diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 2fe19b7561..c4e23aef6b 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -38,7 +38,9 @@ class TopicView self.instance_variable_set("@#{key}".to_sym, value) end - @page = @page.to_i + # work around people somehow sending in arrays, + # arrays are not supported + @page = @page.to_i rescue 1 @page = 1 if @page.zero? @chunk_size = options[:slow_platform] ? TopicView.slow_chunk_size : TopicView.chunk_size @limit ||= @chunk_size diff --git a/lib/version.rb b/lib/version.rb index ce49e0a8cd..e65b9fa4b8 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 3 TINY = 0 - PRE = 'beta8' + PRE = 'beta9' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/plugins/lazyYT/assets/javascripts/lazyYT.js b/plugins/lazyYT/assets/javascripts/lazyYT.js index 498910cdf2..bce0077581 100644 --- a/plugins/lazyYT/assets/javascripts/lazyYT.js +++ b/plugins/lazyYT/assets/javascripts/lazyYT.js @@ -16,11 +16,11 @@ height = $el.data('height'), ratio = ($el.data('ratio')) ? $el.data('ratio') : settings.default_ratio, id = $el.data('youtube-id'), + title = $el.data('youtube-title'), padding_bottom, innerHtml = [], $thumb, thumb_img, - loading_text = $el.text() ? $el.text() : settings.loading_text, youtube_parameters = $el.data('parameters') || ''; ratio = ratio.split(":"); @@ -64,8 +64,12 @@ innerHtml.push('
'); innerHtml.push(''); // .html5-title-text-wrapper @@ -102,15 +106,10 @@ } }); - $.getJSON('https://gdata.youtube.com/feeds/api/videos/' + id + '?v=2&alt=json', function (data) { - $el.find('#lazyYT-title-' + id).text(data.entry.title.$t); - }); - } $.fn.lazyYT = function (newSettings) { var defaultSettings = { - loading_text: 'Loading...', default_ratio: '16:9', callback: null, // ToDO execute callback if given container_class: 'lazyYT-container' diff --git a/plugins/lazyYT/plugin.rb b/plugins/lazyYT/plugin.rb index cf02d90882..7a9b8fc2a2 100644 --- a/plugins/lazyYT/plugin.rb +++ b/plugins/lazyYT/plugin.rb @@ -18,7 +18,7 @@ class Onebox::Engine::YoutubeOnebox def to_html if video_id # Put in the LazyYT div instead of the iframe - "
" + "
" else super end diff --git a/plugins/poll/assets/javascripts/components/poll-option.js.es6 b/plugins/poll/assets/javascripts/components/poll-option.js.es6 index aec6e6c79e..fdc1066314 100644 --- a/plugins/poll/assets/javascripts/components/poll-option.js.es6 +++ b/plugins/poll/assets/javascripts/components/poll-option.js.es6 @@ -12,7 +12,8 @@ export default Em.Component.extend({ var styles = []; if (this.get("color")) { styles.push("color:" + this.get("color")); } if (this.get("background")) { styles.push("background:" + this.get("background")); } - return styles.length > 0 ? styles.join(";") : false; + + return (styles.length > 0 ? styles.join(";") : '').htmlSafe(); }.property("color", "background"), render(buffer) { diff --git a/plugins/poll/assets/javascripts/components/poll-results-standard.js.es6 b/plugins/poll/assets/javascripts/components/poll-results-standard.js.es6 index 64abdf6680..dd778026f7 100644 --- a/plugins/poll/assets/javascripts/components/poll-results-standard.js.es6 +++ b/plugins/poll/assets/javascripts/components/poll-results-standard.js.es6 @@ -13,9 +13,9 @@ export default Em.Component.extend({ if (backgroundColor) { styles.push("background: " + backgroundColor); } option.setProperties({ - percentage: percentage, + percentage, title: I18n.t("poll.option_title", { count: option.get("votes") }), - style: styles.join(";") + style: styles.join(";").htmlSafe() }); }); diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs index b17c1361e9..4f29d18dac 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs @@ -5,7 +5,7 @@ -
+
{{/each}} diff --git a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs index 69662d4729..4d6b3bae16 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs @@ -1,4 +1,19 @@
+
+ {{#if showingResults}} + {{#if isNumber}} + {{poll-results-number poll=poll}} + {{else}} + {{poll-results-standard poll=poll}} + {{/if}} + {{else}} +
    + {{#each option in poll.options}} + {{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}} + {{/each}} +
+ {{/if}} +

{{poll.voters}} @@ -15,21 +30,6 @@ {{/if}} {{/if}}

-
- {{#if showingResults}} - {{#if isNumber}} - {{poll-results-number poll=poll}} - {{else}} - {{poll-results-standard poll=poll}} - {{/if}} - {{else}} -
    - {{#each option in poll.options}} - {{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}} - {{/each}} -
- {{/if}} -
diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 index e9ff8d09f8..2a85abd1c6 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 @@ -5,12 +5,7 @@ function createPollView(container, post, poll, vote) { view = container.lookup("view:poll"); controller.set("vote", vote); - - controller.setProperties({ - model: Em.Object.create(poll), - post: post, - }); - + controller.setProperties({ model: Em.Object.create(poll), post }); view.set("controller", controller); return view; @@ -25,7 +20,7 @@ export default { // listen for back-end to tell us when a post has a poll messageBus.subscribe("/polls", data => { - const post = container.lookup("controller:topic").get("postStream").findLoadedPost(data.post_id); + const post = container.lookup("controller:topic").get('modee.postStream').findLoadedPost(data.post_id); // HACK to trigger the "postViewUpdated" event Em.run.next(_ => post.set("cooked", post.get("cooked") + " ")); }); @@ -33,8 +28,7 @@ export default { // overwrite polls PostView.reopen({ _createPollViews: function($post) { - const self = this, - post = this.get("post"), + const post = this.get("post"), polls = post.get("polls"), votes = post.get("polls_votes") || {}; @@ -54,7 +48,7 @@ export default { pollView = createPollView(container, post, polls[pollName], votes[pollName]); $poll.replaceWith($div); - pollView.constructor.renderer.replaceIn(pollView, $div[0]); + pollView.renderer.replaceIn(pollView, $div[0]); pollViews[pollName] = pollView; }); @@ -80,4 +74,4 @@ export default { }.on("willClearRender") }); } -} +}; diff --git a/plugins/poll/assets/javascripts/poll_dialect.js b/plugins/poll/assets/javascripts/poll_dialect.js index 01c8ff37c7..126f483314 100644 --- a/plugins/poll/assets/javascripts/poll_dialect.js +++ b/plugins/poll/assets/javascripts/poll_dialect.js @@ -8,7 +8,7 @@ const WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "color", "background", "status"]; const WHITELISTED_STYLES = ["color", "background"]; - const ATTRIBUTES_REGEX = new RegExp("(" + WHITELISTED_ATTRIBUTES.join("|") + ")=[^\\s\\]]+", "g"); + const ATTRIBUTES_REGEX = new RegExp("(" + WHITELISTED_ATTRIBUTES.join("|") + ")=['\"]?[^\\s\\]=]+['\"]?", "g"); Discourse.Dialect.replaceBlock({ start: /\[poll([^\]]*)\]([\s\S]*)/igm, @@ -44,8 +44,9 @@ // extract poll attributes (matches[1].match(ATTRIBUTES_REGEX) || []).forEach(function(m) { - var attr = m.split("="); - attributes[DATA_PREFIX + attr[0]] = attr[1]; + var attr = m.split("="), name = attr[0], value = attr[1]; + value = Handlebars.Utils.escapeExpression(value.replace(/["']/g, "")); + attributes[DATA_PREFIX + name] = value; }); // we might need these values later... @@ -97,16 +98,14 @@ contents[0][o].splice(1, 0, attr); } - // // add some information when type is "multiple" - // if (attributes[DATA_PREFIX + "type"] === "multiple") { - - - // } - var result = ["div", attributes], poll = ["div"]; - // 1 - POLL INFO + // 1 - POLL CONTAINER + var container = ["div", { "class": "poll-container" }].concat(contents); + poll.push(container); + + // 2 - POLL INFO var info = ["div", { "class": "poll-info" }]; // # of voters @@ -147,10 +146,6 @@ poll.push(info); - // 2 - POLL CONTAINER - var container = ["div", { "class": "poll-container" }].concat(contents); - poll.push(container); - // 3 - BUTTONS var buttons = ["div", { "class": "poll-buttons" }]; diff --git a/plugins/poll/assets/stylesheets/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss similarity index 85% rename from plugins/poll/assets/stylesheets/poll.scss rename to plugins/poll/assets/stylesheets/common/poll.scss index bc03b5200a..0953710582 100644 --- a/plugins/poll/assets/stylesheets/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -7,9 +7,7 @@ $option-shadow: dark-light-diff($option-background, $primary, 10%, -10%); div.poll { - display: table; border: 1px solid $border-color; - width: 500px; ul, ol { margin: 0; @@ -29,10 +27,9 @@ div.poll { color: $option-foreground; background: $option-background; box-shadow: inset 0 -6px rgba(0,0,0,.25), inset 0 0 0 100px rgba(0,0,0,0); - padding: 0 12px; + padding: .5em .7em; margin-bottom: 10px; border-radius: 4px; - height: 2.3em; &:hover { box-shadow: inset 0 -6px rgba(0,0,0,.35), inset 0 0 0 100px rgba(0,0,0,.1); @@ -66,31 +63,21 @@ div.poll { .poll-info { color: $text-color; - width: 150px; - display: table-cell; text-align: center; vertical-align: middle; - border-right: 1px solid $border-color; - - p { - margin: 40px 20px; - } .info-number { font-size: 3.5em; } .info-text { - display: block; font-size: 1.7em; } } .poll-container { - display: table-cell; vertical-align: middle; padding: 10px; - width: 330px; span { font-size: 2em; @@ -98,16 +85,9 @@ div.poll { } .poll-buttons { - border-top: 1px solid $border-color; - padding: 10px; - button { float: none; } - - .toggle-status { - float: right; - } } .results { diff --git a/plugins/poll/assets/stylesheets/desktop/poll.scss b/plugins/poll/assets/stylesheets/desktop/poll.scss new file mode 100644 index 0000000000..e875c29a03 --- /dev/null +++ b/plugins/poll/assets/stylesheets/desktop/poll.scss @@ -0,0 +1,34 @@ +div.poll { + display: table; + width: 500px; + max-width: 500px; + + .poll-info { + width: 150px; + display: table-cell; + border-left: 1px solid $border-color; + + p { + margin: 40px 20px; + } + + .info-text { + display: block; + } + } + + .poll-container { + display: table-cell; + width: 330px; + max-width: 330px; + } + + .poll-buttons { + border-top: 1px solid $border-color; + padding: 10px; + + .toggle-status { + float: right; + } + } +} diff --git a/plugins/poll/assets/stylesheets/mobile/poll.scss b/plugins/poll/assets/stylesheets/mobile/poll.scss new file mode 100644 index 0000000000..1dc4c95caa --- /dev/null +++ b/plugins/poll/assets/stylesheets/mobile/poll.scss @@ -0,0 +1,9 @@ +div.poll { + .poll-buttons { + padding: 0 5px 5px 5px; + + button { + margin: 4px 2px; + } + } +} diff --git a/plugins/poll/config/locales/client.ar.yml b/plugins/poll/config/locales/client.ar.yml index 79ea4de815..1d803323f4 100644 --- a/plugins/poll/config/locales/client.ar.yml +++ b/plugins/poll/config/locales/client.ar.yml @@ -5,4 +5,15 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -ar: {} +ar: + js: + poll: + voters: + zero: "المصوتون" + one: "المصوت" + other: "المصوتون" + total_votes: + zero: "مجموع الأصوات" + one: "مجموع التصويت" + other: "مجموع الأصوات" + average_rating: "متوسط التصنيف: %{average} " diff --git a/plugins/poll/config/locales/client.de.yml b/plugins/poll/config/locales/client.de.yml index 821318ea03..c19a0ff67f 100644 --- a/plugins/poll/config/locales/client.de.yml +++ b/plugins/poll/config/locales/client.de.yml @@ -8,7 +8,21 @@ de: js: poll: + voters: + zero: "Teilnehmer" + one: "Teilnehmer" + other: "Teilnehmer" + total_votes: + zero: "abgegebene Stimmen" + one: "abgegebene Stimme" + other: "abgegebene Stimmen" average_rating: "Durchschnittliche Bewertung: %{average}" + multiple: + help: + at_least_min_options: "Du kannst %{count} oder mehr Optionen auswählen." + up_to_max_options: "Du kannst bis zu %{count} Optionen auswählen." + x_options: "Du kannst %{count} Optionen auswählen." + between_min_and_max_options: "Du kannst zwischen %{min} und %{max} Optionen auswählen." cast-votes: title: "Gib deine Stimmen ab" label: "Jetzt abstimmen!" @@ -21,10 +35,10 @@ de: open: title: "Umfrage starten" label: "Starten" - confirm: "Möchtest du wirklich diese Umfrage starten?" + confirm: "Möchtest du diese Umfrage wirklich starten?" close: title: "Umfrage beenden" label: "Beenden" - confirm: "Möchtest du wirklich diese Umfrage beenden?" + confirm: "Möchtest du diese Umfrage wirklich beenden?" error_while_toggling_status: "Beim Ändern des Status der Umfrage ist ein Fehler aufgetreten." error_while_casting_votes: "Beim Abstimmen ist ein Fehler aufgetreten." diff --git a/plugins/poll/config/locales/client.es.yml b/plugins/poll/config/locales/client.es.yml index 595e5c174a..a17f23bd30 100644 --- a/plugins/poll/config/locales/client.es.yml +++ b/plugins/poll/config/locales/client.es.yml @@ -8,10 +8,24 @@ es: js: poll: + voters: + zero: "votantes" + one: "votante" + other: "votantes" + total_votes: + zero: "total votos" + one: "total voto" + other: "total votos" average_rating: "Puntuación media: %{average}." + multiple: + help: + at_least_min_options: "Puedes escoger como mínimo %{count} opciones." + up_to_max_options: "Puedes escoger hasta %{count} opciones." + x_options: "Puedes escoger %{count} opciones." + between_min_and_max_options: "Puedes escoger entre %{min} y %{max} opciones." cast-votes: title: "Votar" - label: "Votar ahora" + label: "¡Vota!" show-results: title: "Mostrar los resultados de la encuesta" label: "Mostrar resultados" @@ -21,10 +35,10 @@ es: open: title: "Abrir encuesta" label: "Abrir" - confirm: "¿Estás seguro de querer abrir esta encuesta?" + confirm: "¿Seguro que quieres abrir esta encuesta?" close: title: "Cerrar la encuesta" label: "Cerrar" - confirm: "¿Estás seguro de querer cerrar esta encuesta?" + confirm: "¿Seguro que quieres cerrar esta encuesta?" error_while_toggling_status: "Ha ocurrido un error mientras se cambiaba el estado de esta encuesta." - error_while_casting_votes: "Ha ocurrido un error a la hora de mandar los votos." + error_while_casting_votes: "Ha ocurrido un error a la hora de enviar los votos." diff --git a/plugins/poll/config/locales/client.fa_IR.yml b/plugins/poll/config/locales/client.fa_IR.yml index 12e5b8ac7c..e0445ba6a0 100644 --- a/plugins/poll/config/locales/client.fa_IR.yml +++ b/plugins/poll/config/locales/client.fa_IR.yml @@ -8,7 +8,21 @@ fa_IR: js: poll: + voters: + zero: "voters" + one: "voter" + other: "voters" + total_votes: + zero: "total votes" + one: "total vote" + other: "total votes" average_rating: "میانگین امتیاز: %{average}." + multiple: + help: + at_least_min_options: "You may choose at least %{count} options." + up_to_max_options: "You may choose up to %{count} options." + x_options: "You may choose %{count} options." + between_min_and_max_options: "You may choose between %{min} and %{max} options." cast-votes: title: "انداختن رأی شما" label: "رای بدهید!" diff --git a/plugins/poll/config/locales/client.fi.yml b/plugins/poll/config/locales/client.fi.yml index 52e6958643..203330f0c7 100644 --- a/plugins/poll/config/locales/client.fi.yml +++ b/plugins/poll/config/locales/client.fi.yml @@ -8,7 +8,21 @@ fi: js: poll: + voters: + zero: "äänestäjät" + one: "äänestäjä" + other: "äänestäjät" + total_votes: + zero: "ääntä" + one: "ääni" + other: "ääntä" average_rating: "Keskivertoarvio: %{average}." + multiple: + help: + at_least_min_options: "Voita valita vähintään %{count} vaihtoehtoa." + up_to_max_options: "Voit valita enintään %{count} vaihtoehtoa." + x_options: "Voit valita %{count} vaihtoehtoa." + between_min_and_max_options: "Voit valita %{min}-%{max}%{average}." + multiple: + help: + at_least_min_options: "Puoi scegliere al massimo %{count} opzioni." + up_to_max_options: "Puoi scegliere fino a %{count} opzioni." + x_options: "Puoi scegliere %{count} opzioni." + between_min_and_max_options: "Puoi scegliere tra %{min} e %{max} opzioni." + cast-votes: + title: "Vota" + label: "Vota!" + show-results: + title: "Visualizza i risultati del sondaggio" + label: "Mostra i risultati" + hide-results: + title: "Torna ai tuoi voti" + label: "Nascondi i risultati" + open: + title: "Apri il sondaggio" + label: "Apri" + confirm: "Sicuro di voler aprire questo sondaggio?" + close: + title: "Chiudi il sondaggio" + label: "Chiudi" + confirm: "Sicuro di voler chiudere questo sondaggio?" + error_while_toggling_status: "Si è verificato un errore nel commutare lo stato di questo sondaggio." + error_while_casting_votes: "Si è verificato un errore nella votazione." diff --git a/plugins/poll/config/locales/client.pl_PL.yml b/plugins/poll/config/locales/client.pl_PL.yml index c04d371e5a..0935013e26 100644 --- a/plugins/poll/config/locales/client.pl_PL.yml +++ b/plugins/poll/config/locales/client.pl_PL.yml @@ -5,4 +5,40 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -pl_PL: {} +pl_PL: + js: + poll: + voters: + zero: "głosujących" + one: "głosujący" + other: "głosujących" + total_votes: + zero: "oddanych głosów" + one: "oddany głos" + other: "oddanych głosów" + average_rating: "Średnia ocena: %{average}." + multiple: + help: + at_least_min_options: "Możesz wybrać co najmniej %{count} pozycje." + up_to_max_options: "Możesz wybrać co najwyżej %{count} pozycje." + x_options: "Możesz wybrać %{count} pozycje." + between_min_and_max_options: "Możesz wybrać pomiędzy %{min} a %{max} pozycjami." + cast-votes: + title: "Oddaj głos" + label: "Oddaj głos!" + show-results: + title: "Wyświetl wyniki ankiety" + label: "Pokaż wyniki" + hide-results: + title: "Wróć do oddanych głosów" + label: "Ukryj wyniki" + open: + title: "Otwórz ankietę" + label: "Otwórz" + confirm: "Czy na pewno chcesz otworzyć tę ankietę?" + close: + title: "Zamknij ankietę" + label: "Zamknij" + confirm: "Czy na pewno chcesz zamknąć tę ankietę?" + error_while_toggling_status: "Wystąpił błąd podczas zmiany statusu tej ankiety." + error_while_casting_votes: "Wystąpił błąd podczas oddawania twoich głosów." diff --git a/plugins/poll/config/locales/client.pt_BR.yml b/plugins/poll/config/locales/client.pt_BR.yml index bc85fd86b1..6661d0e50b 100644 --- a/plugins/poll/config/locales/client.pt_BR.yml +++ b/plugins/poll/config/locales/client.pt_BR.yml @@ -5,4 +5,40 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -pt_BR: {} +pt_BR: + js: + poll: + voters: + zero: "votantes" + one: "votante" + other: "votantes" + total_votes: + zero: "total de votos" + one: "total de voto" + other: "total de votos" + average_rating: "Resultado médio: %{average}." + multiple: + help: + at_least_min_options: "Você precisa escolher ao menos %{count} opções." + up_to_max_options: "Você pode escolher %{count} opções." + x_options: "Você pode escolher %{count} opções." + between_min_and_max_options: "Você pode escolher entre %{min} e %{max} opções." + cast-votes: + title: "Seus votos" + label: "Votar agora!" + show-results: + title: "Mostrar o resultado da enquete" + label: "Mostrar resultados" + hide-results: + title: "Voltar para os seus votos" + label: "Esconder resultados" + open: + title: "Abrir a enquete" + label: "Abrir" + confirm: "Você tem certeza que deseja abrir essa enquete?" + close: + title: "Fechar a enquete" + label: "Fechar" + confirm: "Você tem certeza que deseja fechar essa enquete?" + error_while_toggling_status: "Houve um erro ao alternar o status dessa enquete." + error_while_casting_votes: "Houve um erro ao coletar seus votos." diff --git a/plugins/poll/config/locales/client.ru.yml b/plugins/poll/config/locales/client.ru.yml index 599f51fa5b..fffaebada2 100644 --- a/plugins/poll/config/locales/client.ru.yml +++ b/plugins/poll/config/locales/client.ru.yml @@ -8,6 +8,14 @@ ru: js: poll: + voters: + zero: "голосов" + one: "голос" + other: "голосов" + total_votes: + zero: "всего голосов" + one: "всего проголосовало" + other: "всего голосов" average_rating: "Примерный рейтинг: %{average}." cast-votes: title: "Проголосуйте" diff --git a/plugins/poll/config/locales/client.uk.yml b/plugins/poll/config/locales/client.uk.yml index 3acfff74e8..f654b80e8c 100644 --- a/plugins/poll/config/locales/client.uk.yml +++ b/plugins/poll/config/locales/client.uk.yml @@ -5,4 +5,12 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -uk: {} +uk: + js: + poll: + voters: + zero: "проголосувало" + one: "проголосував" + other: "проголосувало" + total_votes: + zero: "кількість голосів" diff --git a/plugins/poll/config/locales/server.cs.yml b/plugins/poll/config/locales/server.cs.yml index 709118475a..65910d615e 100644 --- a/plugins/poll/config/locales/server.cs.yml +++ b/plugins/poll/config/locales/server.cs.yml @@ -8,5 +8,3 @@ cs: site_settings: poll_enabled: "Povolit uživatelům vytvářet hlasování?" - poll: - staff_cannot_add_or_remove_options_after_5_minutes: "Po 5 minutách jdou možnosti hlasováni pouze editovat a nejdou už přidávat ani odebírat. Pokud potřebujete přidat nebo odebrat možnosti, zavřete toto téma a založte nové." diff --git a/plugins/poll/config/locales/server.da.yml b/plugins/poll/config/locales/server.da.yml index 556751c062..02f8b25b78 100644 --- a/plugins/poll/config/locales/server.da.yml +++ b/plugins/poll/config/locales/server.da.yml @@ -19,8 +19,9 @@ da: default_poll_must_have_different_options: "Afstemningen skal have forskellige muligheder." named_poll_must_have_different_options: "Afstemningen %{name} skal have forskellige valgmuligheder." requires_at_least_1_valid_option: "Du skal vælge mindst 1 gyldig mulighed." - cannot_change_polls_after_5_minutes: "Afstemninger kan ikke ændres efter de første 5 minutter. Kontakt en moderator hvis du har brug for at ændre dem." - staff_cannot_add_or_remove_options_after_5_minutes: "Efter de første 5 minutter kan afstemningsmuligheder kun rettes - ikke tilføjes eller slettes. Hvis du vil tilføje eller slette muligheder, skal du lukke emnet og oprette et nyt." + cannot_change_polls_after_5_minutes: "Du kan ikke tilføje, fjerne eller omdøbe afstemninger efter de første 5 minutter." + op_cannot_edit_options_after_5_minutes: "Du kan ikke tilføje eller fjerne afstemningsmuligheder efter de første 5 minutter. Kontakt venligst en moderator hvis du har brug for at rette en mulighed." + staff_cannot_add_or_remove_options_after_5_minutes: "Du kan ikke tilføje eller fjerne afstemningsmuligheder efter de første 5 minutter. Du kan lukke emnet og oprette et nyt istedet." no_polls_associated_with_this_post: "Ingen afstemninger er associeret med dette indlæg." no_poll_with_this_name: "Ingen afstemning med navnet %{name} er associeret med dette indlæg." topic_must_be_open_to_vote: "Emnet skal være åbent for at kunne stemme." diff --git a/plugins/poll/config/locales/server.de.yml b/plugins/poll/config/locales/server.de.yml index 49d3296112..8948fa8c9c 100644 --- a/plugins/poll/config/locales/server.de.yml +++ b/plugins/poll/config/locales/server.de.yml @@ -8,14 +8,20 @@ de: site_settings: poll_enabled: "Benutzern das Erstellen von Umfragen erlauben?" + poll_maximum_options: "Maximale Anzahl der in einer Umfrage erlaubten Optionen." poll: multiple_polls_without_name: "Es gibt mehrere namenlose Umfragen. Benutze das Attribute 'name', um deine Umfragen eindeutig identifizierbar zu machen." multiple_polls_with_same_name: "Es gibt mehre Umfragen mit dem selben Namen: %{name}. Benutze das Attribute 'name', um deine Umfragen eindeutig identifizierbar zu machen." default_poll_must_have_at_least_2_options: "Umfragen müssen mindestens 2 Optionen haben." named_poll_must_have_at_least_2_options: "Die Umfrage mit dem Namen %{name} muss mindestens 2 Optionen haben." + default_poll_must_have_less_options: "Die Umfrage muss weniger als %{max} Optionen haben." + named_poll_must_have_less_options: "Die Umfrage mit dem Namen %{name} muss weniger als %{max} Optionen haben." default_poll_must_have_different_options: "Die Umfrage muss unterschiedliche Optionen haben." - cannot_change_polls_after_5_minutes: "Umfrageoptionen können nach den ersten 5 Minuten nicht mehr geändert werden. Kontaktiere einen Moderator, falls du diese ändern möchtest." - staff_cannot_add_or_remove_options_after_5_minutes: "Nach den ersten 5 Minuten können Umfrageoptionen lediglich bearbeitet, aber nicht hinzugefügt oder entfernt werden. Falls du Optionen hinzufügen oder entfernen möchtest, musst das dieses Thema schließen und ein neues erstellen." + named_poll_must_have_different_options: "Die Umfrage mit dem Namen %{name} muss unterschiedliche Optionen haben." + requires_at_least_1_valid_option: "Sie müssen mindestens 1 gültige Option auswählen." + cannot_change_polls_after_5_minutes: "Nach den ersten 5 Minuten können Sie keine Umfragen mehr hinzufügen, entfernen oder umbenennen." + op_cannot_edit_options_after_5_minutes: "Nach den ersten 5 Minuten können keine Umfrageoptionen mehr hinzugefügt oder entfernt werden. Kontaktieren Sie einen Moderator, wenn Sie eine Umfrageoption ändern müssen." + staff_cannot_add_or_remove_options_after_5_minutes: "Nach den ersten 5 Minuten können keine Umfrageoptionen mehr hinzugefügt oder entfernt werden. Sie sollten dieses Thema schließen und stattdessen ein neues erstellen." no_polls_associated_with_this_post: "Diesem Beitrag sind keine Umfragen zugeordnet." no_poll_with_this_name: "Diesem Beitrag ist keine Umfrage mit dem Namen %{name} zugeordnet." topic_must_be_open_to_vote: "Das Thema muss zum Abstimmen geöffnet sein." diff --git a/plugins/poll/config/locales/server.en.yml b/plugins/poll/config/locales/server.en.yml index 1590536aff..b1d5582c25 100644 --- a/plugins/poll/config/locales/server.en.yml +++ b/plugins/poll/config/locales/server.en.yml @@ -24,8 +24,10 @@ en: named_poll_must_have_different_options: "Poll named %{name} must have different options." requires_at_least_1_valid_option: "You must select at least 1 valid option." - cannot_change_polls_after_5_minutes: "Polls cannot be changed after the first 5 minutes. Contact a moderator if you need to change them." - staff_cannot_add_or_remove_options_after_5_minutes: "After the first 5 minutes, poll options can only be edited, not added or removed. If you need to add or remove options, you should close this topic and create a new one." + + cannot_change_polls_after_5_minutes: "You cannot add, remove or rename polls after the first 5 minutes." + op_cannot_edit_options_after_5_minutes: "You cannot add or remove poll options after the first 5 minutes. Please contact a moderator if you need to edit a poll option." + staff_cannot_add_or_remove_options_after_5_minutes: "You cannot add or remove poll options after the first 5 minutes. You should close this topic and create a new one instead." no_polls_associated_with_this_post: "No polls are associated with this post." no_poll_with_this_name: "No poll named %{name} associated with this post." diff --git a/plugins/poll/config/locales/server.es.yml b/plugins/poll/config/locales/server.es.yml index 86b4c056fc..91a9ffb828 100644 --- a/plugins/poll/config/locales/server.es.yml +++ b/plugins/poll/config/locales/server.es.yml @@ -8,14 +8,20 @@ es: site_settings: poll_enabled: "¿Permitir la creación de encuestas?" + poll_maximum_options: "Número máximo de opciones permitidas en una encuesta." poll: multiple_polls_without_name: "Hay varias encuestas sin nombre. Usa la etiqueta 'name' para diferenciar tus encuestas." multiple_polls_with_same_name: "Hay varias encuestas con el mismo nombre: %{name}. Usa la etiqueta 'name' para diferenciar tus encuestas." default_poll_must_have_at_least_2_options: "La encuesta debe tener al menos 2 opciones." named_poll_must_have_at_least_2_options: "La encuesta llamada %{name} debe tener al menos 2 opciones." - default_poll_must_have_different_options: "La encuesta debe tener opciones diferentes." - cannot_change_polls_after_5_minutes: "Las encuestas no puede cambiarse después de 5 minutos después de su creación. Contacta a un moderador si necesitas cambiarlas." - staff_cannot_add_or_remove_options_after_5_minutes: "Después de los primeros 5 minutos, las opciones de la encuesta solo pueden ser editadas, no se pueden añadir ni eliminar. Si necesitas añadir o eliminar opciones, puedes cerrar este tema y crear uno nuevo." + default_poll_must_have_less_options: "La encuesta debe tener menos de %{max} opciones." + named_poll_must_have_less_options: "La encuesta llamada %{name} debe tener menos de %{max} opciones." + default_poll_must_have_different_options: "La encuesta debe tener diferentes opciones." + named_poll_must_have_different_options: "La encuesta llamada %{name} debe tener diferentes opciones." + requires_at_least_1_valid_option: "Debes escoger al menos 1 opción válida." + cannot_change_polls_after_5_minutes: "No se puede modificar, eliminar o renombrar una encuesta una vez transcurridos 5 minutos después de su creación. " + op_cannot_edit_options_after_5_minutes: "No se puede modificar, eliminar o renombrar una encuesta una vez transcurridos 5 minutos después de su creación. Por favor, contacta un moderador si necesitas editar las opciones de la encuesta." + staff_cannot_add_or_remove_options_after_5_minutes: "No se pueden añadir o eliminar opciones de una encuesta después de los primeros 5 minutos. Podrías cerrar este tema y crear uno nuevo." no_polls_associated_with_this_post: "No hay encuestas asociadas a este post." no_poll_with_this_name: "No hay ninguna encuesta llamada %{name} asociada con este post." topic_must_be_open_to_vote: "Este tema debe estar abierto para votar." diff --git a/plugins/poll/config/locales/server.fa_IR.yml b/plugins/poll/config/locales/server.fa_IR.yml index 3227c512d7..df4e6bc980 100644 --- a/plugins/poll/config/locales/server.fa_IR.yml +++ b/plugins/poll/config/locales/server.fa_IR.yml @@ -8,14 +8,20 @@ fa_IR: site_settings: poll_enabled: "به کاربران اجازه ساخت نظرسنجی ها را بده ؟ " + poll_maximum_options: "Maximum number of options allowed in a poll." poll: multiple_polls_without_name: "چند نظرسنجی متفاوت بدون اسم است. استفاده کن از 'اسم/code>' ویژگی ٬ تا نظرسنجی منحصر به فرد را تشخیص دهد." multiple_polls_with_same_name: "چند نظرسنجی با اسم برابر وجود دارند: %{name}.استفاده کن از 'اسم/code>' ویژگی ٬ تا نظرسنجی منحصر به فرد را تشخیص دهد." default_poll_must_have_at_least_2_options: "نظرسنجی باید حداقل 2 گزینه داشته باشد." named_poll_must_have_at_least_2_options: "اسم نظرسنجی %{name} باید حداقل 2 گزینه داشته باشد. " + default_poll_must_have_less_options: "Poll must have less than %{max} options." + named_poll_must_have_less_options: "Poll named %{name} must have less than %{max} options." default_poll_must_have_different_options: "نظرسنجی باید گزینه های متفاوت داشته باشد. " - cannot_change_polls_after_5_minutes: "نظرسنجی بعد از 5 دقیقه نمی تواند عوض شود. با گردانندگان تماس بگیر اگر نیاز به تغییر آن را دارید. " - staff_cannot_add_or_remove_options_after_5_minutes: "بعد از 5 دقیقه نظرسنجی می تواند ویرایش شود٬ نه اضافه می شود نه حذف می شود. اگر نیاز به اضافه کردن یا پاک کردن گزینه ها را دارید٬ باید جستار را پاک کیند و یک جستار جدید بوجود آورید. " + named_poll_must_have_different_options: "Poll named %{name} must have different options." + requires_at_least_1_valid_option: "You must select at least 1 valid option." + cannot_change_polls_after_5_minutes: "You cannot add, remove or rename polls after the first 5 minutes." + op_cannot_edit_options_after_5_minutes: "You cannot add or remove poll options after the first 5 minutes. Please contact a moderator if you need to edit a poll option." + staff_cannot_add_or_remove_options_after_5_minutes: "You cannot add or remove poll options after the first 5 minutes. You should close this topic and create a new one instead." no_polls_associated_with_this_post: "هیچ نظرسنجی با این نوشته در تماس نیستند. " no_poll_with_this_name: "هیچ نظرسنجی اسم گزاری نشده %{name} در تماس به این نوشته نیست. " topic_must_be_open_to_vote: "جستار باید برای رای گزاری باز باشد. " diff --git a/plugins/poll/config/locales/server.fi.yml b/plugins/poll/config/locales/server.fi.yml index 742d73dc3f..12dc5bf0aa 100644 --- a/plugins/poll/config/locales/server.fi.yml +++ b/plugins/poll/config/locales/server.fi.yml @@ -8,14 +8,17 @@ fi: site_settings: poll_enabled: "Annetaanko käyttäjien luoda äänestyskyselyitä?" + poll_maximum_options: "Äänestysvaihtoehtojen suurin sallittu määrä." poll: multiple_polls_without_name: "Viesti sisältää useamman nimettömän äänestyskyselyn. Nimeä ne 'name'-määreellä." multiple_polls_with_same_name: "Useamman kyselyn nimi on %{name}. Anna kaikille eri nimet 'name'-määreellä." default_poll_must_have_at_least_2_options: "Äänestyksessä pitää olla vähintään 2 vaihtoehtoa." - named_poll_must_have_at_least_2_options: "Äänestyksessä nimeltä %{name} täytyy olla vähintään 2 vaihtoehtoa." + named_poll_must_have_at_least_2_options: "Äänestyksessä nimeltä %{name} pitää olla vähintään 2 vaihtoehtoa." + default_poll_must_have_less_options: "Äänestyksessä pitää olla vähemmän, kuin %{max} vaihtoehtoa." + named_poll_must_have_less_options: "Äänestyksen nimeltä %{name} pitää sisältää vähemmän, kuin %{max} vaihtoehtoa." default_poll_must_have_different_options: "Äänestysvaihtoehtojen on erottava toisistaan." - cannot_change_polls_after_5_minutes: "Äänestyskyselyitä ei voi muuttaa enää 5 minuutin jälkeen. Ota yhteyttä ylläpitoon, jos sinulla on tarve muuttaa kyselyä." - staff_cannot_add_or_remove_options_after_5_minutes: "Ensimmäisen 5 minuutin jälkeen voit enää vain muotaka olemassa olevia äänestysvaihtoehtoja. Jos sinulla on tarve lisätä tai poistaa vaihtoehtoja, sulje tämä ketju ja luo uusi." + named_poll_must_have_different_options: "Äänestyksen nimeltä %{name} vaihtoehtojen on erottava toisistaan." + requires_at_least_1_valid_option: "Sinun täytyy valita vähintään 1 vaihtoehto." no_polls_associated_with_this_post: "Tämä viesti ei sisällä äänestyskyselyä." no_poll_with_this_name: "Tämä viesti ei sisällä äänestyskyselyä nimeltä %{name}." topic_must_be_open_to_vote: "Vain avoimessa ketjussa voi äänestää." diff --git a/plugins/poll/config/locales/server.fr.yml b/plugins/poll/config/locales/server.fr.yml index a9f9f85f22..22321aa0e8 100644 --- a/plugins/poll/config/locales/server.fr.yml +++ b/plugins/poll/config/locales/server.fr.yml @@ -16,8 +16,6 @@ fr: named_poll_must_have_at_least_2_options: "Le sondage %{name} doit contenir au moins deux options." default_poll_must_have_less_options: "Un sondage peut contenir jusque %{max} options." default_poll_must_have_different_options: "Les sondages doivent contenir des options différentes les unes des autres." - cannot_change_polls_after_5_minutes: "Les sondages ne peuvent être modifiés après cinq minutes. Contactez un modérateur si vous avez besoin de le faire." - staff_cannot_add_or_remove_options_after_5_minutes: "Après cinq minutes, les options du sondage peuvent uniquement être modifiées. Vous ne pouvez pas en ajouter ou en supprimer. Si vous avez besoin de le faire, vous devez fermer ce sujet et en créer en nouveau." no_polls_associated_with_this_post: "Aucun sondage n'est associé à ce message." no_poll_with_this_name: "Aucun sondage nommé %{name} n'est associé avec ce message." topic_must_be_open_to_vote: "Le sujet doit être ouvert pour pouvoir voter." diff --git a/plugins/poll/config/locales/server.it.yml b/plugins/poll/config/locales/server.it.yml index 060eb8db01..1f096450b5 100644 --- a/plugins/poll/config/locales/server.it.yml +++ b/plugins/poll/config/locales/server.it.yml @@ -8,8 +8,21 @@ it: site_settings: poll_enabled: "Permettere agli utenti di creare sondaggi?" + poll_maximum_options: "Numero massimo di opzioni permesse in un sondaggio." poll: multiple_polls_without_name: "Ci sono più sondaggi privi di nome. Usa l'attributo 'name' per identificare univocamente i tuoi sondaggi." multiple_polls_with_same_name: "Ci sono più sondaggi con lo stesso nome: %{name}. Usa l'attributo 'name' per identificare univocamente i tuoi sondaggi." default_poll_must_have_at_least_2_options: "Il sondaggio deve avere almeno due opzioni." named_poll_must_have_at_least_2_options: "Il sondaggio con nome %{name} deve avere almeno due opzioni." + default_poll_must_have_less_options: "Il sondaggio deve avere meno di %{max} opzioni." + named_poll_must_have_less_options: "Il sondaggio dal nome %{name} deve avere meno di %{max} opzioni." + default_poll_must_have_different_options: "Il sondaggio deve avere opzioni diverse." + named_poll_must_have_different_options: "Il sondaggio dal nome %{name} deve avere opzioni diverse." + requires_at_least_1_valid_option: "Devi scegliere almeno una opzione valida." + cannot_change_polls_after_5_minutes: "Non puoi aggiungere, cancellare o rinominare i sondaggi dopo i primi 5 minuti." + no_polls_associated_with_this_post: "Non ci sono sondaggi associati con questo messaggio." + no_poll_with_this_name: "Nessun sondaggio avente nome %{name} è associato a questo messaggio." + topic_must_be_open_to_vote: "L'argomento deve essere aperto per poter votare." + poll_must_be_open_to_vote: "Il sondaggio deve essere aperto per poter votare." + topic_must_be_open_to_toggle_status: "L'argomento deve essere aperto per commutare lo stato." + only_staff_or_op_can_toggle_status: "Solo un membro dello staff o l'autore originale possono commutare lo stato di un sondaggio." diff --git a/plugins/poll/config/locales/server.pl_PL.yml b/plugins/poll/config/locales/server.pl_PL.yml index c04d371e5a..9e64538081 100644 --- a/plugins/poll/config/locales/server.pl_PL.yml +++ b/plugins/poll/config/locales/server.pl_PL.yml @@ -5,4 +5,26 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -pl_PL: {} +pl_PL: + site_settings: + poll_enabled: "Pozwolić użytkownikom na tworzenie ankiet?" + poll_maximum_options: "Maksymalna ilość dozwolonych opcji w ankiecie." + poll: + multiple_polls_without_name: "Istnieje kilka nienazwanych ankiet. Uzyj atrybutu 'name', aby umożliwić ich jednoznaczną identyfikację." + multiple_polls_with_same_name: "Istnieje kilka ankiet o tej samej nazwie: %{name}. Użyj atrybutu 'name', aby umożliwić ich jednoznaczną identyfikację." + default_poll_must_have_at_least_2_options: "Ankieta musi posiadać co najmniej 2 opcje." + named_poll_must_have_at_least_2_options: "Ankieta %{name} musi posiadać co najmniej 2 opcje do wyboru." + default_poll_must_have_less_options: "Ankieta musi posiadać mniej niż %{max} opcji do wyboru." + named_poll_must_have_less_options: "Ankieta %{name} musi posiadać mniej niż %{max} opcji do wyboru." + default_poll_must_have_different_options: "Ankieta musi posiadać kilka różnych opcji do wyboru." + named_poll_must_have_different_options: "Ankieta %{name} musi posiadać kilka różnych opcji do wyboru." + requires_at_least_1_valid_option: "Musisz wybrać co najmniej 1 poprawną opcje." + cannot_change_polls_after_5_minutes: "Po upływie 5 minut ankiety nie mogą być zmieniane." + op_cannot_edit_options_after_5_minutes: "Po upływie 5 minut nie można dodawać lub usuwać opcji wyboru w ankietach. Skontaktuj się z moderatorem jeśli naprawdę musisz zmienić opcję w tej ankiecie." + staff_cannot_add_or_remove_options_after_5_minutes: "Po upływie 5 minut nie można dodawać lub usuwać opcji wyboru w ankietach. Jeśli mimo wszystko chcesz wprowadzić tak drastyczne zmiany, zamknij/usuń ten temat i utwórz poprawną ankietę w nowym." + no_polls_associated_with_this_post: "Wskazany wpis nie posiada przypisanych ankiet." + no_poll_with_this_name: "Ankieta %{name} nie jest przypisana do tego wpisu." + topic_must_be_open_to_vote: "Głosowanie jest możliwe tylko w otwartych tematach." + poll_must_be_open_to_vote: "Głosowanie jest możliwe tylko w otwartych ankietach." + topic_must_be_open_to_toggle_status: "Zmiana statusu jest możliwa jedynie w otwartych tematach." + only_staff_or_op_can_toggle_status: "Status może być zmieniony przez autora wpisu lub członka załogi serwisu." diff --git a/plugins/poll/config/locales/server.pt_BR.yml b/plugins/poll/config/locales/server.pt_BR.yml index 691a6527d3..2344927dc6 100644 --- a/plugins/poll/config/locales/server.pt_BR.yml +++ b/plugins/poll/config/locales/server.pt_BR.yml @@ -8,6 +8,11 @@ pt_BR: site_settings: poll_enabled: "Permitir que usuários criem votações?" + poll_maximum_options: "Número máximo de opções permitidas em uma enquete." poll: + multiple_polls_without_name: "Existem várias enquetes sem nome. Use o atributo 'name' para identificar suas enquetes." + multiple_polls_with_same_name: "Existem várias enquetes com o mesmo nome: %{name}. Use o atributo 'name' para identificar suas enquetes." + default_poll_must_have_at_least_2_options: "Enquetes devem ter ao mínimo 2 opções." + named_poll_must_have_at_least_2_options: "A enquete de nome %{name} deve ter ao menos 2 opções." default_poll_must_have_different_options: "A votação deve ter opções diferentes." no_polls_associated_with_this_post: "Nenhuma votação está associada com essa mensagem." diff --git a/plugins/poll/config/locales/server.sq.yml b/plugins/poll/config/locales/server.sq.yml index fd2389b404..8640d0c933 100644 --- a/plugins/poll/config/locales/server.sq.yml +++ b/plugins/poll/config/locales/server.sq.yml @@ -14,8 +14,6 @@ sq: default_poll_must_have_at_least_2_options: "Poll must have at least 2 options." named_poll_must_have_at_least_2_options: "Poll named %{name} must have at least 2 options." default_poll_must_have_different_options: "Poll must have different options." - cannot_change_polls_after_5_minutes: "Polls cannot be changed after the first 5 minutes. Contact a moderator if you need to change them." - staff_cannot_add_or_remove_options_after_5_minutes: "After the first 5 minutes, poll options can only be edited, not added or removed. If you need to add or remove options, you should close this topic and create a new one." no_polls_associated_with_this_post: "No polls are associated with this post." no_poll_with_this_name: "No poll named %{name} associated with this post." topic_must_be_open_to_vote: "The topic must be open to vote." diff --git a/plugins/poll/config/locales/server.tr_TR.yml b/plugins/poll/config/locales/server.tr_TR.yml index 81cc3025f8..dbce7401ac 100644 --- a/plugins/poll/config/locales/server.tr_TR.yml +++ b/plugins/poll/config/locales/server.tr_TR.yml @@ -14,7 +14,6 @@ tr_TR: default_poll_must_have_at_least_2_options: "Ankette en az 2 seçenek olmalı." named_poll_must_have_at_least_2_options: "%{name} isimli ankette en az 2 seçenek olmalı." default_poll_must_have_different_options: "Anketin farklı seçenekleri olmalı." - cannot_change_polls_after_5_minutes: "Anketler ilk 5 dakikadan sonra değiştirilemez. Değiştirmeniz gerekiyorsa bir moderatör ile iletişime geçin." no_polls_associated_with_this_post: "Bu gönderiyle alakalı bir anket yok." no_poll_with_this_name: "Bu gönderiyle alakalı %{name} isimli bir anket yok." topic_must_be_open_to_vote: "Konu oylanabilmesi için açık olmalı." diff --git a/plugins/poll/config/locales/server.uk.yml b/plugins/poll/config/locales/server.uk.yml index 3acfff74e8..7e72e9bbe6 100644 --- a/plugins/poll/config/locales/server.uk.yml +++ b/plugins/poll/config/locales/server.uk.yml @@ -5,4 +5,7 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -uk: {} +uk: + site_settings: + poll_enabled: "Дозволити користувачам створювати опитування?" + poll_maximum_options: "Максимальна кількість варіантів в опитуванні" diff --git a/plugins/poll/config/locales/server.zh_CN.yml b/plugins/poll/config/locales/server.zh_CN.yml index 3725f029bf..4cd1599072 100644 --- a/plugins/poll/config/locales/server.zh_CN.yml +++ b/plugins/poll/config/locales/server.zh_CN.yml @@ -19,8 +19,9 @@ zh_CN: default_poll_must_have_different_options: "投票必须有不同的选项。" named_poll_must_have_different_options: "%{name}投票的选项必须有不同的选项。" requires_at_least_1_valid_option: "你必须选择至少 1 个有效的选项。" - cannot_change_polls_after_5_minutes: "投票在创建的 5 分钟后不能被修改。如果你要修改他们,请联系版主。" - staff_cannot_add_or_remove_options_after_5_minutes: "投票创建 5 分钟后,投票选项只能被编辑,无法增加或者删除。如果你想要增加或删除选项,你应该关闭这个主题并再创建一个新的。" + cannot_change_polls_after_5_minutes: "你不能在创建 5 分钟后添加、删除或重命名投票。" + op_cannot_edit_options_after_5_minutes: "你不能在创建 5 分钟后添加或删除投票选项。如果你需要修改投票选项请联系版主。" + staff_cannot_add_or_remove_options_after_5_minutes: "你不能在创建 5 分钟后添加或删除投票选项。你应该关闭这个主题并创建一个新的。" no_polls_associated_with_this_post: "这个帖子中没有投票。" no_poll_with_this_name: "投票“%{name}”没有被关联到帖子。" topic_must_be_open_to_vote: "主题必须开放才能投票。" diff --git a/plugins/poll/lib/tasks/migrate_old_polls.rake b/plugins/poll/lib/tasks/migrate_old_polls.rake index d1422cc1fd..6a3ba5982f 100644 --- a/plugins/poll/lib/tasks/migrate_old_polls.rake +++ b/plugins/poll/lib/tasks/migrate_old_polls.rake @@ -14,13 +14,12 @@ task "poll:migrate_old_polls" => :environment do # go back in time Timecop.freeze(post.created_at + 1.minute) do # fix the RAW when needed + post.raw << "\n\n" if post.raw !~ /\[poll\]/ lists = /^[ ]*- .+?$\n\n/m.match(post.raw) next if lists.blank? || lists.length == 0 first_list = lists[0] post.raw = post.raw.sub(first_list, "\n[poll]\n#{first_list.strip}\n[/poll]\n") - else - post.raw = post.raw + " " end # save the poll post.save diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index b5e48f254c..3808e0aaa5 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -4,7 +4,10 @@ # authors: Vikhyat Korrapati (vikhyat), Régis Hanol (zogstrip) # url: https://github.com/discourse/discourse/tree/master/plugins/poll -register_asset "stylesheets/poll.scss" +register_asset "stylesheets/common/poll.scss" +register_asset "stylesheets/desktop/poll.scss", :desktop +register_asset "stylesheets/mobile/poll.scss", :mobile + register_asset "javascripts/poll_dialect.js", :server_side PLUGIN_NAME ||= "discourse_poll".freeze @@ -260,10 +263,10 @@ after_initialize do if polls.keys != previous_polls.keys || polls.values.map { |p| p["options"] } != previous_polls.values.map { |p| p["options"] } - # outside the 5-minute edit window? + # outside of the 5-minute edit window? if post.created_at < 5.minutes.ago - # cannot add/remove/change/re-order polls - if polls.keys != previous_polls.keys + # cannot add/remove/rename polls + if polls.keys.sort != previous_polls.keys.sort post.errors.add(:base, I18n.t("poll.cannot_change_polls_after_5_minutes")) return end @@ -278,8 +281,8 @@ after_initialize do end end else - # OP cannot change polls - post.errors.add(:base, I18n.t("poll.cannot_change_polls_after_5_minutes")) + # OP cannot edit poll options + post.errors.add(:base, I18n.t("poll.op_cannot_edit_options_after_5_minutes")) return end end diff --git a/plugins/poll/spec/controllers/posts_controller_spec.rb b/plugins/poll/spec/controllers/posts_controller_spec.rb index 0862586531..3d63d8425b 100644 --- a/plugins/poll/spec/controllers/posts_controller_spec.rb +++ b/plugins/poll/spec/controllers/posts_controller_spec.rb @@ -49,6 +49,14 @@ describe PostsController do expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_less_options", max: SiteSetting.poll_maximum_options)) end + it "prevents self-xss" do + xhr :post, :create, { title: title, raw: "[poll name=]\n- A\n- B\n[/poll]" } + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["cooked"]).to match("data-poll-") + expect(json["polls"]["<script>alert(xss)</script>"]).to be + end + describe "edit window" do describe "within the first 5 minutes" do @@ -92,7 +100,7 @@ describe PostsController do xhr :put, :update, { id: post_id, post: { raw: new_raw } } expect(response).not_to be_success json = ::JSON.parse(response.body) - expect(json["errors"][0]).to eq(I18n.t("poll.cannot_change_polls_after_5_minutes")) + expect(json["errors"][0]).to eq(I18n.t("poll.op_cannot_edit_options_after_5_minutes")) end it "can be edited by staff" do @@ -112,14 +120,14 @@ describe PostsController do describe "named polls" do it "should have different options" do - xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A\n- A[/poll]" } + xhr :post, :create, { title: title, raw: "[poll name=""foo""]\n- A\n- A[/poll]" } expect(response).not_to be_success json = ::JSON.parse(response.body) expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_different_options", name: "foo")) end it "should have at least 2 options" do - xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A[/poll]" } + xhr :post, :create, { title: title, raw: "[poll name='foo']\n- A[/poll]" } expect(response).not_to be_success json = ::JSON.parse(response.body) expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_at_least_2_options", name: "foo")) diff --git a/script/import_scripts/sfn.rb b/script/import_scripts/sfn.rb index b5178146c2..f8a14f00d9 100644 --- a/script/import_scripts/sfn.rb +++ b/script/import_scripts/sfn.rb @@ -40,7 +40,7 @@ class ImportScripts::Sfn < ImportScripts::Base @external_users = {} - CSV.foreach("/Users/zogstrip/Downloads/sfn.csv", col_sep: ";") do |row| + CSV.foreach("/Users/zogstrip/Desktop/sfn.csv", col_sep: ";") do |row| next unless @personify_id_to_contact_key.include?(row[0]) id = @personify_id_to_contact_key[row[0]] @@ -90,6 +90,7 @@ class ImportScripts::Sfn < ImportScripts::Base id: user["id"], email: email, name: full_name, + username: email.split("@")[0], bio_raw: bio, created_at: user["created_at"], post_create_action: proc do |newuser| @@ -191,13 +192,13 @@ class ImportScripts::Sfn < ImportScripts::Base "{9613BAC2-229B-4563-9E1C-35C31CDDCE2F}" => 49, # "Students", } - # def import_categories - # puts "", "importing categories..." + def import_categories + puts "", "importing categories..." - # create_categories(NEW_CATEGORIES) do |category| - # { id: category, name: category } - # end - # end + create_categories(NEW_CATEGORIES) do |category| + { id: category, name: category } + end + end def import_topics puts "", "importing topics..." diff --git a/script/memstats.rb b/script/memstats.rb old mode 100644 new mode 100755 diff --git a/script/test_email_settings.rb b/script/test_email_settings.rb old mode 100644 new mode 100755 diff --git a/script/version_bump.rb b/script/version_bump.rb old mode 100644 new mode 100755 diff --git a/spec/components/discourse_stylesheets_spec.rb b/spec/components/discourse_stylesheets_spec.rb index 79c3aa477d..c7be72e96d 100644 --- a/spec/components/discourse_stylesheets_spec.rb +++ b/spec/components/discourse_stylesheets_spec.rb @@ -7,14 +7,14 @@ describe DiscourseStylesheets do it "can compile desktop bundle" do DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"]) builder = described_class.new(:desktop) - expect(builder.compile).to include('my-plugin-thing') + expect(builder.compile(force: true)).to include('my-plugin-thing') FileUtils.rm builder.stylesheet_fullpath end it "can compile mobile bundle" do DiscoursePluginRegistry.stubs(:mobile_stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"]) builder = described_class.new(:mobile) - expect(builder.compile).to include('my-plugin-thing') + expect(builder.compile(force: true)).to include('my-plugin-thing') FileUtils.rm builder.stylesheet_fullpath end @@ -24,7 +24,7 @@ describe DiscourseStylesheets do "#{Rails.root}/spec/fixtures/scss/broken.scss" ]) builder = described_class.new(:desktop) - expect(builder.compile).not_to include('my-plugin-thing') + expect(builder.compile(force: true)).not_to include('my-plugin-thing') FileUtils.rm builder.stylesheet_fullpath end end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb index cb9771447d..175aea63b6 100644 --- a/spec/controllers/notifications_controller_spec.rb +++ b/spec/controllers/notifications_controller_spec.rb @@ -6,17 +6,17 @@ describe NotificationsController do let!(:user) { log_in } it 'should succeed for recent' do - xhr :get, :recent + xhr :get, :index, recent: true expect(response).to be_success end it 'should succeed for history' do - xhr :get, :history + xhr :get, :index expect(response).to be_success end - it 'should succeed for history' do - xhr :get, :reset_new + it 'should succeed' do + xhr :put, :mark_read expect(response).to be_success end @@ -24,7 +24,7 @@ describe NotificationsController do notification = Fabricate(:notification, user: user) expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) - xhr :get, :recent + xhr :get, :index, recent: true expect(user.reload.unread_notifications).to eq(0) expect(user.reload.total_unread_notifications).to eq(1) end @@ -33,7 +33,7 @@ describe NotificationsController do notification = Fabricate(:notification, user: user) expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) - xhr :get, :recent, silent: true + xhr :get, :index, recent: true, silent: true expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) end @@ -42,7 +42,7 @@ describe NotificationsController do notification = Fabricate(:notification, user: user) expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) - xhr :put, :reset_new + xhr :put, :mark_read user.reload expect(user.reload.unread_notifications).to eq(0) expect(user.reload.total_unread_notifications).to eq(0) @@ -51,7 +51,7 @@ describe NotificationsController do context 'when not logged in' do it 'should raise an error' do - expect { xhr :get, :recent }.to raise_error(Discourse::NotLoggedIn) + expect { xhr :get, :index, recent: true }.to raise_error(Discourse::NotLoggedIn) end end diff --git a/test/javascripts/controllers/header-test.js.es6 b/test/javascripts/controllers/header-test.js.es6 deleted file mode 100644 index 4b82269b7f..0000000000 --- a/test/javascripts/controllers/header-test.js.es6 +++ /dev/null @@ -1,33 +0,0 @@ -moduleFor("controller:header", "controller:header", { - needs: ['controller:application'] -}); - -test("showNotifications action", function() { - let resolveRequestWith; - const request = new Ember.RSVP.Promise(function(resolve) { - resolveRequestWith = resolve; - }); - - const currentUser = Discourse.User.create({ unread_notifications: 1}); - const controller = this.subject({ currentUser: currentUser }); - const viewSpy = { showDropdownBySelector: sinon.spy() }; - - sandbox.stub(Discourse, "ajax").withArgs("/notifications").returns(request); - - Ember.run(function() { - controller.send("showNotifications", viewSpy); - }); - - equal(controller.get("notifications"), null, "notifications are null before data has finished loading"); - equal(currentUser.get("unread_notifications"), 1, "current user's unread notifications count is not zeroed before data has finished loading"); - ok(viewSpy.showDropdownBySelector.calledWith("#user-notifications"), "dropdown with loading glyph is shown before data has finished loading"); - - Ember.run(function() { - resolveRequestWith(["notification"]); - }); - - // Can't use deepEquals because controller.get("notifications") is an ArrayProxy, not an Array - ok(controller.get("notifications").indexOf("notification") !== -1, "notification is in the controller"); - equal(currentUser.get("unread_notifications"), 0, "current user's unread notifications count is zeroed after data has finished loading"); - ok(viewSpy.showDropdownBySelector.calledWith("#user-notifications"), "dropdown with notifications is shown after data has finished loading"); -}); diff --git a/test/javascripts/controllers/notification-test.js.es6 b/test/javascripts/controllers/notification-test.js.es6 deleted file mode 100644 index 63e0da8644..0000000000 --- a/test/javascripts/controllers/notification-test.js.es6 +++ /dev/null @@ -1,54 +0,0 @@ -import Site from 'discourse/models/site'; - -const notificationFixture = { - notification_type: 1, //mentioned - post_number: 1, - topic_id: 1234, - slug: "a-slug", - data: { - topic_title: "some title", - display_username: "velesin" - }, - site: Site.current() -}; - -moduleFor("controller:notification"); - -test("scope property is correct", function() { - const controller = this.subject(notificationFixture); - equal(controller.get("scope"), "notifications.mentioned"); -}); - -test("username property is correct", function() { - const controller = this.subject(notificationFixture); - equal(controller.get("username"), "velesin"); -}); - -test("description property returns badge name when there is one", function() { - const fixtureWithBadgeName = _.extend({}, notificationFixture, { data: { badge_name: "badge" } }); - const controller = this.subject(fixtureWithBadgeName); - equal(controller.get("description"), "badge"); -}); - -test("description property returns empty string when there is no topic title", function() { - const fixtureWithEmptyTopicTitle = _.extend({}, notificationFixture, { data: { topic_title: "" } }); - const controller = this.subject(fixtureWithEmptyTopicTitle); - equal(controller.get("description"), ""); -}); - -test("description property returns topic title", function() { - const fixtureWithTopicTitle = _.extend({}, notificationFixture, { data: { topic_title: "topic" } }); - const controller = this.subject(fixtureWithTopicTitle); - equal(controller.get("description"), "topic"); -}); - -test("url property returns badge's url when there is a badge", function() { - const fixtureWithBadge = _.extend({}, notificationFixture, { data: { badge_id: 1, badge_name: "Badge Name"} }); - const controller = this.subject(fixtureWithBadge); - equal(controller.get("url"), "/badges/1/badge-name"); -}); - -test("url property returns topic's url when there is a topic", function() { - const controller = this.subject(notificationFixture); - equal(controller.get("url"), "/t/a-slug/1234"); -}); diff --git a/test/javascripts/controllers/site-map-category-test.js.es6 b/test/javascripts/controllers/site-map-category-test.js.es6 index 8977799788..77cdbf145b 100644 --- a/test/javascripts/controllers/site-map-category-test.js.es6 +++ b/test/javascripts/controllers/site-map-category-test.js.es6 @@ -20,8 +20,7 @@ test("unreadTotal default", function() { test("unreadTotal with values", function() { var controller = this.subject({ currentUser: Discourse.User.create(), - unreadTopics: 1, - newTopics: 3 + model: { unreadTopics: 1, newTopics: 3 } }); equal(controller.get('unreadTotal'), 4); }); diff --git a/test/javascripts/fixtures/notification_fixtures.js.es6 b/test/javascripts/fixtures/notification_fixtures.js.es6 index 40e4e6a2b6..22493d34ee 100644 --- a/test/javascripts/fixtures/notification_fixtures.js.es6 +++ b/test/javascripts/fixtures/notification_fixtures.js.es6 @@ -1,2 +1,2 @@ /*jshint maxlen:10000000 */ -export default {"/notifications": [ { notification_type: 2, read: true, post_number: 2, topic_id: 1234, slug: "a-slug", data: { topic_title: "some title", display_username: "velesin" } } ] }; +export default {"/notifications": {notifications: [ { id: 123, notification_type: 2, read: true, post_number: 2, topic_id: 1234, slug: "a-slug", data: { topic_title: "some title", display_username: "velesin" } } ] }}; diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 054a4686a4..64547080b4 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -37,8 +37,6 @@ var oldAvatar = Discourse.Utilities.avatarImg; function acceptance(name, options) { module("Acceptance: " + name, { setup: function() { - Ember.run(Discourse, Discourse.advanceReadiness); - // Don't render avatars in acceptance tests, it's faster and no 404s Discourse.Utilities.avatarImg = () => ""; @@ -72,6 +70,8 @@ function acceptance(name, options) { if (options && options.teardown) { options.teardown.call(this); } + Discourse.User.resetCurrent(); + Discourse.Site.resetCurrent(Discourse.Site.create(fixtures['site.json'].site)); Discourse.Utilities.avatarImg = oldAvatar; Discourse.reset(); diff --git a/test/javascripts/jshint-test.js.es6.erb b/test/javascripts/jshint-test.js.es6.erb index cccd67f15c..6c5884aeaa 100644 --- a/test/javascripts/jshint-test.js.es6.erb +++ b/test/javascripts/jshint-test.js.es6.erb @@ -79,7 +79,6 @@ qHint.sendRequest = function (url, callback) { req.send(); }; - <% TO_IGNORE = File.read("#{Rails.root}/.jshintignore").split("\n") diff --git a/test/javascripts/mdtest/fixtures/Amps and angle encoding.text b/test/javascripts/mdtest/fixtures/Amps and angle encoding.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Amps and angle encoding.xhtml b/test/javascripts/mdtest/fixtures/Amps and angle encoding.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Auto links.text b/test/javascripts/mdtest/fixtures/Auto links.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Auto links.xhtml b/test/javascripts/mdtest/fixtures/Auto links.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Backslash escapes.text b/test/javascripts/mdtest/fixtures/Backslash escapes.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Backslash escapes.xhtml b/test/javascripts/mdtest/fixtures/Backslash escapes.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Blockquotes with code blocks.text b/test/javascripts/mdtest/fixtures/Blockquotes with code blocks.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Blockquotes with code blocks.xhtml b/test/javascripts/mdtest/fixtures/Blockquotes with code blocks.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Code Blocks.text b/test/javascripts/mdtest/fixtures/Code Blocks.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Code Blocks.xhtml b/test/javascripts/mdtest/fixtures/Code Blocks.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Code Spans.text b/test/javascripts/mdtest/fixtures/Code Spans.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Code Spans.xhtml b/test/javascripts/mdtest/fixtures/Code Spans.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Hard-wrapped paragraphs with list-like lines.text b/test/javascripts/mdtest/fixtures/Hard-wrapped paragraphs with list-like lines.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Hard-wrapped paragraphs with list-like lines.xhtml b/test/javascripts/mdtest/fixtures/Hard-wrapped paragraphs with list-like lines.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Horizontal rules.text b/test/javascripts/mdtest/fixtures/Horizontal rules.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Horizontal rules.xhtml b/test/javascripts/mdtest/fixtures/Horizontal rules.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Images.text b/test/javascripts/mdtest/fixtures/Images.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Images.xhtml b/test/javascripts/mdtest/fixtures/Images.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Inline HTML (Advanced).text b/test/javascripts/mdtest/fixtures/Inline HTML (Advanced).text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Inline HTML (Advanced).xhtml b/test/javascripts/mdtest/fixtures/Inline HTML (Advanced).xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Inline HTML (Simple).text b/test/javascripts/mdtest/fixtures/Inline HTML (Simple).text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Inline HTML (Simple).xhtml b/test/javascripts/mdtest/fixtures/Inline HTML (Simple).xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Inline HTML comments.text b/test/javascripts/mdtest/fixtures/Inline HTML comments.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Inline HTML comments.xhtml b/test/javascripts/mdtest/fixtures/Inline HTML comments.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Links, inline style.text b/test/javascripts/mdtest/fixtures/Links, inline style.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Links, inline style.xhtml b/test/javascripts/mdtest/fixtures/Links, inline style.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Links, reference style.text b/test/javascripts/mdtest/fixtures/Links, reference style.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Links, reference style.xhtml b/test/javascripts/mdtest/fixtures/Links, reference style.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Links, shortcut references.text b/test/javascripts/mdtest/fixtures/Links, shortcut references.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Links, shortcut references.xhtml b/test/javascripts/mdtest/fixtures/Links, shortcut references.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Literal quotes in titles.text b/test/javascripts/mdtest/fixtures/Literal quotes in titles.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Literal quotes in titles.xhtml b/test/javascripts/mdtest/fixtures/Literal quotes in titles.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Markdown Documentation - Basics.text b/test/javascripts/mdtest/fixtures/Markdown Documentation - Basics.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Markdown Documentation - Basics.xhtml b/test/javascripts/mdtest/fixtures/Markdown Documentation - Basics.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Markdown Documentation - Syntax.text b/test/javascripts/mdtest/fixtures/Markdown Documentation - Syntax.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Markdown Documentation - Syntax.xhtml b/test/javascripts/mdtest/fixtures/Markdown Documentation - Syntax.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Nested blockquotes.text b/test/javascripts/mdtest/fixtures/Nested blockquotes.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Nested blockquotes.xhtml b/test/javascripts/mdtest/fixtures/Nested blockquotes.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Ordered and unordered lists.text b/test/javascripts/mdtest/fixtures/Ordered and unordered lists.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Ordered and unordered lists.xhtml b/test/javascripts/mdtest/fixtures/Ordered and unordered lists.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Strong and em together.text b/test/javascripts/mdtest/fixtures/Strong and em together.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Strong and em together.xhtml b/test/javascripts/mdtest/fixtures/Strong and em together.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Tabs.text b/test/javascripts/mdtest/fixtures/Tabs.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Tabs.xhtml b/test/javascripts/mdtest/fixtures/Tabs.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Tidyness.text b/test/javascripts/mdtest/fixtures/Tidyness.text old mode 100755 new mode 100644 diff --git a/test/javascripts/mdtest/fixtures/Tidyness.xhtml b/test/javascripts/mdtest/fixtures/Tidyness.xhtml old mode 100755 new mode 100644 diff --git a/test/javascripts/mixins/selected-posts-count-test.js.es6 b/test/javascripts/mixins/selected-posts-count-test.js.es6 index 830d9033ee..9533acdce5 100644 --- a/test/javascripts/mixins/selected-posts-count-test.js.es6 +++ b/test/javascripts/mixins/selected-posts-count-test.js.es6 @@ -1,9 +1,10 @@ -module("Discourse.SelectedPostsCount"); +module("SelectedPostsCount"); +import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import Topic from 'discourse/models/topic'; var buildTestObj = function(params) { - return Ember.Object.createWithMixins(Discourse.SelectedPostsCount, params || {}); + return Ember.Object.createWithMixins(SelectedPostsCount, params || {}); }; test("without selectedPosts", function () { diff --git a/test/javascripts/models/nav-item-test.js.es6 b/test/javascripts/models/nav-item-test.js.es6 index bb32d28422..1af1bb14ea 100644 --- a/test/javascripts/models/nav-item-test.js.es6 +++ b/test/javascripts/models/nav-item-test.js.es6 @@ -1,16 +1,10 @@ -var asianCategory = Discourse.Category.create({name: '确实是这样', id: 343434}); module("Discourse.NavItem", { setup: function() { Ember.run(function() { + const asianCategory = Discourse.Category.create({name: '确实是这样', id: 343434}); Discourse.Site.currentProp('categories').addObject(asianCategory); }); - }, - - teardown: function() { - Em.run(function() { - Discourse.Site.currentProp('categories').removeObject(asianCategory); - }); } }); diff --git a/test/javascripts/models/rest-model-test.js.es6 b/test/javascripts/models/rest-model-test.js.es6 index 182532b139..73a77e7203 100644 --- a/test/javascripts/models/rest-model-test.js.es6 +++ b/test/javascripts/models/rest-model-test.js.es6 @@ -19,7 +19,7 @@ test('munging', function() { test('update', function() { const store = createStore(); - store.find('widget', 123).then(function(widget) { + return store.find('widget', 123).then(function(widget) { equal(widget.get('name'), 'Trout Lure'); ok(!widget.get('isSaving')); @@ -36,7 +36,7 @@ test('updating simultaneously', function() { expect(2); const store = createStore(); - store.find('widget', 123).then(function(widget) { + return store.find('widget', 123).then(function(widget) { const firstPromise = widget.update({ name: 'new name' }); const secondPromise = widget.update({ name: 'new name' }); @@ -47,7 +47,6 @@ test('updating simultaneously', function() { secondPromise.catch(function() { ok(true, 'the second promise fails'); }); - }); }); @@ -62,7 +61,7 @@ test('save new', function() { const promise = widget.save({ name: 'Evil Widget' }); ok(widget.get('isSaving')); - promise.then(function() { + return promise.then(function() { ok(!widget.get('isSaving')); ok(widget.get('id'), 'it has an id'); ok(widget.get('name'), 'Evil Widget'); @@ -90,7 +89,7 @@ test('creating simultaneously', function() { test('destroyRecord', function() { const store = createStore(); - store.find('widget', 123).then(function(widget) { + return store.find('widget', 123).then(function(widget) { widget.destroyRecord().then(function(result) { ok(result); }); diff --git a/test/javascripts/models/result-set-test.js.es6 b/test/javascripts/models/result-set-test.js.es6 index 2df92f5bd5..c846fd8872 100644 --- a/test/javascripts/models/result-set-test.js.es6 +++ b/test/javascripts/models/result-set-test.js.es6 @@ -20,6 +20,7 @@ test('pagination support', function() { equal(rs.get('totalRows'), 4); ok(rs.get('loadMoreUrl'), 'has a url to load more'); ok(!rs.get('loadingMore'), 'it is not loading more'); + ok(rs.get('canLoadMore')); const promise = rs.loadMore(); @@ -28,6 +29,7 @@ test('pagination support', function() { ok(!rs.get('loadingMore'), 'it finished loading more'); equal(rs.get('length'), 4); ok(!rs.get('loadMoreUrl')); + ok(!rs.get('canLoadMore')); }); }); }); diff --git a/test/javascripts/models/store-test.js.es6 b/test/javascripts/models/store-test.js.es6 index 6411f141f4..f0dca5ad75 100644 --- a/test/javascripts/models/store-test.js.es6 +++ b/test/javascripts/models/store-test.js.es6 @@ -37,7 +37,7 @@ test('createRecord with a record as attributes returns that record from the map' test('find', function() { const store = createStore(); - store.find('widget', 123).then(function(w) { + return store.find('widget', 123).then(function(w) { equal(w.get('name'), 'Trout Lure'); equal(w.get('id'), 123); ok(!w.get('isNew'), 'found records are not new'); @@ -51,28 +51,28 @@ test('find', function() { test('find with object id', function() { const store = createStore(); - store.find('widget', {id: 123}).then(function(w) { + return store.find('widget', {id: 123}).then(function(w) { equal(w.get('firstObject.name'), 'Trout Lure'); }); }); test('find with query param', function() { const store = createStore(); - store.find('widget', {name: 'Trout Lure'}).then(function(w) { + return store.find('widget', {name: 'Trout Lure'}).then(function(w) { equal(w.get('firstObject.id'), 123); }); }); test('update', function() { const store = createStore(); - store.update('widget', 123, {name: 'hello'}).then(function(result) { + return store.update('widget', 123, {name: 'hello'}).then(function(result) { ok(result); }); }); test('findAll', function() { const store = createStore(); - store.findAll('widget').then(function(result) { + return store.findAll('widget').then(function(result) { equal(result.get('length'), 2); const w = result.findBy('id', 124); ok(!w.get('isNew'), 'found records are not new'); @@ -80,9 +80,9 @@ test('findAll', function() { }); }); -test('destroyRecord', function() { +test('destroyRecord', function(assert) { const store = createStore(); - store.find('widget', 123).then(function(w) { + return store.find('widget', 123).then(function(w) { store.destroyRecord('widget', w).then(function(result) { ok(result); }); @@ -91,7 +91,7 @@ test('destroyRecord', function() { test('find embedded', function() { const store = createStore(); - store.find('fruit', 1).then(function(f) { + return store.find('fruit', 1).then(function(f) { ok(f.get('farmer'), 'it has the embedded object'); ok(f.get('category'), 'categories are found automatically'); }); @@ -99,7 +99,7 @@ test('find embedded', function() { test('findAll embedded', function() { const store = createStore(); - store.findAll('fruit').then(function(fruits) { + return store.findAll('fruit').then(function(fruits) { equal(fruits.objectAt(0).get('farmer.name'), 'Old MacDonald'); equal(fruits.objectAt(0).get('farmer'), fruits.objectAt(1).get('farmer'), 'points at the same object'); equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker'); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 9935676b67..a968d47947 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -9,10 +9,10 @@ //= require ../../app/assets/javascripts/discourse/lib/probes // Externals we need to load first -//= require development/jquery-2.1.1 +//= require jquery.debug //= require jquery.ui.widget //= require handlebars -//= require development/ember +//= require ember.custom.debug //= require message-bus //= require ember-qunit //= require fake_xml_http_request @@ -76,9 +76,7 @@ d.write('