diff --git a/.image_optim.yml b/.image_optim.yml deleted file mode 100644 index 746b85dc3b..0000000000 --- a/.image_optim.yml +++ /dev/null @@ -1,12 +0,0 @@ -skip_missing_workers: true -allow_lossy: false -# PNG -advpng: false -optipng: - level: 2 -pngcrush: false -pngout: false -pngquant: false -# JPG -jpegrecompress: false -timeout: 15 diff --git a/.rubocop.yml b/.rubocop.yml index 792d6e22e1..cd2bcd9762 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,14 +1,108 @@ AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.4 + DisabledByDefault: true + Exclude: + - 'bundle/**/*' + - 'vendor/**/*' + - 'node_modules/**/*' -Metrics/LineLength: - Max: 120 +# Prefer &&/|| over and/or. +Style/AndOr: + Enabled: true -Metrics/MethodLength: +# Do not use braces for hash literals when they are the last argument of a +# method call. +Style/BracesAroundHashParameters: + Enabled: true + +# Align `when` with `case`. +Layout/CaseIndentation: + Enabled: true + +# Align comments with method definitions. +Layout/CommentIndentation: + Enabled: true + +# No extra empty lines. +Layout/EmptyLines: + Enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: true + +# Two spaces, no tabs (for indentation). +Layout/IndentationWidth: + Enabled: true + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +# Defining a method with parameters needs parentheses. +Style/MethodDefParentheses: + Enabled: true + +# Use `foo {}` not `foo{}`. +Layout/SpaceBeforeBlockBraces: + Enabled: true + +# Use `foo { bar }` not `foo {bar}`. +Layout/SpaceInsideBlockBraces: + Enabled: true + +# Use `{ a: 1 }` not `{a:1}`. +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: + Enabled: true + +# Detect hard tabs, no hard tabs. +Layout/Tab: + Enabled: true + +# Blank lines should not have any spaces. +Layout/TrailingBlankLines: + Enabled: true + +# No trailing whitespace. +Layout/TrailingWhitespace: + Enabled: true + +Lint/BlockAlignment: + Enabled: true + +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Lint/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + Enabled: true + +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented + +Layout/AlignHash: + Enabled: true + +Bundler/OrderedGems: Enabled: false - -Style/Documentation: - Enabled: false - -Style/FrozenStringLiteralComment: - Enabled: False diff --git a/.travis.yml b/.travis.yml index 5eaaec0902..dc66517428 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,7 @@ env: - RUBY_GC_MALLOC_LIMIT=50000000 matrix: - "RAILS_MASTER=0 QUNIT_RUN=0" - - "RAILS_MASTER=1 QUNIT_RUN=0" - "RAILS_MASTER=0 QUNIT_RUN=1" - - "RAILS_MASTER=1 QUNIT_RUN=1" addons: postgresql: 9.5 @@ -20,9 +18,6 @@ addons: - jhead matrix: - allow_failures: - - env: "RAILS_MASTER=1 QUNIT_RUN=0" - - env: "RAILS_MASTER=1 QUNIT_RUN=1" fast_finish: true rvm: @@ -41,7 +36,7 @@ cache: - vendor/bundle before_install: - - gem install bundler + - gem install bundler rubocop - git clone --depth=1 https://github.com/discourse/discourse-backup-uploads-to-s3.git plugins/discourse-backup-uploads-to-s3 - git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert - git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday @@ -53,6 +48,7 @@ before_install: - eslint --ext .es6 test/javascripts - eslint --ext .es6 plugins/**/assets/javascripts - eslint test/javascripts + - rubocop --parallel before_script: - bundle exec rake db:create db:migrate @@ -62,4 +58,4 @@ install: - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi" script: - - bash -c "if [ '$QUNIT_RUN' == '0' ]; then bundle exec rspec && bundle exec rake plugin:spec; else bundle exec rake qunit:test['200000']; fi" + - bash -c "if [ '$QUNIT_RUN' == '0' ]; then bundle exec rspec && bundle exec rake plugin:spec; else LOAD_PLUGINS=1 bundle exec rake qunit:test['300000']; fi" diff --git a/Gemfile b/Gemfile index 03dbe0a17b..ba6165e350 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,7 @@ gem 'aws-sdk', require: false gem 'excon', require: false gem 'unf', require: false -gem 'email_reply_trimmer', '0.1.6' +gem 'email_reply_trimmer', '0.1.7' # TODO Use official image_optim gem once https://github.com/toy/image_optim/pull/149 # is merged. @@ -122,7 +122,6 @@ group :test do gem 'webmock', require: false gem 'fakeweb', '~> 1.3.0', require: false gem 'minitest', require: false - gem 'timecop' # TODO: Remove once we upgrade to Rails 5. gem 'test_after_commit' end @@ -152,6 +151,7 @@ group :development do gem 'binding_of_caller' gem 'annotate' gem 'foreman', require: false + gem 'rubocop', require: false end # this is an optional gem, it provides a high performance replacement @@ -184,13 +184,12 @@ gem 'simple-rss', require: false gem 'stackprof', require: false, platform: :mri gem 'memory_profiler', require: false, platform: :mri -gem 'rmmseg-cpp', require: false +gem 'cppjieba_rb', require: false gem 'logster' gem 'sassc', require: false - if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index c84ad60a34..b15fe8ddc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,38 +1,38 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) + actionmailer (4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.8) - actionview (= 4.2.8) - activesupport (= 4.2.8) + actionpack (4.2.9) + actionview (= 4.2.9) + activesupport (= 4.2.9) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.8) - activesupport (= 4.2.8) + actionview (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.3) active_model_serializers (0.8.3) activemodel (>= 3.0) - activejob (4.2.8) - activesupport (= 4.2.8) + activejob (4.2.9) + activesupport (= 4.2.9) globalid (>= 0.3.0) - activemodel (4.2.8) - activesupport (= 4.2.8) + activemodel (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) - activerecord (4.2.8) - activemodel (= 4.2.8) - activesupport (= 4.2.8) + activerecord (4.2.9) + activemodel (= 4.2.9) + activesupport (= 4.2.9) arel (~> 6.0) - activesupport (4.2.8) + activesupport (4.2.9) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -71,6 +71,7 @@ GEM coderay (1.1.1) concurrent-ruby (1.0.5) connection_pool (2.2.1) + cppjieba_rb (0.3.0) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.2) @@ -84,7 +85,7 @@ GEM image_size (~> 1.5) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - email_reply_trimmer (0.1.6) + email_reply_trimmer (0.1.7) ember-data-source (2.2.1) ember-source (>= 1.8, < 3.0) ember-handlebars-template (0.7.5) @@ -127,7 +128,7 @@ GEM hiredis (0.6.1) htmlentities (4.3.4) http_accept_language (2.0.5) - i18n (0.8.4) + i18n (0.8.6) image_size (1.5.0) in_threads (1.4.0) jmespath (1.3.1) @@ -137,7 +138,7 @@ GEM thor (>= 0.14, < 2.0) jwt (1.5.6) kgio (2.11.0) - libv8 (5.3.332.38.5) + libv8 (5.9.211.38.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -153,12 +154,14 @@ GEM rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.99.3) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mini_mime (0.1.3) mini_portile2 (2.2.0) - mini_racer (0.1.9) - libv8 (~> 5.3) - minitest (5.10.2) + mini_racer (0.1.11) + libv8 (~> 5.7) + minitest (5.10.3) mocha (1.2.1) metaclass (~> 0.0.1) mock_redis (0.17.3) @@ -211,7 +214,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.14) + onebox (1.8.16) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -222,7 +225,11 @@ GEM openid-redis-store (0.0.2) redis ruby-openid + parallel (1.11.2) + parser (2.4.0.0) + ast (~> 2.2) pg (0.20.0) + powerpack (0.1.1) progress (3.3.1) pry (0.10.4) coderay (~> 1.1.0) @@ -245,16 +252,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.8) - actionmailer (= 4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) - activemodel (= 4.2.8) - activerecord (= 4.2.8) - activesupport (= 4.2.8) + rails (4.2.9) + actionmailer (= 4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) + activemodel (= 4.2.9) + activerecord (= 4.2.9) + activesupport (= 4.2.9) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.8) + railties (= 4.2.9) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -266,11 +273,13 @@ GEM loofah (~> 2.0) rails_multisite (1.0.6) rails (> 4.2, < 5) - railties (4.2.8) - actionpack (= 4.2.8) - activesupport (= 4.2.8) + railties (4.2.9) + actionpack (= 4.2.9) + activesupport (= 4.2.9) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) + rainbow (2.2.2) + rake raindrops (0.18.0) rake (12.0.0) rake-compiler (1.0.4) @@ -286,7 +295,6 @@ GEM redis-namespace (1.5.3) redis (~> 3.0, >= 3.0.4) rinku (2.0.2) - rmmseg-cpp (0.2.9) rspec (3.6.0) rspec-core (~> 3.6.0) rspec-expectations (~> 3.6.0) @@ -312,10 +320,18 @@ GEM rspec-support (~> 3.6.0) rspec-support (3.6.0) rtlit (0.0.5) + rubocop (0.49.1) + parallel (~> 1.10) + parser (>= 2.3.3.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) ruby-ll (2.1.2) ansi ast ruby-openid (2.7.0) + ruby-progressbar (1.8.1) ruby-readability (0.7.0) guess_html_encoding (>= 0.0.4) nokogiri (>= 1.6.0) @@ -363,7 +379,6 @@ GEM thor (0.19.4) thread_safe (0.3.6) tilt (2.0.7) - timecop (0.8.1) trollop (2.1.2) tzinfo (1.2.3) thread_safe (~> 0.1) @@ -372,6 +387,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.4) + unicode-display_width (1.3.0) unicorn (5.3.0) kgio (~> 2.6) raindrops (~> 0.7) @@ -395,9 +411,10 @@ DEPENDENCIES bullet byebug certified + cppjieba_rb discourse-qunit-rails discourse_image_optim - email_reply_trimmer (= 0.1.6) + email_reply_trimmer (= 0.1.7) ember-handlebars-template (= 0.7.5) ember-rails (= 0.18.5) ember-source @@ -459,11 +476,11 @@ DEPENDENCIES redis redis-namespace rinku - rmmseg-cpp rspec rspec-html-matchers rspec-rails rtlit + rubocop ruby-readability sanitize sassc @@ -476,11 +493,10 @@ DEPENDENCIES test_after_commit thor tilt - timecop uglifier unf unicorn webmock BUNDLED WITH - 1.15.1 + 1.15.3 diff --git a/Rakefile b/Rakefile index 179ba560eb..7a7f9ee44b 100755 --- a/Rakefile +++ b/Rakefile @@ -9,4 +9,3 @@ Discourse::Application.load_tasks # this prevents crashes when migrating a database in production in certain # PostgreSQL configuations when trying to create structure.sql Rake::Task["db:structure:dump"].clear if Rails.env.production? - diff --git a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 b/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 index e4d19613b5..8d43474a9d 100644 --- a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 +++ b/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 @@ -1,4 +1,4 @@ -import { iconHTML } from 'discourse-common/helpers/fa-icon'; +import { iconHTML } from 'discourse-common/lib/icon-library'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; export default Ember.Component.extend(bufferedRender({ diff --git a/app/assets/javascripts/admin/components/admin-watched-word.js.es6 b/app/assets/javascripts/admin/components/admin-watched-word.js.es6 new file mode 100644 index 0000000000..3c78becb49 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-watched-word.js.es6 @@ -0,0 +1,19 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; +import { bufferedRender } from 'discourse-common/lib/buffered-render'; + +export default Ember.Component.extend(bufferedRender({ + classNames: ['watched-word'], + + buildBuffer(buffer) { + buffer.push(iconHTML('times')); + buffer.push(' ' + this.get('word.word')); + }, + + click() { + this.get('word').destroy().then(() => { + this.sendAction('action', this.get('word')); + }).catch(e => { + bootbox.alert(I18n.t("generic_error_with_reason", {error: `http: ${e.status} - ${e.body}`})); + });; + } +})); diff --git a/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 index d176b5a40a..c5f1a4a244 100644 --- a/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 +++ b/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 @@ -1,5 +1,5 @@ import computed from 'ember-addons/ember-computed-decorators'; -import { iconHTML } from 'discourse-common/helpers/fa-icon'; +import { iconHTML } from 'discourse-common/lib/icon-library'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; export default Ember.Component.extend(bufferedRender({ diff --git a/app/assets/javascripts/admin/components/resumable-upload.js.es6 b/app/assets/javascripts/admin/components/resumable-upload.js.es6 index 73a46eded3..36f420d100 100644 --- a/app/assets/javascripts/admin/components/resumable-upload.js.es6 +++ b/app/assets/javascripts/admin/components/resumable-upload.js.es6 @@ -1,3 +1,4 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; /*global Resumable:true */ @@ -40,7 +41,7 @@ export default Ember.Component.extend(bufferedRender({ buildBuffer(buffer) { const icon = this.get("isUploading") ? "times" : "upload"; - buffer.push(``); + buffer.push(iconHTML(icon)); buffer.push("" + this.get("text") + ""); buffer.push(""); }, diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6 new file mode 100644 index 0000000000..95f859abf4 --- /dev/null +++ b/app/assets/javascripts/admin/components/watched-word-form.js.es6 @@ -0,0 +1,49 @@ +import WatchedWord from 'admin/models/watched-word'; +import { on, observes } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNames: ['watched-word-form'], + formSubmitted: false, + actionKey: null, + showSuccessMessage: false, + + @observes('word') + removeSuccessMessage() { + if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) { + this.set('showSuccessMessage', false); + } + }, + + actions: { + submit() { + if (!this.get('formSubmitted')) { + this.set('formSubmitted', true); + + const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') }); + + watchedWord.save().then(result => { + this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true }); + this.sendAction('action', WatchedWord.create(result)); + Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus()); + }).catch(e => { + this.set('formSubmitted', false); + const msg = (e.responseJSON && e.responseJSON.errors) ? + I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) : + I18n.t("generic_error"); + bootbox.alert(msg, () => this.$('.watched-word-input').focus()); + }); + } + } + }, + + @on("didInsertElement") + _init() { + Ember.run.schedule('afterRender', () => { + this.$('.watched-word-input').keydown(e => { + if (e.keyCode === 13) { + this.send('submit'); + } + }); + }); + } +}); diff --git a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 new file mode 100644 index 0000000000..8d9e6ba0a9 --- /dev/null +++ b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 @@ -0,0 +1,25 @@ +import computed from "ember-addons/ember-computed-decorators"; +import UploadMixin from "discourse/mixins/upload"; + +export default Em.Component.extend(UploadMixin, { + type: 'csv', + classNames: 'watched-words-uploader', + uploadUrl: '/admin/logs/watched_words/upload', + addDisabled: Em.computed.alias("uploading"), + + validateUploadedFilesOptions() { + return { csvOnly: true }; + }, + + @computed('actionKey') + data(actionKey) { + return { action_key: actionKey }; + }, + + uploadDone() { + if (this) { + bootbox.alert(I18n.t("admin.watched_words.form.upload_successful")); + this.sendAction("done"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 index 52dbc07791..cab20f908f 100644 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-group.js.es6 @@ -31,10 +31,10 @@ export default Ember.Controller.extend({ ]; }.property(), - @computed('model.visibility_level', 'model.public') - disableMembershipRequestSetting(visibility_level, publicGroup) { + @computed('model.visibility_level', 'model.public_admission') + disableMembershipRequestSetting(visibility_level, publicAdmission) { visibility_level = parseInt(visibility_level); - return (visibility_level !== 0) || publicGroup; + return (visibility_level !== 0) || publicAdmission; }, @computed('model.visibility_level', 'model.allow_membership_requests') diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 new file mode 100644 index 0000000000..bd11f15fcf --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 @@ -0,0 +1,65 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import WatchedWord from 'admin/models/watched-word'; + +export default Ember.Controller.extend({ + actionNameKey: null, + adminWatchedWords: Ember.inject.controller(), + showWordsList: Ember.computed.or('adminWatchedWords.filtered', 'adminWatchedWords.showWords'), + + findAction(actionName) { + return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName); + }, + + @computed('adminWatchedWords.model', 'actionNameKey') + filteredContent() { + if (!this.get('actionNameKey')) { return []; } + + const a = this.findAction(this.get('actionNameKey')); + return a ? a.words : []; + }, + + @computed('actionNameKey') + actionDescription(actionNameKey) { + return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey); + }, + + actions: { + recordAdded(arg) { + const a = this.findAction(this.get('actionNameKey')); + if (a) { + a.words.unshiftObject(arg); + a.incrementProperty('count'); + Em.run.schedule('afterRender', () => { + // remove from other actions lists + let match = null; + this.get('adminWatchedWords.model').forEach(action => { + if (match) return; + + if (action.nameKey !== this.get('actionNameKey')) { + match = action.words.findBy('id', arg.id); + if (match) { + action.words.removeObject(match); + action.decrementProperty('count'); + } + } + }); + }); + } + }, + + recordRemoved(arg) { + const a = this.findAction(this.get('actionNameKey')); + if (a) { + a.words.removeObject(arg); + a.decrementProperty('count'); + } + }, + + uploadComplete() { + WatchedWord.findAll().then(data => { + this.set('adminWatchedWords.model', data); + }); + } + } + +}); diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 new file mode 100644 index 0000000000..07d53fd230 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 @@ -0,0 +1,51 @@ +import debounce from 'discourse/lib/debounce'; + +export default Ember.Controller.extend({ + filter: null, + filtered: false, + showWords: false, + disableShowWords: Ember.computed.alias('filtered'), + + filterContentNow() { + + if (!!Ember.isEmpty(this.get('allWatchedWords'))) return; + + let filter; + if (this.get('filter')) { + filter = this.get('filter').toLowerCase(); + } + + if (filter === undefined || filter.length < 1) { + this.set('model', this.get('allWatchedWords')); + return; + } + + const matchesByAction = []; + + this.get('allWatchedWords').forEach(wordsForAction => { + const wordRecords = wordsForAction.words.filter(wordRecord => { + return (wordRecord.word.indexOf(filter) > -1); + }); + matchesByAction.pushObject( Ember.Object.create({ + nameKey: wordsForAction.nameKey, + name: wordsForAction.name, + words: wordRecords, + count: wordRecords.length + }) ); + }); + + this.set('model', matchesByAction); + }, + + filterContent: debounce(function() { + this.filterContentNow(); + this.set('filtered', !Ember.isEmpty(this.get('filter'))); + }, 250).observes('filter'), + + actions: { + clearFilter() { + this.setProperties({ filter: '' }); + } + } + +}); diff --git a/app/assets/javascripts/admin/helpers/check-icon.js.es6 b/app/assets/javascripts/admin/helpers/check-icon.js.es6 new file mode 100644 index 0000000000..0a68c0be0b --- /dev/null +++ b/app/assets/javascripts/admin/helpers/check-icon.js.es6 @@ -0,0 +1,7 @@ +import { registerUnbound } from 'discourse-common/lib/helpers'; +import { renderIcon } from 'discourse-common/lib/icon-library'; + +registerUnbound('check-icon', function(value) { + let icon = value ? "check" : "times"; + return new Handlebars.SafeString(renderIcon('string', icon)); +}); diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 0720976e69..662f0b046c 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -1,3 +1,4 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; import { propertyNotEqual } from 'discourse/lib/computed'; @@ -108,7 +109,7 @@ const AdminUser = Discourse.User.extend({ "class": "cancel-inline", "link": true }, { - "label": ' ' + I18n.t("admin.user.delete_all_posts"), + "label": `${iconHTML('exclamation-triangle')} ` + I18n.t("admin.user.delete_all_posts"), "class": "btn btn-danger", "callback": function() { ajax("/admin/users/" + user.get('id') + "/delete_all_posts", { @@ -337,7 +338,7 @@ const AdminUser = Discourse.User.extend({ "class": "cancel", "link": true }, { - "label": '' + I18n.t('admin.user.block_accept'), + "label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.block_accept'), "class": "btn btn-danger", "callback": function() { performBlock(); } }]; @@ -386,7 +387,7 @@ const AdminUser = Discourse.User.extend({ "class": "cancel", "link": true }, { - "label": '' + I18n.t('admin.user.anonymize_yes'), + "label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.anonymize_yes'), "class": "btn btn-danger", "callback": function() { performAnonymize(); } }]; @@ -450,7 +451,7 @@ const AdminUser = Discourse.User.extend({ "class": "btn", "link": true }, { - "label": '' + I18n.t('admin.user.delete_and_block'), + "label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.delete_and_block'), "class": "btn btn-danger", "callback": function(){ performDestroy(true); } }, { @@ -479,7 +480,7 @@ const AdminUser = Discourse.User.extend({ "class": "cancel-inline", "link": true }, { - "label": ' ' + I18n.t("flagging.yes_delete_spammer"), + "label": `${iconHTML('exclamation-triangle')} ` + I18n.t("flagging.yes_delete_spammer"), "class": "btn btn-danger", "callback": function() { return ajax("/admin/users/" + user.get('id') + '.json', { diff --git a/app/assets/javascripts/admin/models/flagged-post.js.es6 b/app/assets/javascripts/admin/models/flagged-post.js.es6 index c7a654608f..7a627f1569 100644 --- a/app/assets/javascripts/admin/models/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/models/flagged-post.js.es6 @@ -2,7 +2,7 @@ import { ajax } from 'discourse/lib/ajax'; import AdminUser from 'admin/models/admin-user'; import Topic from 'discourse/models/topic'; import Post from 'discourse/models/post'; - +import { iconHTML } from 'discourse-common/lib/icon-library'; const FlaggedPost = Post.extend({ @@ -35,13 +35,14 @@ const FlaggedPost = Post.extend({ dispositionIcon: function (disposition) { if (!disposition) { return null; } - var icon, title = I18n.t('admin.flags.dispositions.' + disposition); + let icon; + let title = 'admin.flags.dispositions.' + disposition; switch (disposition) { - case "deferred": { icon = "fa-external-link"; break; } - case "agreed": { icon = "fa-thumbs-o-up"; break; } - case "disagreed": { icon = "fa-thumbs-o-down"; break; } + case "deferred": { icon = "external-link"; break; } + case "agreed": { icon = "thumbs-o-up"; break; } + case "disagreed": { icon = "thumbs-o-down"; break; } } - return ""; + return iconHTML(icon, { title }); }, wasEdited: function () { diff --git a/app/assets/javascripts/admin/models/watched-word.js.es6 b/app/assets/javascripts/admin/models/watched-word.js.es6 new file mode 100644 index 0000000000..c0418aef97 --- /dev/null +++ b/app/assets/javascripts/admin/models/watched-word.js.es6 @@ -0,0 +1,37 @@ +import { ajax } from 'discourse/lib/ajax'; + +const WatchedWord = Discourse.Model.extend({ + save() { + return ajax("/admin/logs/watched_words" + (this.id ? '/' + this.id : '') + ".json", { + type: this.id ? 'PUT' : 'POST', + data: {word: this.get('word'), action_key: this.get('action')}, + dataType: 'json' + }); + }, + + destroy() { + return ajax("/admin/logs/watched_words/" + this.get('id') + ".json", {type: 'DELETE'}); + } +}); + +WatchedWord.reopenClass({ + findAll() { + return ajax("/admin/logs/watched_words").then(function (list) { + const actions = {}; + list.words.forEach(s => { + if (!actions[s.action]) { actions[s.action] = []; } + actions[s.action].pushObject(WatchedWord.create(s)); + }); + + list.actions.forEach(a => { + if (!actions[a]) { actions[a] = []; } + }); + + return Object.keys(actions).map(function(n) { + return Ember.Object.create({nameKey: n, name: I18n.t('admin.watched_words.actions.' + n), words: actions[n], count: actions[n].length}); + }); + }); + } +}); + +export default WatchedWord; diff --git a/app/assets/javascripts/admin/models/web-hook.js.es6 b/app/assets/javascripts/admin/models/web-hook.js.es6 index 325d931045..27d72878b0 100644 --- a/app/assets/javascripts/admin/models/web-hook.js.es6 +++ b/app/assets/javascripts/admin/models/web-hook.js.es6 @@ -37,7 +37,7 @@ export default RestModel.extend({ }, groupFinder(term) { - return Group.findAll({search: term, ignore_automatic: false}); + return Group.findAll({ term: term, ignore_automatic: false }); }, @computed('wildcard_web_hook', 'web_hook_event_types.[]') @@ -82,4 +82,3 @@ export default RestModel.extend({ return this.createProperties(); } }); - diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index dd87207156..34e55914ad 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -62,6 +62,10 @@ export default function() { this.route('screenedEmails', { path: '/screened_emails' }); this.route('screenedIpAddresses', { path: '/screened_ip_addresses' }); this.route('screenedUrls', { path: '/screened_urls' }); + this.route('adminWatchedWords', { path: '/watched_words', resetNamespace: true}, function() { + this.route('index', { path: '/' } ); + this.route('action', { path: '/action/:action_id' } ); + }); }); this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() { diff --git a/app/assets/javascripts/admin/routes/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words-action.js.es6 new file mode 100644 index 0000000000..123884d6fc --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-watched-words-action.js.es6 @@ -0,0 +1,11 @@ +export default Discourse.Route.extend({ + model(params) { + this.controllerFor('adminWatchedWordsAction').set('actionNameKey', params.action_id); + let filteredContent = this.controllerFor('adminWatchedWordsAction').get('filteredContent'); + return Ember.Object.create({ + nameKey: params.action_id, + name: I18n.t('admin.watched_words.actions.' + params.action_id), + words: filteredContent + }); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-watched-words-index.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words-index.js.es6 new file mode 100644 index 0000000000..0103744b52 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-watched-words-index.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + beforeModel() { + this.replaceWith('adminWatchedWords.action', this.modelFor('adminWatchedWords')[0].nameKey); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 new file mode 100644 index 0000000000..77f9082b57 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 @@ -0,0 +1,15 @@ +import WatchedWord from 'admin/models/watched-word'; + +export default Discourse.Route.extend({ + queryParams: { + filter: { replace: true } + }, + + model() { + return WatchedWord.findAll(); + }, + + afterModel(watchedWordsList) { + this.controllerFor('adminWatchedWords').set('allWatchedWords', watchedWordsList); + } +}); diff --git a/app/assets/javascripts/admin/templates/api-keys.hbs b/app/assets/javascripts/admin/templates/api-keys.hbs index 60449052bb..b25eebd69a 100644 --- a/app/assets/javascripts/admin/templates/api-keys.hbs +++ b/app/assets/javascripts/admin/templates/api-keys.hbs @@ -29,5 +29,5 @@ {{/if}} {{#unless hasMasterKey}} - + {{/unless}} diff --git a/app/assets/javascripts/admin/templates/badges-index.hbs b/app/assets/javascripts/admin/templates/badges-index.hbs index c94f59fe10..674eae8de2 100644 --- a/app/assets/javascripts/admin/templates/badges-index.hbs +++ b/app/assets/javascripts/admin/templates/badges-index.hbs @@ -3,7 +3,7 @@
{{#link-to 'adminBadges.show' 'new' class="btn"}} - {{fa-icon "plus"}} {{i18n 'admin.badges.new'}} + {{d-icon "plus"}} {{i18n 'admin.badges.new'}} {{/link-to}}
{{/d-section}} diff --git a/app/assets/javascripts/admin/templates/badges-show.hbs b/app/assets/javascripts/admin/templates/badges-show.hbs index e0ae7001df..e40e6bfec0 100644 --- a/app/assets/javascripts/admin/templates/badges-show.hbs +++ b/app/assets/javascripts/admin/templates/badges-show.hbs @@ -38,7 +38,7 @@ content=badgeGroupings optionValuePath="content.id" optionLabelPath="content.displayName"}} -   +   diff --git a/app/assets/javascripts/admin/templates/badges.hbs b/app/assets/javascripts/admin/templates/badges.hbs index 3ca7f91294..aeeaac9c9e 100644 --- a/app/assets/javascripts/admin/templates/badges.hbs +++ b/app/assets/javascripts/admin/templates/badges.hbs @@ -15,7 +15,7 @@ {{/each}} {{#link-to 'adminBadges.show' 'new' class="btn"}} - {{fa-icon "plus"}} {{i18n 'admin.badges.new'}} + {{d-icon "plus"}} {{i18n 'admin.badges.new'}} {{/link-to}}

diff --git a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs index 304570c0a5..82fbf0d799 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs @@ -1,6 +1,6 @@ {{#if report.icon}} - {{fa-icon report.icon}} + {{d-icon report.icon}} {{/if}} {{report.title}} @@ -8,15 +8,15 @@ {{number report.todayCount}} - {{number report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} + {{number report.yesterdayCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}} - {{number report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} + {{number report.lastSevenDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}} - {{number report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}} + {{number report.lastThirtyDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}} {{number report.total}} diff --git a/app/assets/javascripts/admin/templates/components/setting-validation-message.hbs b/app/assets/javascripts/admin/templates/components/setting-validation-message.hbs index 92451dbbf9..88d59816fe 100644 --- a/app/assets/javascripts/admin/templates/components/setting-validation-message.hbs +++ b/app/assets/javascripts/admin/templates/components/setting-validation-message.hbs @@ -1,4 +1,4 @@
- {{fa-icon "times"}} + {{d-icon "times"}} {{message}}
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/string.hbs b/app/assets/javascripts/admin/templates/components/site-settings/string.hbs index 71d7216f27..77e254b42f 100644 --- a/app/assets/javascripts/admin/templates/components/site-settings/string.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/string.hbs @@ -1,3 +1,8 @@ -{{text-field value=value classNames="input-setting-string"}} +{{#if setting.textarea}} + {{textarea value=value classNames="input-setting-textarea"}} +{{else}} + {{text-field value=value classNames="input-setting-string"}} +{{/if}} + {{setting-validation-message message=validationMessage}}
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs new file mode 100644 index 0000000000..64fbcb6570 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs @@ -0,0 +1,7 @@ +{{i18n 'admin.watched_words.form.label'}} +{{text-field value=word disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off"}} +{{d-button action="submit" disabled=formSubmitted label="admin.watched_words.form.add"}} + +{{#if showSuccessMessage}} + {{i18n 'admin.watched_words.form.success'}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs new file mode 100644 index 0000000000..caa74a8615 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs @@ -0,0 +1,7 @@ + +
+One word per line diff --git a/app/assets/javascripts/admin/templates/customize-colors-show.hbs b/app/assets/javascripts/admin/templates/customize-colors-show.hbs index 6b3c8a3e91..e688e13316 100644 --- a/app/assets/javascripts/admin/templates/customize-colors-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors-show.hbs @@ -5,13 +5,13 @@ {{#unless model.theme_id}} {{/unless}} - - + + {{#if model.theme_id}} {{i18n "admin.customize.theme_owner"}} {{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}} {{else}} - + {{/if}} {{model.savingStatus}} diff --git a/app/assets/javascripts/admin/templates/customize-colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs index 1ed39b8fcf..920f6f1954 100644 --- a/app/assets/javascripts/admin/templates/customize-colors.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors.hbs @@ -4,12 +4,12 @@ {{#each model as |scheme|}} {{#unless scheme.is_base}}
  • - {{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{fa-icon 'paint-brush'}}{{scheme.description}}{{/link-to}} + {{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{d-icon 'paint-brush'}}{{scheme.description}}{{/link-to}}
  • {{/unless}} {{/each}} - + {{outlet}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index fafce666a6..83eebb83d0 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -19,7 +19,7 @@
  • {{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true title=field.title}} {{i18n 'admin.customize.theme.desktop'}} - {{fa-icon 'desktop'}} + {{d-icon 'desktop'}} {{/link-to}}
  • {{/if}} @@ -27,7 +27,7 @@
  • {{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}} {{i18n 'admin.customize.theme.mobile'}} - {{fa-icon 'mobile'}} + {{d-icon 'mobile'}} {{/link-to}}
  • {{/if}} @@ -46,14 +46,14 @@ {{#each fields as |field|}}
  • {{#link-to 'adminCustomizeThemes.edit' model.id currentTargetName field.name replace=true title=field.title}} - {{#if field.icon}}{{fa-icon field.icon}} {{/if}} + {{#if field.icon}}{{d-icon field.icon}} {{/if}} {{i18n field.key}} {{/link-to}}
  • {{/each}}
  • - + {{d-icon maximizeIcon}}
  • diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 6e89af4767..638d7f24d4 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -5,7 +5,7 @@ {{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}} {{d-button action="cancelEditingName" class="btn-small cancel-edit" icon="times"}} {{else}} - {{model.name}} {{fa-icon "pencil"}} + {{model.name}} {{d-icon "pencil"}} {{/if}} @@ -15,7 +15,7 @@

    {{#if model.remote_theme.license_url}}

    - {{i18n "admin.customize.theme.license"}} {{fa-icon "copyright"}} + {{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}}

    {{/if}} {{/if}} @@ -133,8 +133,8 @@ {{/if}} {{/if}} - {{fa-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} - {{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}} + {{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} + {{d-icon "download"}} {{i18n 'admin.export_json.button_text'}} {{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}} diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index 43129006a5..c9474917ef 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -7,10 +7,10 @@ {{#link-to 'adminCustomizeThemes.show' theme replace=true}} {{theme.name}} {{#if theme.user_selectable}} - {{fa-icon "user"}} + {{d-icon "user"}} {{/if}} {{#if theme.default}} - {{fa-icon "asterisk"}} + {{d-icon "asterisk"}} {{/if}} {{/link-to}} diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs index c6c4487009..b8c0297d48 100644 --- a/app/assets/javascripts/admin/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/templates/dashboard.hbs @@ -29,15 +29,15 @@
    - + - + - + - +
    {{fa-icon "shield"}} {{i18n 'admin.dashboard.admins'}}{{d-icon "shield"}} {{i18n 'admin.dashboard.admins'}} {{#link-to 'adminUsersList.show' 'admins'}}{{admins}}{{/link-to}}{{fa-icon "ban"}} {{i18n 'admin.dashboard.suspended'}}{{d-icon "ban"}} {{i18n 'admin.dashboard.suspended'}} {{#link-to 'adminUsersList.show' 'suspended'}}{{suspended}}{{/link-to}}
    {{fa-icon "shield"}} {{i18n 'admin.dashboard.moderators'}}{{d-icon "shield"}} {{i18n 'admin.dashboard.moderators'}} {{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}}{{fa-icon "ban"}} {{i18n 'admin.dashboard.blocked'}}{{d-icon "ban"}} {{i18n 'admin.dashboard.blocked'}} {{#link-to 'adminUsersList.show' 'blocked'}}{{blocked}}{{/link-to}}
    @@ -87,7 +87,7 @@ - + @@ -176,7 +176,7 @@
    {{#if foundProblems}}
    -
    {{fa-icon "exclamation-triangle"}}
    +
    {{d-icon "exclamation-triangle"}}
    {{#conditional-loading-spinner condition=loadingProblems}}

    diff --git a/app/assets/javascripts/admin/templates/email-sent.hbs b/app/assets/javascripts/admin/templates/email-sent.hbs index 847076f098..9dd7996ff5 100644 --- a/app/assets/javascripts/admin/templates/email-sent.hbs +++ b/app/assets/javascripts/admin/templates/email-sent.hbs @@ -30,7 +30,7 @@ {{/if}}

    diff --git a/app/assets/javascripts/admin/templates/emojis.hbs b/app/assets/javascripts/admin/templates/emojis.hbs index 6f35c097a9..675e8249be 100644 --- a/app/assets/javascripts/admin/templates/emojis.hbs +++ b/app/assets/javascripts/admin/templates/emojis.hbs @@ -20,7 +20,7 @@ - + {{/each}} diff --git a/app/assets/javascripts/admin/templates/flags-list.hbs b/app/assets/javascripts/admin/templates/flags-list.hbs index 8796f31ff3..0c26c8254b 100644 --- a/app/assets/javascripts/admin/templates/flags-list.hbs +++ b/app/assets/javascripts/admin/templates/flags-list.hbs @@ -19,7 +19,9 @@ {{#if flaggedPost.postAuthorFlagged}} {{#if flaggedPost.user}} {{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="large"}}{{/link-to}} - {{#if flaggedPost.wasEdited}}{{/if}} + {{#if flaggedPost.wasEdited}} + {{d-icon "pencil" title="admin.flags.was_edited"}} + {{/if}} {{/if}} {{/if}} {{#if adminActiveFlagsView}} @@ -31,7 +33,7 @@

    {{#if flaggedPost.topic.isPrivateMessage}} - {{fa-icon "envelope"}} + {{d-icon "envelope"}} {{/if}} {{topic-status topic=flaggedPost.topic}} {{{unbound flaggedPost.topic.fancyTitle}}} @@ -90,7 +92,7 @@ {{format-age flagger.disposedAt}} {{{flagger.dispositionIcon}}} {{#if flagger.tookAction}} - + {{d-icon "gavel" title="admin.flags.took_action"}} {{/if}} @@ -129,7 +131,8 @@

    {{/if}} - + + {{/if}}

    @@ -141,14 +144,14 @@ diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index d1e8f3df18..1dd0a55cd1 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -9,19 +9,18 @@ {{/if}} - {{#if model.id}} - {{#unless model.automatic}} -
    - - {{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}} -
    + {{#unless model.automatic}} +
    + + {{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}} +
    -
    - - {{d-editor value=model.bio_raw}} -
    +
    + + {{d-editor value=model.bio_raw}} +
    - {{#if model.hasOwners}} + {{#if model.hasOwners}}
    @@ -30,31 +29,55 @@ {{/each}}
    - {{/if}} -
    - - {{user-selector usernames=model.ownerUsernames placeholderKey="admin.groups.selector_placeholder" id="owner-selector"}} - {{d-button action="addOwners" class="add" icon="plus" label="admin.groups.add"}} -
    - {{/unless}} + {{/if}} +
    - {{group-members-input model=model}} + + + {{user-selector usernames=model.ownerUsernames + placeholderKey="admin.groups.selector_placeholder" + id="owner-selector"}} + + {{#if model.id}} + {{d-button + action="addOwners" + class="add" + icon="plus" + label="admin.groups.add"}} + {{/if}}
    - {{/if}} + {{/unless}} + +
    + {{group-members-input model=model addButton=model.id}} +
    - {{combo-box name="alias" valueAttribute="value" value=model.visibility_level content=visibilityLevelOptions}} + {{combo-box name="alias" + valueAttribute="value" + value=model.visibility_level + content=visibilityLevelOptions + castInteger=true}}
    {{#unless model.automatic}}
    +
    + +
    +
    @@ -123,7 +146,7 @@
    {{#unless model.automatic}} - + {{/unless}} {{savingStatus}} diff --git a/app/assets/javascripts/admin/templates/groups-type-index.hbs b/app/assets/javascripts/admin/templates/groups-type-index.hbs index 88d870c1f6..196ded96d2 100644 --- a/app/assets/javascripts/admin/templates/groups-type-index.hbs +++ b/app/assets/javascripts/admin/templates/groups-type-index.hbs @@ -3,7 +3,7 @@
    {{#link-to 'adminGroup' 'new' class="btn"}} - {{fa-icon "plus"}} {{i18n 'admin.groups.new'}} + {{d-icon "plus"}} {{i18n 'admin.groups.new'}} {{/link-to}}
    diff --git a/app/assets/javascripts/admin/templates/groups-type.hbs b/app/assets/javascripts/admin/templates/groups-type.hbs index b4d2a0acf3..a1fa829897 100644 --- a/app/assets/javascripts/admin/templates/groups-type.hbs +++ b/app/assets/javascripts/admin/templates/groups-type.hbs @@ -18,7 +18,7 @@ {{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}} {{else}} {{#link-to 'adminGroup' 'new' class="btn"}} - {{fa-icon "plus"}} {{i18n 'admin.groups.new'}} + {{d-icon "plus"}} {{i18n 'admin.groups.new'}} {{/link-to}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/logs.hbs b/app/assets/javascripts/admin/templates/logs.hbs index 5c412949fa..d72e5a9262 100644 --- a/app/assets/javascripts/admin/templates/logs.hbs +++ b/app/assets/javascripts/admin/templates/logs.hbs @@ -3,6 +3,7 @@ {{nav-item route='adminLogs.screenedEmails' label='admin.logs.screened_emails.title'}} {{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}} {{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}} + {{nav-item route='adminWatchedWords' label='admin.watched_words.title'}} {{#if currentUser.admin}} {{nav-item path='/logs' label='admin.logs.logster.title'}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/logs/screened-emails.hbs b/app/assets/javascripts/admin/templates/logs/screened-emails.hbs index 685d8455c3..f99ff865e1 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-emails.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-emails.hbs @@ -1,6 +1,6 @@

    {{i18n 'admin.logs.screened_emails.description'}} - +


    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 ca6c216f50..00e67a9aa5 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs @@ -41,9 +41,9 @@
    {{#if item.isBlocked}} - {{fa-icon "ban"}} + {{d-icon "ban"}} {{else}} - {{fa-icon "check"}} + {{d-icon "check"}} {{/if}} {{item.actionName}}
    diff --git a/app/assets/javascripts/admin/templates/logs/screened-urls.hbs b/app/assets/javascripts/admin/templates/logs/screened-urls.hbs index 40aaceba70..7b1048f1b4 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-urls.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-urls.hbs @@ -1,6 +1,6 @@

    {{i18n 'admin.logs.screened_urls.description'}} - +


    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 f469ec266e..50f02f9f5b 100644 --- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs @@ -7,25 +7,25 @@ {{#if actionFilter}} {{i18n 'admin.logs.action'}}: {{actionFilter}} - {{fa-icon "times-circle"}} + {{d-icon "times-circle"}} {{/if}} {{#if filters.acting_user}} {{i18n 'admin.logs.staff_actions.staff_user'}}: {{filters.acting_user}} - {{fa-icon "times-circle"}} + {{d-icon "times-circle"}} {{/if}} {{#if filters.target_user}} {{i18n 'admin.logs.staff_actions.target_user'}}: {{filters.target_user}} - {{fa-icon "times-circle"}} + {{d-icon "times-circle"}} {{/if}} {{#if filters.subject}} {{i18n 'admin.logs.staff_actions.subject'}}: {{filters.subject}} - {{fa-icon "times-circle"}} + {{d-icon "times-circle"}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs index 6f845b22c8..63ce16601b 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs @@ -1,13 +1,13 @@ {{#d-modal-body title="admin.flags.agree_flag_modal_title"}} {{#if model.user_deleted}} - + {{else}} {{#unless model.postHidden}} - + {{/unless}} {{/if}} - + {{#if model.canDeleteAsSpammer}} - + {{/if}} {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs b/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs index c22bd9d99d..70b0c1c3fc 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-badge-preview.hbs @@ -24,7 +24,7 @@ {{#if count_warning}}
    -

    {{i18n 'admin.badges.preview.bad_count_warning.header'}}

    +

    {{d-icon "warning"}} {{i18n 'admin.badges.preview.bad_count_warning.header'}}

    {{i18n 'admin.badges.preview.bad_count_warning.text'}}

    {{/if}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs b/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs index 5286bbf0b0..d58a63c134 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs @@ -7,6 +7,6 @@ valueAttribute="base_scheme_id"}} {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs index c2abeae36d..89cf7452a4 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs @@ -1,7 +1,7 @@ {{#d-modal-body title="admin.flags.delete_flag_modal_title"}} - - + + {{#if model.canDeleteAsSpammer}} - + {{/if}} {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs b/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs index 80eb0844b3..f37c1f414d 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-edit-badge-groupings.hbs @@ -5,15 +5,15 @@
  • {{#if wc.editing}} {{input value=wc.name}} - + {{else}} {{wc.displayName}} {{/if}}
    - - - - + + + +
  • {{/each}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs index 9d9326ad3e..afb7525fb5 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs @@ -11,6 +11,6 @@ {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/site-text-edit.hbs b/app/assets/javascripts/admin/templates/site-text-edit.hbs index 5e02eb0731..4e134433d8 100644 --- a/app/assets/javascripts/admin/templates/site-text-edit.hbs +++ b/app/assets/javascripts/admin/templates/site-text-edit.hbs @@ -13,7 +13,7 @@ {{/save-controls}} {{#link-to 'adminSiteText.index' class="go-back"}} - {{fa-icon 'arrow-left'}} + {{d-icon 'arrow-left'}} {{i18n 'admin.site_text.go_back'}} {{/link-to}} diff --git a/app/assets/javascripts/admin/templates/user-badges.hbs b/app/assets/javascripts/admin/templates/user-badges.hbs index 393695e842..9977aca39d 100644 --- a/app/assets/javascripts/admin/templates/user-badges.hbs +++ b/app/assets/javascripts/admin/templates/user-badges.hbs @@ -1,7 +1,7 @@
    diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index f6a7a2d75e..7a016f150f 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -3,7 +3,7 @@
    {{#if model.canViewProfile}} {{#link-to 'user' model class="btn"}} - {{fa-icon "user"}} + {{d-icon "user"}} {{i18n 'admin.user.show_public_profile'}} {{/link-to}} {{/if}} @@ -151,7 +151,7 @@ {{i18n 'badges.badge_count' count=model.badge_count}}
    - {{#link-to 'adminUser.badges' model class="btn"}}{{fa-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}} + {{#link-to 'adminUser.badges' model class="btn"}}{{d-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}}
    {{/if}} @@ -281,9 +281,11 @@
    {{#if model.canLockTrustLevel}} {{#if model.trust_level_locked}} - {{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}} + {{d-icon "lock" title="admin.user.trust_level_locked_tip"}} + {{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}} {{else}} - {{d-button action="lockTrustLevel" actionParam=true label="admin.user.lock_trust_level"}} + {{d-icon "unlock" title="admin.user.trust_level_unlocked_tip"}} + {{d-button action="lockTrustLevel" actionParam=true label="admin.user.lock_trust_level"}} {{/if}} {{/if}} {{#if model.tl3Requirements}} @@ -487,7 +489,7 @@

    - {{fa-icon "exclamation-triangle"}} {{model.deleteExplanation}} + {{d-icon "exclamation-triangle"}} {{model.deleteExplanation}}
    {{/if}} diff --git a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs index e0536ddd08..1561a95670 100644 --- a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs +++ b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs @@ -1,7 +1,7 @@
    @@ -24,7 +24,7 @@
    - + @@ -32,67 +32,67 @@ - + - + - + - + - + - + - + - + - + - + - + @@ -105,16 +105,16 @@ {{#if model.tl3Requirements.requirements_lost}} {{! tl implicitly not locked }} {{#if model.tl3Requirements.on_grace_period}} - {{i18n 'admin.user.tl3_requirements.on_grace_period'}} + {{d-icon "times"}} {{i18n 'admin.user.tl3_requirements.on_grace_period'}} {{else}} {{! not on grace period }} - {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} + {{d-icon "times"}} {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} {{i18n 'admin.user.tl3_requirements.will_be_demoted'}} {{/if}} {{else}} {{! requirements not lost - remains tl3 }} {{#if model.tl3Requirements.trust_level_locked}} - {{i18n 'admin.user.tl3_requirements.locked_will_not_be_demoted'}} + {{d-icon "lock"}} {{i18n 'admin.user.tl3_requirements.locked_will_not_be_demoted'}} {{else}} {{! tl not locked }} - {{i18n 'admin.user.tl3_requirements.qualifies'}} + {{d-icon "check"}} {{i18n 'admin.user.tl3_requirements.qualifies'}} {{#if model.tl3Requirements.on_grace_period}} {{i18n 'admin.user.tl3_requirements.on_grace_period'}} {{/if}} @@ -123,13 +123,13 @@ {{else}} {{! is not tl3 }} {{#if model.tl3Requirements.requirements_met}} {{! met & not tl3 - will be promoted}} - {{i18n 'admin.user.tl3_requirements.qualifies'}} + {{d-icon "check"}} {{i18n 'admin.user.tl3_requirements.qualifies'}} {{i18n 'admin.user.tl3_requirements.will_be_promoted'}} {{else}} {{! requirements not met - remains regular }} {{#if model.tl3Requirements.trust_level_locked}} - {{i18n 'admin.user.tl3_requirements.locked_will_not_be_promoted'}} + {{d-icon "lock"}} {{i18n 'admin.user.tl3_requirements.locked_will_not_be_promoted'}} {{else}} - {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} + {{d-icon "times"}} {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} {{/if}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/users-list-show.hbs b/app/assets/javascripts/admin/templates/users-list-show.hbs index cc0c5ce09e..9bb578af7d 100644 --- a/app/assets/javascripts/admin/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/templates/users-list-show.hbs @@ -92,10 +92,10 @@ {{/if}} diff --git a/app/assets/javascripts/admin/templates/version-checks.hbs b/app/assets/javascripts/admin/templates/version-checks.hbs index 25c560ad6a..4f39265026 100644 --- a/app/assets/javascripts/admin/templates/version-checks.hbs +++ b/app/assets/javascripts/admin/templates/version-checks.hbs @@ -18,7 +18,7 @@ {{#if versionCheck.noCheckPerformed}} diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 632ad9d8f9..61c5442a93 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -69,8 +69,6 @@ //= require ./discourse/components/notifications-button //= require ./discourse/lib/link-mentions //= require ./discourse/components/site-header -//= require ./discourse/lib/emoji/groups -//= require ./discourse/lib/emoji/toolbar //= require ./discourse/components/d-editor //= require ./discourse/lib/screen-track //= require ./discourse/routes/discourse diff --git a/app/assets/javascripts/discourse-common/components/combo-box.js.es6 b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 index e5828e8fc7..0d8cd538b8 100644 --- a/app/assets/javascripts/discourse-common/components/combo-box.js.es6 +++ b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 @@ -1,5 +1,6 @@ import { bufferedRender } from 'discourse-common/lib/buffered-render'; import { on, observes } from 'ember-addons/ember-computed-decorators'; +import { iconHTML } from 'discourse-common/lib/icon-library'; export default Ember.Component.extend(bufferedRender({ tagName: 'select', @@ -97,7 +98,7 @@ export default Ember.Component.extend(bufferedRender({ this.selectionTemplate = (item) => { let name = Em.get(item, 'text'); name = Handlebars.escapeExpression(name); - return `${name}`; + return iconHTML(this.get('selectionIcon')) + name; }; } diff --git a/app/assets/javascripts/discourse-common/helpers/d-icon.js.es6 b/app/assets/javascripts/discourse-common/helpers/d-icon.js.es6 new file mode 100644 index 0000000000..b85b9ebf26 --- /dev/null +++ b/app/assets/javascripts/discourse-common/helpers/d-icon.js.es6 @@ -0,0 +1,6 @@ +import { registerUnbound } from 'discourse-common/lib/helpers'; +import { renderIcon } from 'discourse-common/lib/icon-library'; + +registerUnbound('d-icon', function(id, params) { + return new Handlebars.SafeString(renderIcon('string', id, params)); +}); diff --git a/app/assets/javascripts/discourse-common/helpers/fa-icon.js.es6 b/app/assets/javascripts/discourse-common/helpers/fa-icon.js.es6 index 3c1f8b9b5a..bd736c8482 100644 --- a/app/assets/javascripts/discourse-common/helpers/fa-icon.js.es6 +++ b/app/assets/javascripts/discourse-common/helpers/fa-icon.js.es6 @@ -1,25 +1,12 @@ import { registerUnbound } from 'discourse-common/lib/helpers'; +import { renderIcon } from 'discourse-common/lib/icon-library'; +import deprecated from 'discourse-common/lib/deprecated'; -export function iconClasses(icon, params) { - let classes = "fa fa-" + icon; - if (params.modifier) { classes += " fa-" + params.modifier; } - if (params['class']) { classes += ' ' + params['class']; } - return classes; -} - -export function iconHTML(icon, params) { - params = params || {}; - - var html = ""; - } - return html; +export function iconHTML(id, params) { + return renderIcon('string', id, params); } registerUnbound('fa-icon', function(icon, params) { + deprecated("Use `{{d-icon}}` instead of `{{fa-icon}}"); return new Handlebars.SafeString(iconHTML(icon, params)); }); diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 new file mode 100644 index 0000000000..60fe773995 --- /dev/null +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -0,0 +1,71 @@ +import { h } from 'virtual-dom'; +let _renderers = []; + +export function renderIcon(renderType, id, params) { + for (let i=0; i<_renderers.length; i++) { + let renderer = _renderers[i]; + let rendererForType = renderer[renderType]; + + if (rendererForType) { + let result = rendererForType(id, params || {}); + if (result) { + return result; + } + } + } +} + +export function iconHTML(id, params) { + return renderIcon('string', id, params); +} + +export function iconNode(id, params) { + return renderIcon('node', id, params); +} + +export function registerIconRenderer(renderer) { + _renderers.unshift(renderer); +} + +// Support for font awesome icons +function faClasses(id, params) { + let classNames = `fa fa-${id} d-icon d-icon-${id}`; + if (params) { + if (params.modifier) { classNames += " fa-" + params.modifier; } + if (params['class']) { classNames += ' ' + params['class']; } + } + return classNames; +} + +// default resolver is font awesome +registerIconRenderer({ + name: 'font-awesome', + + string(id, params) { + let tagName = params.tagName || 'i'; + let html = `<${tagName} class='${faClasses(id, params)}'`; + if (params.title) { html += ` title='${I18n.t(params.title)}'`; } + if (params.label) { html += " aria-hidden='true'"; } + html += `>`; + if (params.label) { + html += "" + I18n.t(params.label) + ""; + } + return html; + }, + + node(id, params) { + let tagName = params.tagName || 'i'; + + const properties = { + className: faClasses(id, params), + attributes: { "aria-hidden": true } + }; + + if (params.title) { properties.attributes.title = params.title; } + if (params.label) { + return h(tagName, properties, h('span.sr-only', I18n.t(params.label))); + } else { + return h(tagName, properties); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 b/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 index 60978f3959..0ad58a506c 100644 --- a/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 @@ -1,3 +1,4 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import Combobox from 'discourse-common/components/combo-box'; import { CLOSE_STATUS_TYPE } from 'discourse/controllers/edit-topic-timer'; @@ -111,9 +112,7 @@ export default Combobox.extend({ let icons; if (icon) { - icons = icon.split(',').map(i => { - return ``; - }).join(" "); + icons = icon.split(',').map(i => iconHTML(i)).join(" "); } if (time) { diff --git a/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 index 8dbb4d3423..6a6c1a3353 100644 --- a/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 @@ -1,4 +1,4 @@ -import { iconHTML } from 'discourse-common/helpers/fa-icon'; +import { iconHTML } from 'discourse-common/lib/icon-library'; import DropdownButton from 'discourse/components/dropdown-button'; import computed from "ember-addons/ember-computed-decorators"; @@ -15,14 +15,14 @@ export default DropdownButton.extend({ { id: 'create', title: I18n.t('category.create'), description: I18n.t('category.create_long'), - styleClasses: 'fa fa-plus' } + icon: 'plus' } ]; if (includeReorder) { items.push({ id: 'reorder', title: I18n.t('categories.reorder.title'), description: I18n.t('categories.reorder.title_long'), - styleClasses: 'fa fa-random' + icon: 'random' }); } return items; diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6 index e9f05c33fe..0f51ca5751 100644 --- a/app/assets/javascripts/discourse/components/category-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/category-drop.js.es6 @@ -1,4 +1,6 @@ import { setting } from 'discourse/lib/computed'; +import computed from 'ember-addons/ember-computed-decorators'; + var get = Ember.get; export default Ember.Component.extend({ @@ -8,10 +10,10 @@ export default Ember.Component.extend({ tagName: 'li', - iconClass: function() { - if (this.get('expanded')) { return "fa fa-caret-down"; } - return "fa fa-caret-right"; - }.property('expanded'), + @computed('expanded') + expandIcon(expanded) { + return expanded ? 'caret-down' : 'caret-right'; + }, allCategoriesUrl: function() { if (this.get('subCategory')) { @@ -33,7 +35,7 @@ export default Ember.Component.extend({ }.property('category'), dropdownButtonClass: function() { - var result = 'badge-category category-dropdown-button'; + let result = 'dropdown-header category-dropdown-button'; if (Em.isNone(this.get('category'))) { result += ' home'; } @@ -57,21 +59,35 @@ export default Ember.Component.extend({ }.property('category'), badgeStyle: function() { - var category = this.get('category'); + let category = this.get('category'); + + const categoryStyle = this.siteSettings.category_style; + if (categoryStyle === 'bullet') { + return; + } if (category) { - var color = get(category, 'color'), - textColor = get(category, 'text_color'); + let color = get(category, 'color'); + let textColor = get(category, 'text_color'); if (color || textColor) { - var style = ""; - if (color) { style += "background-color: #" + color + "; border-color: #" + color + ";"; } - if (textColor) { style += "color: #" + textColor + "; "; } + let style = ""; + if (color) { + if (categoryStyle === "bar") { + style += `border-color: #${color};`; + } else if (categoryStyle === "box") { + style += `background-color: #${color};`; + if (textColor) { style += "color: #" + textColor + "; "; } + } + } + return style.htmlSafe(); } } - return "background-color: #eee; color: #333".htmlSafe(); + if (categoryStyle === 'box') { + return "background-color: #eee; color: #333".htmlSafe(); + } }.property('category'), clickEventName: function() { diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index fe2ab5aae8..8f2528cdd3 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -1,10 +1,11 @@ import userSearch from 'discourse/lib/user-search'; -import { default as computed, on } from 'ember-addons/ember-computed-decorators'; +import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags'; import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag'; import Composer from 'discourse/models/composer'; import { load } from 'pretty-text/oneboxer'; +import { applyInlineOneboxes } from 'pretty-text/inline-oneboxer'; import { ajax } from 'discourse/lib/ajax'; import InputValidation from 'discourse/models/input-validation'; import { findRawTemplate } from 'discourse/lib/raw-templates'; @@ -30,6 +31,14 @@ export default Ember.Component.extend({ _setupPreview() { const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true')); this.set('showPreview', val === 'true'); + + this.appEvents.on('composer:show-preview', () => { + this.set('showPreview', true); + }); + + this.appEvents.on('composer:hide-preview', () => { + this.set('showPreview', false); + }); }, @computed('site.mobileView', 'showPreview') @@ -42,9 +51,16 @@ export default Ember.Component.extend({ return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview'); }, + @observes('showPreview') + showPreviewChanged() { + this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); + }, + @computed markdownOptions() { return { + previewing: true, + lookupAvatarByPostNumber: (postNumber, topicId) => { const topic = this.get('topic'); if (!topic) { return; } @@ -158,6 +174,10 @@ export default Ember.Component.extend({ }); }, + _loadInlineOneboxes(inline) { + applyInlineOneboxes(inline, ajax); + }, + _loadOneboxes($oneboxes) { const post = this.get('composer.post'); let refresh = false; @@ -445,6 +465,8 @@ export default Ember.Component.extend({ @on('willDestroyElement') _composerClosed() { this.appEvents.trigger('composer:will-close'); + this.appEvents.off('composer:show-preview'); + this.appEvents.off('composer:hide-preview'); Ember.run.next(() => { $('#main-outlet').css('padding-bottom', 0); // need to wait a bit for the "slide down" transition of the composer @@ -486,7 +508,6 @@ export default Ember.Component.extend({ togglePreview() { this.toggleProperty('showPreview'); - this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); }, extraButtons(toolbar) { @@ -558,6 +579,18 @@ export default Ember.Component.extend({ Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450); } + let inline = {}; + $('a.inline-onebox-loading', $preview).each(function(index, link) { + let $link = $(link); + $link.removeClass('inline-onebox-loading'); + let text = $link.text(); + inline[text] = inline[text] || []; + inline[text].push($link); + }); + if (Object.keys(inline).length > 0) { + Ember.run.debounce(this, this._loadInlineOneboxes, inline, 450); + } + this.trigger('previewRefreshed', $preview); this.sendAction('afterRefresh', $preview); }, diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index 2e37da138e..8841500af8 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -2,6 +2,7 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor import InputValidation from 'discourse/models/input-validation'; import { load, lookupCache } from 'pretty-text/oneboxer'; import { ajax } from 'discourse/lib/ajax'; +import afterTransition from 'discourse/lib/after-transition'; export default Ember.Component.extend({ classNames: ['title-input'], @@ -10,7 +11,11 @@ export default Ember.Component.extend({ didInsertElement() { this._super(); if (this.get('focusTarget') === 'title') { - this.$('input').putCursorAtEnd(); + const $input = this.$("input"); + + afterTransition(this.$().closest("#reply-control"), () => { + $input.putCursorAtEnd(); + }); } }, diff --git a/app/assets/javascripts/discourse/components/composer-toggles.js.es6 b/app/assets/javascripts/discourse/components/composer-toggles.js.es6 new file mode 100644 index 0000000000..cd5369638c --- /dev/null +++ b/app/assets/javascripts/discourse/components/composer-toggles.js.es6 @@ -0,0 +1,14 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + tagName: '', + + @computed('composeState') + toggleIcon(composeState) { + if (composeState === "draft" || composeState === "saving") { + return "times"; + } + return "chevron-down"; + } +}); + diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 289d5ee4f1..f0638423d0 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -1,6 +1,5 @@ /*global Mousetrap:true */ import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; -import { showSelector } from "discourse/lib/emoji/toolbar"; import Category from 'discourse/models/category'; import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags'; import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; @@ -78,7 +77,11 @@ class Toolbar { group: 'insertions', icon: 'quote-right', shortcut: 'Shift+9', - perform: e => e.applyList('> ', 'blockquote_text') + perform: e => e.applyList( + '> ', + 'blockquote_text', + { applyEmptyLines: true, multiline: true } + ) }); this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'}); @@ -198,6 +201,7 @@ export default Ember.Component.extend({ linkText: '', lastSel: null, _mouseTrap: null, + emojiPickerIsActive: false, @computed('placeholder') placeholderTranslated(placeholder) { @@ -328,7 +332,6 @@ export default Ember.Component.extend({ _applyEmojiAutocomplete($editorInput) { if (!this.siteSettings.enable_emoji) { return; } - const register = this.register; const self = this; $editorInput.autocomplete({ @@ -346,20 +349,8 @@ export default Ember.Component.extend({ if (v.code) { return `${v.code}:`; } else { - showSelector({ - appendTo: self.$(), - register, - onSelect: title => { - // Remove the previously type characters when a new emoji is selected from the selector. - let selected = self._getSelected(); - let newPre = selected.pre.replace(/:[^:]+$/, ":"); - let numOfRemovedChars = selected.pre.length - newPre.length; - selected.pre = newPre; - selected.start -= numOfRemovedChars; - selected.end -= numOfRemovedChars; - self._addText(selected, `${title}:`); - } - }); + $editorInput.autocomplete({cancel: true}); + self.set('emojiPickerIsActive', true); return ""; } }, @@ -453,11 +444,15 @@ export default Ember.Component.extend({ }, // perform the same operation over many lines of text - _getMultilineContents(lines, head, hval, hlen, tail, tlen) { + _getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) { let operation = OP.NONE; + const applyEmptyLines = opts && opts.applyEmptyLines; + return lines.map(l => { - if (l.length === 0) { return l; } + if (!applyEmptyLines && l.length === 0) { + return l; + } if (operation !== OP.ADDED && (l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail)) { @@ -513,8 +508,15 @@ export default Ember.Component.extend({ this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`); this._selectText(sel.start - hlen, sel.value.length); } else { - const contents = this._getMultilineContents(lines, head, hval, hlen, tail, tlen); - + const contents = this._getMultilineContents( + lines, + head, + hval, + hlen, + tail, + tlen, + opts + ); this.set('value', `${pre}${contents}${post}`); if (lines.length === 1 && tlen > 0) { this._selectText(sel.start + hlen, sel.value.length); @@ -525,9 +527,9 @@ export default Ember.Component.extend({ } }, - _applyList(sel, head, exampleKey) { + _applyList(sel, head, exampleKey, opts) { if (sel.value.indexOf("\n") !== -1) { - this._applySurround(sel, head, '', exampleKey); + this._applySurround(sel, head, '', exampleKey, opts); } else { const [hval, hlen] = getHead(head); @@ -587,9 +589,10 @@ export default Ember.Component.extend({ if (post.length > 0) { post = post.replace(/^\n*/, "\n\n"); + } else { + post = "\n"; } - const value = pre + text + post; const $textarea = this.$('textarea.d-editor-input'); @@ -614,13 +617,28 @@ export default Ember.Component.extend({ }, actions: { + emojiSelected(code) { + let selected = this._getSelected(); + const captures = selected.pre.match(/\B:(\w*)$/); + + if(_.isEmpty(captures)) { + this._addText(selected, `:${code}:`); + } else { + let numOfRemovedChars = selected.pre.length - captures[1].length; + selected.pre = selected.pre.slice(0, selected.pre.length - captures[1].length); + selected.start -= numOfRemovedChars; + selected.end -= numOfRemovedChars; + this._addText(selected, `${code}:`); + } + }, + toolbarButton(button) { const selected = this._getSelected(button.trimLeading); const toolbarEvent = { selected, selectText: (from, length) => this._selectText(from, length), applySurround: (head, tail, exampleKey, opts) => this._applySurround(selected, head, tail, exampleKey, opts), - applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey), + applyList: (head, exampleKey, opts) => this._applyList(selected, head, exampleKey, opts), addText: text => this._addText(selected, text), replaceText: text => this._addText({pre: '', post: ''}, text), getText: () => this.get('value'), @@ -692,11 +710,7 @@ export default Ember.Component.extend({ }, emoji() { - showSelector({ - appendTo: this.$(), - register: this.register, - onSelect: title => this._addText(this._getSelected(), `:${title}:`) - }); + this.set('emojiPickerIsActive', !this.get('emojiPickerIsActive')); } } }); diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index 475c79d049..1e1a40cdc3 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -2,7 +2,7 @@ import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ elementId: 'discourse-modal', - classNameBindings: [':modal', ':hidden', 'modalClass'], + classNameBindings: [':modal', 'modalClass'], attributeBindings: ['data-keyboard'], // We handle ESC ourselves @@ -17,6 +17,7 @@ export default Ember.Component.extend({ }); this.appEvents.on('modal:body-shown', data => { + this.$().removeClass('hidden'); if (data.title) { this.set('title', I18n.t(data.title)); } else if (data.rawTitle) { diff --git a/app/assets/javascripts/discourse/components/directory-toggle.js.es6 b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 index 3f55ec0543..8da529957a 100644 --- a/app/assets/javascripts/discourse/components/directory-toggle.js.es6 +++ b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 @@ -1,4 +1,4 @@ -import { iconHTML } from 'discourse-common/helpers/fa-icon'; +import { iconHTML } from 'discourse-common/lib/icon-library'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; export default Ember.Component.extend(bufferedRender({ diff --git a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 b/app/assets/javascripts/discourse/components/dropdown-button.js.es6 index 032c80e2af..bffeaa6cdc 100644 --- a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 +++ b/app/assets/javascripts/discourse/components/dropdown-button.js.es6 @@ -1,3 +1,4 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; export default Ember.Component.extend(bufferedRender({ @@ -29,7 +30,7 @@ export default Ember.Component.extend(bufferedRender({ buffer.push("

    " + title + "

    "); } - buffer.push(``); + buffer.push(``); buffer.push("
    {{fa-icon "envelope"}} {{i18n 'admin.dashboard.private_messages_short'}}{{d-icon "envelope"}} {{i18n 'admin.dashboard.private_messages_short'}} {{i18n 'admin.dashboard.reports.today'}} {{i18n 'admin.dashboard.reports.yesterday'}} {{i18n 'admin.dashboard.reports.last_7_days'}} - {{#if l.bounced}}{{fa-icon "repeat" title="admin.email.bounced"}}{{/if}} + {{#if l.bounced}}{{d-icon "repeat" title="admin.email.bounced"}}{{/if}} {{l.to_address}} {{l.email_type}}
    :{{e.name}}:
    {{#if adminActiveFlagsView}} - + {{#if flaggedPost.postHidden}} - + {{else}} - + {{/if}} - - + + {{/if}}
    {{i18n 'admin.user.tl3_requirements.visits'}}{{check-icon model.tl3Requirements.met.days_visited}} {{model.tl3Requirements.days_visited_percent}}% ({{model.tl3Requirements.days_visited}} / {{model.tl3Requirements.time_period}} {{i18n 'admin.user.tl3_requirements.days'}})
    {{i18n 'admin.user.tl3_requirements.topics_replied_to'}}{{check-icon model.tl3Requirements.met.topics_replied_to}} {{model.tl3Requirements.num_topics_replied_to}} {{model.tl3Requirements.min_topics_replied_to}}
    {{i18n 'admin.user.tl3_requirements.topics_viewed'}}{{check-icon model.tl3Requirements.met.topics_viewed}} {{model.tl3Requirements.topics_viewed}} {{model.tl3Requirements.min_topics_viewed}}
    {{i18n 'admin.user.tl3_requirements.topics_viewed_all_time'}}{{check-icon model.tl3Requirements.met.topics_viewed_all_time}} {{model.tl3Requirements.topics_viewed_all_time}} {{model.tl3Requirements.min_topics_viewed_all_time}}
    {{i18n 'admin.user.tl3_requirements.posts_read'}}{{check-icon model.tl3Requirements.met.posts_read}} {{model.tl3Requirements.posts_read}} {{model.tl3Requirements.min_posts_read}}
    {{i18n 'admin.user.tl3_requirements.posts_read_all_time'}}{{check-icon model.tl3Requirements.met.posts_read_all_time}} {{model.tl3Requirements.posts_read_all_time}} {{model.tl3Requirements.min_posts_read_all_time}}
    {{i18n 'admin.user.tl3_requirements.flagged_posts'}}{{check-icon model.tl3Requirements.met.flagged_posts}} {{model.tl3Requirements.num_flagged_posts}} {{i18n 'max_of_count' count=model.tl3Requirements.max_flagged_posts}}
    {{i18n 'admin.user.tl3_requirements.flagged_by_users'}}{{check-icon model.tl3Requirements.met.flagged_by_users}} {{model.tl3Requirements.num_flagged_by_users}} {{i18n 'max_of_count' count=model.tl3Requirements.max_flagged_by_users}}
    {{i18n 'admin.user.tl3_requirements.likes_given'}}{{check-icon model.tl3Requirements.met.likes_given}} {{model.tl3Requirements.num_likes_given}} {{model.tl3Requirements.min_likes_given}}
    {{i18n 'admin.user.tl3_requirements.likes_received'}}{{check-icon model.tl3Requirements.met.likes_received}} {{model.tl3Requirements.num_likes_received}} {{model.tl3Requirements.min_likes_received}}
    {{i18n 'admin.user.tl3_requirements.likes_received_days'}}{{check-icon model.tl3Requirements.met.likes_received_days}} {{model.tl3Requirements.num_likes_received_days}} {{model.tl3Requirements.min_likes_received_days}}
    {{i18n 'admin.user.tl3_requirements.likes_received_users'}}{{check-icon model.tl3Requirements.met.likes_received_users}} {{model.tl3Requirements.num_likes_received_users}} {{model.tl3Requirements.min_likes_received_users}}
    {{#if user.admin}} - {{fa-icon "shield" title="admin.title" }} + {{d-icon "shield" title="admin.title" }} {{/if}} {{#if user.moderator}} - {{fa-icon "shield" title="admin.moderator" }} + {{d-icon "shield" title="admin.moderator" }} {{/if}}
    - {{fa-icon "frown-o"}} + {{d-icon "frown-o"}} {{i18n 'admin.dashboard.no_check_performed'}} @@ -28,9 +28,9 @@ {{#if versionCheck.version_check_pending}}{{dash-if-empty versionCheck.installed_version}}{{/if}} {{#if versionCheck.version_check_pending}} - {{fa-icon "smile-o"}} + {{d-icon "smile-o"}} {{else}} - {{fa-icon "frown-o"}} + {{d-icon "frown-o"}} {{/if}} @@ -46,13 +46,13 @@ {{dash-if-empty versionCheck.latest_version}} {{#if versionCheck.upToDate }} - {{fa-icon "smile-o"}} + {{d-icon "smile-o"}} {{else}} {{#if versionCheck.behindByOneVersion}} - {{fa-icon "meh-o"}} + {{d-icon "meh-o"}} {{else}} - {{fa-icon "frown-o"}} + {{d-icon "frown-o"}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/watched-words-action.hbs b/app/assets/javascripts/admin/templates/watched-words-action.hbs new file mode 100644 index 0000000000..3a7351d9ed --- /dev/null +++ b/app/assets/javascripts/admin/templates/watched-words-action.hbs @@ -0,0 +1,18 @@ +

    {{model.name}}

    + +

    {{actionDescription}}

    + +{{watched-word-form actionKey=actionNameKey action="recordAdded"}} + +{{watched-word-uploader uploading=uploading actionKey=actionNameKey done="uploadComplete"}} + +
    +
    + {{#if showWordsList}} + {{#each filteredContent as |word| }} +
    {{admin-watched-word word=word action="recordRemoved"}}
    + {{/each}} + {{else}} + {{i18n 'admin.watched_words.word_count' count=model.words.length}} + {{/if}} +
    diff --git a/app/assets/javascripts/admin/templates/watched-words.hbs b/app/assets/javascripts/admin/templates/watched-words.hbs new file mode 100644 index 0000000000..501e4af97b --- /dev/null +++ b/app/assets/javascripts/admin/templates/watched-words.hbs @@ -0,0 +1,31 @@ +
    + +
    + {{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}} + {{d-button action="clearFilter" label="admin.watched_words.clear_filter"}} +
    +
    + +
    + +
    + +
    + {{outlet}} +
    + +
    diff --git a/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs b/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs index f3b8825e5b..e0d0609871 100644 --- a/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs +++ b/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs @@ -1,10 +1,10 @@
    {{#link-to 'adminWebHooks' tagName='button' classNames='btn'}} - {{fa-icon 'list'}} {{i18n 'admin.web_hooks.events.go_list'}} + {{d-icon 'list'}} {{i18n 'admin.web_hooks.events.go_list'}} {{/link-to}} {{d-button icon="send" label="admin.web_hooks.events.ping" action="ping" disabled=pingDisabled}} {{#link-to 'adminWebHooks.show' model.extras.web_hook_id tagName='button' classNames='btn'}} - {{fa-icon 'edit'}} {{i18n 'admin.web_hooks.events.go_details'}} + {{d-icon 'edit'}} {{i18n 'admin.web_hooks.events.go_details'}} {{/link-to}}
    diff --git a/app/assets/javascripts/admin/templates/web-hooks-show.hbs b/app/assets/javascripts/admin/templates/web-hooks-show.hbs index c7a8e03088..8530de5f12 100644 --- a/app/assets/javascripts/admin/templates/web-hooks-show.hbs +++ b/app/assets/javascripts/admin/templates/web-hooks-show.hbs @@ -1,5 +1,5 @@ {{#link-to 'adminWebHooks' class="go-back"}} - {{fa-icon 'arrow-left'}} + {{d-icon 'arrow-left'}} {{i18n 'admin.web_hooks.go_back'}} {{/link-to}} @@ -49,12 +49,12 @@
    - + {{category-selector categories=model.categories blacklist=model.categories}}
    {{i18n 'admin.web_hooks.categories_filter_instructions'}}
    - + {{group-selector groupNames=model.groupsFilterInName groupFinder=model.groupFinder}}
    {{i18n 'admin.web_hooks.groups_filter_instructions'}}
    diff --git a/app/assets/javascripts/admin/templates/web-hooks.hbs b/app/assets/javascripts/admin/templates/web-hooks.hbs index 082cf170b4..81d4b86e4d 100644 --- a/app/assets/javascripts/admin/templates/web-hooks.hbs +++ b/app/assets/javascripts/admin/templates/web-hooks.hbs @@ -1,6 +1,6 @@
    {{#link-to 'adminWebHooks.show' 'new' tagName='button' classNames='btn'}} - {{fa-icon 'plus'}} {{i18n 'admin.web_hooks.new'}} + {{d-icon 'plus'}} {{i18n 'admin.web_hooks.new'}} {{/link-to}}
    @@ -24,7 +24,7 @@
    {{#link-to 'adminWebHooks.show' webHook}}{{webHook.payload_url}}{{/link-to}} {{webHook.description}} - {{#link-to 'adminWebHooks.show' webHook tagName='button' classNames='btn btn-default no-text'}}{{fa-icon 'edit'}}{{/link-to}} + {{#link-to 'adminWebHooks.show' webHook tagName='button' classNames='btn btn-default no-text'}}{{d-icon 'edit'}}{{/link-to}} {{d-button class="destroy btn-danger" action='destroy' actionParam=webHook icon="remove"}}
    @@ -89,7 +89,7 @@ {{#if contactInfo}}
    -

    {{fa-icon "envelope-o"}} {{i18n 'about.contact'}}

    +

    {{d-icon "envelope-o"}} {{i18n 'about.contact'}}

    {{{contactInfo}}}

    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs index efbf18a33b..1c429ddb39 100644 --- a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs @@ -5,7 +5,7 @@ {{#if option.model}} {{category-link option.model allowUncategorized="true" link="false"}} {{else}} - {{fa-icon 'tag'}}{{option.text}} x {{option.count}} + {{d-icon 'tag'}}{{option.text}} x {{option.count}} {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/auto-update-input.hbs b/app/assets/javascripts/discourse/templates/components/auto-update-input.hbs index 1e6fbe778b..50f0a8ee25 100644 --- a/app/assets/javascripts/discourse/templates/components/auto-update-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/auto-update-input.hbs @@ -14,11 +14,11 @@ {{#if isCustom}}
    - {{fa-icon "calendar"}} {{date-picker-future value=date defaultDate=date}} + {{d-icon "calendar"}} {{date-picker-future value=date defaultDate=date}}
    - {{fa-icon "clock-o"}} + {{d-icon "clock-o"}} {{input type="time" value=time}}
    {{/if}} @@ -33,7 +33,7 @@ {{#if willCloseImmediately}}
    - {{fa-icon "warning"}} + {{d-icon "warning"}} {{willCloseI18n}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs index 89c46363e3..b43cbb0d56 100644 --- a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs @@ -1,5 +1,5 @@ {{#if uploading}} diff --git a/app/assets/javascripts/discourse/templates/components/badge-card.hbs b/app/assets/javascripts/discourse/templates/components/badge-card.hbs index 93d3c490b6..3c519d85dd 100644 --- a/app/assets/javascripts/discourse/templates/components/badge-card.hbs +++ b/app/assets/javascripts/discourse/templates/components/badge-card.hbs @@ -2,7 +2,7 @@ {{displayCount}} {{/if}} {{#if badge.has_badge}} - {{fa-icon "check"}} + {{d-icon "check"}} {{/if}}
    diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs index 8909e8a9ba..434e18fe62 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs @@ -9,7 +9,7 @@

    {{#if c.read_restricted}} - {{fa-icon 'lock'}} + {{d-icon 'lock'}} {{/if}} {{c.name}}

    diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs index 3bb4fde665..c473668648 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs @@ -9,7 +9,7 @@

    {{#if c.read_restricted}} - {{fa-icon 'lock'}} + {{d-icon 'lock'}} {{/if}} {{c.name}}

    diff --git a/app/assets/javascripts/discourse/templates/components/category-drop.hbs b/app/assets/javascripts/discourse/templates/components/category-drop.hbs index 9e9cc410e5..d660be87db 100644 --- a/app/assets/javascripts/discourse/templates/components/category-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-drop.hbs @@ -1,21 +1,23 @@ {{#if category}} - + {{#if category.read_restricted}} - {{fa-icon "lock"}} + {{d-icon "lock"}} {{/if}} {{category.name}} {{else}} {{#if noSubcategories}} - {{i18n 'categories.no_subcategory'}} + {{i18n 'categories.no_subcategory'}} {{else}} - {{allCategoriesLabel}} + {{allCategoriesLabel}} {{/if}} {{/if}} {{#if categories}} - + + {{d-icon expandIcon}} +
    {{#if subCategory}} diff --git a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs index 49e3b088f1..fcb0bdeb07 100644 --- a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs @@ -1,6 +1,6 @@ {{#if category.read_restricted}} - {{fa-icon 'lock'}} + {{d-icon 'lock'}} {{/if}} {{category.name}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs index 46daaeb10e..3d3f6fc8eb 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs @@ -30,7 +30,7 @@ {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} {{uploadProgress}}% {{#if isCancellable}} - {{fa-icon "times"}} + {{d-icon "times"}} {{/if}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs new file mode 100644 index 0000000000..09e841bd85 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs @@ -0,0 +1,12 @@ +{{#if site.mobileView}} + {{flat-button + class="toggle-toolbar" + icon="bars" + action=toggleToolbar}} +{{/if}} + +{{flat-button + class="toggler" + icon=toggleIcon + action=toggleComposer + title='composer.toggler'}} diff --git a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs index a29839a790..859433546c 100644 --- a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs @@ -1,5 +1,5 @@ {{#if uploading}} diff --git a/app/assets/javascripts/discourse/templates/components/d-button.hbs b/app/assets/javascripts/discourse/templates/components/d-button.hbs index e01a9bb5c8..d06d14e66d 100644 --- a/app/assets/javascripts/discourse/templates/components/d-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-button.hbs @@ -1,5 +1,5 @@ {{#if icon}} - {{fa-icon icon}} + {{d-icon icon}} {{/if}} {{#if translatedLabel}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 78d00be874..961b63c6ef 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -33,3 +33,5 @@ {{plugin-outlet name="editor-preview"}}
    + +{{emoji-picker active=emojiPickerIsActive emojiSelected=(action 'emojiSelected')}} diff --git a/app/assets/javascripts/discourse/templates/components/disabled-icon.hbs b/app/assets/javascripts/discourse/templates/components/disabled-icon.hbs index b7749fecee..100849f312 100644 --- a/app/assets/javascripts/discourse/templates/components/disabled-icon.hbs +++ b/app/assets/javascripts/discourse/templates/components/disabled-icon.hbs @@ -1,5 +1,5 @@ -{{fa-icon icon modifier="stack-2x"}} +{{d-icon icon modifier="stack-2x"}} {{#if disabled}} - {{fa-icon "ban" modifier="stack-2x"}} + {{d-icon "ban" modifier="stack-2x"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs b/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs index b46760d5c1..d7e0c5e1d4 100644 --- a/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs +++ b/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs @@ -1,7 +1,9 @@ {{#if visible}}
    diff --git a/app/assets/javascripts/discourse/templates/components/group-member.hbs b/app/assets/javascripts/discourse/templates/components/group-member.hbs index d476fdf033..ddc6456b83 100644 --- a/app/assets/javascripts/discourse/templates/components/group-member.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-member.hbs @@ -1 +1 @@ -{{avatar member imageSize="small"}} {{member.username}} {{#unless automatic}}{{fa-icon "times"}}{{/unless}} +{{avatar member imageSize="small"}} {{member.username}} {{#unless automatic}}{{d-icon "times"}}{{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs index c720c630f5..ad66499dbb 100644 --- a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs @@ -1,23 +1,30 @@ -
    - - {{currentPage}}/{{totalPages}} - -
    -
    - {{#each model.members as |member|}} - {{group-member member=member automatic=model.automatic removeAction="removeMember"}} - {{/each}} -
    + +{{#if model.members}} +
    + + {{currentPage}}/{{totalPages}} + +
    +
    + {{#each model.members as |member|}} + {{group-member member=member automatic=model.automatic removeAction="removeMember"}} + {{/each}} +
    +{{/if}} {{#unless model.automatic}}
    - {{user-selector usernames=model.usernames}} + {{user-selector usernames=model.usernames + placeholderKey="admin.groups.selector_placeholder" + id="member-selector"}} - {{d-button action="addMembers" - class="add" - icon="plus" - disabled=disableAddButton - label="groups.edit.add_members"}} + {{#if addButton}} + {{d-button action="addMembers" + class="add" + icon="plus" + disabled=disableAddButton + label="groups.edit.add_members"}} + {{/if}}
    {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs b/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs index ed17b2a79b..a154287430 100644 --- a/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs @@ -1,17 +1,15 @@ {{#if canJoinGroup}} - {{#if userIsGroupUser}} - {{d-button action="leaveGroup" - class="btn-danger group-index-leave" - icon="minus" - label="groups.leave" - disabled=updatingMembership}} - {{else}} - {{d-button action="joinGroup" - class="group-index-join" - icon="plus" - label="groups.join" - disabled=updatingMembership}} - {{/if}} + {{d-button action="joinGroup" + class="group-index-join" + icon="user-plus" + label="groups.join" + disabled=updatingMembership}} +{{else if canLeaveGroup}} + {{d-button action="leaveGroup" + class="btn-danger group-index-leave" + icon="user-times" + label="groups.leave" + disabled=updatingMembership}} {{else if model.allow_membership_requests}} {{#if userIsGroupUser}} {{#if showMembershipStatus}} @@ -25,7 +23,7 @@ {{d-button action="requestMembership" class="group-index-request" disabled=loading - icon="envelope" + icon="user-plus" label="groups.request"}} {{#if loading}} diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs index 14f0695fd2..5a12cff39b 100644 --- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs @@ -1,11 +1,11 @@
    {{#if backgroundStyle}} - + {{/if}} {{i18n 'upload_selector.uploading'}} {{uploadProgress}}%
    diff --git a/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs b/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs index 35d5afd0bb..44e1b1809b 100644 --- a/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs +++ b/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs @@ -1,11 +1,11 @@ {{#if ip}} {{/if}} {{#if show}}
    - {{fa-icon "times"}} + {{d-icon "times"}}

    {{i18n 'ip_lookup.title'}}

    {{#if location}} @@ -42,7 +42,7 @@ {{totalOthersWithSameIP}} {{#if other_accounts.length}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/period-chooser.hbs b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs index 3defac6a02..ce4d6066c5 100644 --- a/app/assets/javascripts/discourse/templates/components/period-chooser.hbs +++ b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs @@ -1,5 +1,5 @@

    {{period-title period showDateRange=true}}

    - +
      diff --git a/app/assets/javascripts/discourse/templates/components/queued-post.hbs b/app/assets/javascripts/discourse/templates/components/queued-post.hbs index 4b094fdb21..78a7723ab6 100644 --- a/app/assets/javascripts/discourse/templates/components/queued-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/queued-post.hbs @@ -11,7 +11,7 @@ {{post.user.username}} {{/user-link}} {{#if post.user.blocked}} - + {{d-icon "ban" title="user.blocked_tooltip"}} {{/if}}
    diff --git a/app/assets/javascripts/discourse/templates/components/share-popup.hbs b/app/assets/javascripts/discourse/templates/components/share-popup.hbs index b6f77b67c3..ad3dc852dd 100644 --- a/app/assets/javascripts/discourse/templates/components/share-popup.hbs +++ b/app/assets/javascripts/discourse/templates/components/share-popup.hbs @@ -16,13 +16,13 @@ {{#if topic.details.can_reply_as_new_topic}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/share-source.hbs b/app/assets/javascripts/discourse/templates/components/share-source.hbs index 64242d17fe..185f80ab9c 100644 --- a/app/assets/javascripts/discourse/templates/components/share-source.hbs +++ b/app/assets/javascripts/discourse/templates/components/share-source.hbs @@ -1,6 +1,6 @@ - {{#if source.faIcon}} - + {{#if source.icon}} + {{d-icon source.icon}} {{else}} {{{source.htmlIcon}}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/small-action.hbs b/app/assets/javascripts/discourse/templates/components/small-action.hbs index 821ed4ff91..852ef95aba 100644 --- a/app/assets/javascripts/discourse/templates/components/small-action.hbs +++ b/app/assets/javascripts/discourse/templates/components/small-action.hbs @@ -1,10 +1,10 @@
    {{#if post}} {{#if post.can_delete}} - + {{/if}} {{#if post.can_edit}} - + {{/if}} {{avatar post imageSize="small"}} diff --git a/app/assets/javascripts/discourse/templates/components/stream-item.hbs b/app/assets/javascripts/discourse/templates/components/stream-item.hbs index ea3c9dcec9..f2f44fc734 100644 --- a/app/assets/javascripts/discourse/templates/components/stream-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/stream-item.hbs @@ -10,7 +10,7 @@ {{#if item.deleted_by}} - {{fa-icon "trash-o"}} + {{d-icon "trash-o"}} {{avatar item.deleted_by imageSize="tiny" extraClasses="actor" ignoreTitle="true"}} {{format-date item.deleted_at leaveAgo="true"}} @@ -27,11 +27,11 @@ {{#each item.children as |child|}}
    - + {{d-icon child.icon class="icon"}} {{#each child.items as |grandChild|}} {{#if grandChild.removableBookmark}} {{else}}
    {{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true" avatarTemplatePath="acting_avatar_template"}}
    diff --git a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs index 833ab89590..8dc8869f6f 100644 --- a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs @@ -1,19 +1,21 @@ {{#if showTagDropdown}} {{#if tagId}} {{#if noTagsSelected}} - {{noTagsLabel}} + {{noTagsLabel}} {{else}} - {{tagId}} + {{tagId}} {{/if}} {{else}} - {{allTagsLabel}} + {{allTagsLabel}} {{/if}} {{#if tags}} - + + {{d-icon expandedIcon}} +
    - - + + {{#if renderTags}} {{#each tags as |t|}}
    diff --git a/app/assets/javascripts/discourse/templates/components/topic-entrance.hbs b/app/assets/javascripts/discourse/templates/components/topic-entrance.hbs index 399ac0bdaf..c6885392c4 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-entrance.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-entrance.hbs @@ -1,7 +1,7 @@ {{#d-button action="enterTop" class="full jump-top"}} - {{fa-icon 'caret-up'}} {{{topDate}}} + {{d-icon 'caret-up'}} {{{topDate}}} {{/d-button}} {{#d-button action="enterBottom" class="full jump-button"}} - {{{bottomDate}}} {{fa-icon 'caret-down'}} + {{{bottomDate}}} {{d-icon 'caret-down'}} {{/d-button}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs index 7e28cc0f9a..15b5882a00 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs @@ -28,7 +28,7 @@ action=toggleBookmark}} diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index a7041a5065..1c948ba800 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -36,25 +36,25 @@ {{#if isSuspended}}
    - {{fa-icon "ban"}} + {{d-icon "ban"}} {{i18n 'user.suspended_notice' date=user.suspendedTillDate}}
    {{i18n 'user.suspended_reason'}} {{user.suspend_reason}}
    @@ -71,12 +71,12 @@ {{#if hasLocationOrWebsite}}
    {{#if user.location}} - {{fa-icon "map-marker"}} {{user.location}} + {{d-icon "map-marker"}} {{user.location}} {{/if}} {{#if user.website_name}} - {{fa-icon "globe"}} + {{d-icon "globe"}} {{#if linkWebsite}} {{user.website_name}} {{else}} diff --git a/app/assets/javascripts/discourse/templates/components/user-stat.hbs b/app/assets/javascripts/discourse/templates/components/user-stat.hbs index 67d28d7276..72bfaedac6 100644 --- a/app/assets/javascripts/discourse/templates/components/user-stat.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-stat.hbs @@ -1,5 +1,5 @@ - {{#if icon}}{{fa-icon icon}}{{/if}} + {{#if icon}}{{d-icon icon}}{{/if}} {{number value}} {{{i18n label count=value}}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 9e20fd7330..9ddc7aeedb 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -22,10 +22,10 @@ {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}}
    - {{#if site.mobileView}} - - {{/if}} - + {{composer-toggles + composeState=model.composeState + toggleComposer=(action "toggle") + toggleToolbar=(action "toggleToolbar")}} {{#if model.viewOpen}}
    @@ -112,7 +112,9 @@ {{#if site.mobileView}} {{#if whisperOrUnlistTopic}} - + + {{d-icon "eye-slash"}} + {{/if}} {{/if}}
    @@ -130,7 +132,7 @@
    {{#if model.topic}} - {{fa-icon "mail-forward"}} {{{draftTitle}}} + {{d-icon "mail-forward"}} {{{draftTitle}}} {{else}} {{i18n "composer.saved_draft"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/composer/custom-body.hbs b/app/assets/javascripts/discourse/templates/composer/custom-body.hbs index 4b2be21c2d..60493d7526 100644 --- a/app/assets/javascripts/discourse/templates/composer/custom-body.hbs +++ b/app/assets/javascripts/discourse/templates/composer/custom-body.hbs @@ -1,3 +1,3 @@ -{{fa-icon "close"}} +{{d-icon "close"}} {{#if message.title}}

    {{message.title}}

    {{/if}}

    {{{message.body}}}

    diff --git a/app/assets/javascripts/discourse/templates/composer/education.hbs b/app/assets/javascripts/discourse/templates/composer/education.hbs index a461a9b512..c2d574cb69 100644 --- a/app/assets/javascripts/discourse/templates/composer/education.hbs +++ b/app/assets/javascripts/discourse/templates/composer/education.hbs @@ -1,2 +1,2 @@ -{{fa-icon "times"}} +{{d-icon "times"}} {{{message.body}}} diff --git a/app/assets/javascripts/discourse/templates/composer/group-mentioned.hbs b/app/assets/javascripts/discourse/templates/composer/group-mentioned.hbs index 8497ac5dbd..a91a313e0f 100644 --- a/app/assets/javascripts/discourse/templates/composer/group-mentioned.hbs +++ b/app/assets/javascripts/discourse/templates/composer/group-mentioned.hbs @@ -1,2 +1,2 @@ -{{fa-icon "close"}} +{{d-icon "close"}} {{{message.body}}} diff --git a/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs b/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs index 304352f549..bdd2cd5a3f 100644 --- a/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs +++ b/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs @@ -1,4 +1,4 @@ -{{fa-icon "close"}} +{{d-icon "close"}}

    {{i18n 'composer.similar_topics'}}

      diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 22e5eb3a60..fb1a079d26 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -8,7 +8,7 @@ {{/if}} {{#if showResetNew}} - + {{/if}}
    {{/if}} @@ -60,7 +60,8 @@ {{/if}} {{#if showResetNew}} - + {{/if}} {{#if latest}} diff --git a/app/assets/javascripts/discourse/templates/emoji-picker-recent.raw.hbs b/app/assets/javascripts/discourse/templates/emoji-picker-recent.raw.hbs new file mode 100644 index 0000000000..0ead5e5aee --- /dev/null +++ b/app/assets/javascripts/discourse/templates/emoji-picker-recent.raw.hbs @@ -0,0 +1,3 @@ +{{#each recentEmojis as |emoji|}} + +{{/each}} diff --git a/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb b/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb new file mode 100644 index 0000000000..e2570e95b0 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb @@ -0,0 +1,75 @@ +
    +
    + +
    + + <% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %> +
    + +
    + <% end %> + + <% if !Emoji.custom.blank? %> +
    + +
    + <% end %> +
    + +
    +
    + {{d-icon 'search'}} + + +
    + +
    + +
    +
    +
    + {{i18n 'emoji_picker.recent'}} + {{d-icon 'trash'}} +
    +
    +
    + + <% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %> +
    +
    + {{i18n 'emoji_picker.<%= group["name"] %>'}} +
    +
    + <% group["icons"].each do |icon| %> + + <% end %> +
    +
    + <% end %> + + {{#if customEmojis.length}} +
    +
    + {{i18n 'emoji_picker.custom'}} +
    +
    + {{#each customEmojis as |emoji|}} + + {{/each}} +
    +
    + {{/if}} +
    + +
    diff --git a/app/assets/javascripts/discourse/templates/emoji-toolbar.raw.hbs b/app/assets/javascripts/discourse/templates/emoji-toolbar.raw.hbs deleted file mode 100644 index fb684b57d6..0000000000 --- a/app/assets/javascripts/discourse/templates/emoji-toolbar.raw.hbs +++ /dev/null @@ -1,45 +0,0 @@ -
    -
      - {{#each toolbarItems as |item|}}
    • {{/each}} -
    -
    -
    {{#if log.prev_value}} {{#if expandDetails}} - {{fa-icon 'ellipsis-v'}} + {{d-icon 'ellipsis-v'}} {{else}} - {{fa-icon 'ellipsis-h'}} + {{d-icon 'ellipsis-h'}} {{/if}} {{/if}}
    - {{#each rows as |row|}} - - {{#each row as |item|}} - - {{/each}} - - {{/each}} -
    -
    - - - -
    - diff --git a/app/assets/javascripts/discourse/templates/exception.hbs b/app/assets/javascripts/discourse/templates/exception.hbs index 69537d6d70..2c6d59e487 100644 --- a/app/assets/javascripts/discourse/templates/exception.hbs +++ b/app/assets/javascripts/discourse/templates/exception.hbs @@ -7,7 +7,7 @@
    {{#if networkFixed}} - + {{d-icon "check-circle"}} {{/if}} {{desc}} diff --git a/app/assets/javascripts/discourse/templates/flat-button.raw.hbs b/app/assets/javascripts/discourse/templates/flat-button.raw.hbs new file mode 100644 index 0000000000..9b5f10eebc --- /dev/null +++ b/app/assets/javascripts/discourse/templates/flat-button.raw.hbs @@ -0,0 +1,3 @@ + diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index e3962efe12..712529c40e 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -45,32 +45,23 @@
    {{/if}} - {{#conditional-loading-spinner condition=loading}} - - {{#unless hasResults}} -

    - {{#if searchActive}} - {{i18n "search.no_results"}} - {{/if}} -

    - {{/unless}} - - {{#if hasResults}} -
    -
    - - {{{i18n "search.result_count" count=resultCount term=noSortQ}}} - -
    -
    - - {{i18n "search.sort_by"}} - - {{combo-box value=sortOrder content=sortOrders castInteger="true"}} -
    + {{#if hasResults}} +
    +
    + + {{{i18n "search.result_count" count=resultCount term=noSortQ}}} +
    - {{/if}} +
    + + {{i18n "search.sort_by"}} + + {{combo-box value=sortOrder content=sortOrders castInteger="true"}} +
    +
    + {{/if}} + {{#load-more selector=".fps-result" action="loadMore"}} {{#each model.posts as |result|}}
    @@ -116,7 +107,7 @@ {{#if showLikeCount}} {{#if result.like_count}} {{/if}} {{/if}} @@ -124,11 +115,30 @@
    {{/each}} - {{#if hasResults}} - - {{/if}} + {{#conditional-loading-spinner condition=loading }} + {{#unless hasResults}} +

    + {{#if searchActive}} + {{i18n "search.no_results"}} + {{/if}} +

    + {{/unless}} + + {{#if hasResults}} + {{#unless loading}} + + {{/unless}} + {{/if}} + {{/conditional-loading-spinner}} + + {{/load-more}} - {{/conditional-loading-spinner}} {{/d-section}} diff --git a/app/assets/javascripts/discourse/templates/group-edit.hbs b/app/assets/javascripts/discourse/templates/group-edit.hbs index 03122d2aba..d5552e5327 100644 --- a/app/assets/javascripts/discourse/templates/group-edit.hbs +++ b/app/assets/javascripts/discourse/templates/group-edit.hbs @@ -21,11 +21,22 @@
    +
    + +
    +
    @@ -34,7 +45,7 @@ {{input type='checkbox' checked=model.allow_membership_requests class="group-edit-allow-membership-requests" - disabled=model.public}} + disabled=model.public_admission}} {{i18n 'groups.allow_membership_requests'}} diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index 24afa5a0c7..aa34b15c1a 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -35,7 +35,7 @@ {{#each getTabs as |tab|}}
  • {{#link-to tab.location model title=tab.message}} - {{#if tab.icon}}{{fa-icon tab.icon}}{{/if}} + {{#if tab.icon}}{{d-icon tab.icon}}{{/if}} {{tab.message}} {{#if tab.count}}({{tab.count}}){{/if}} {{/link-to}} @@ -43,6 +43,14 @@ {{/each}} {{/mobile-nav}} + {{#if displayGroupMessageButton}} + {{d-button + action="messageGroup" + class="btn-primary group-message-button" + icon="envelope" + label="groups.message"}} + {{/if}} + {{group-membership-button model=model showLogin='showLogin'}}
  • diff --git a/app/assets/javascripts/discourse/templates/invites/show.hbs b/app/assets/javascripts/discourse/templates/invites/show.hbs index 69f3611a79..ade57c9d26 100644 --- a/app/assets/javascripts/discourse/templates/invites/show.hbs +++ b/app/assets/javascripts/discourse/templates/invites/show.hbs @@ -42,7 +42,8 @@  {{input-tip validation=passwordValidation}}
    {{passwordInstructions}} {{i18n 'invites.optional_description'}} -
    {{i18n 'login.caps_lock_warning'}}
    +
    + {{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}
    diff --git a/app/assets/javascripts/discourse/templates/list/action-list.raw.hbs b/app/assets/javascripts/discourse/templates/list/action-list.raw.hbs index 428a5c53a7..5714b98d83 100644 --- a/app/assets/javascripts/discourse/templates/list/action-list.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/action-list.raw.hbs @@ -1,6 +1,6 @@ {{#if postNumbers}}
    - + {{d-icon icon}} {{#each postNumbers as |postNumber|}} #{{postNumber}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/list/posts-count-column.raw.hbs b/app/assets/javascripts/discourse/templates/list/posts-count-column.raw.hbs index 599d695abf..94106df50d 100644 --- a/app/assets/javascripts/discourse/templates/list/posts-count-column.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/posts-count-column.raw.hbs @@ -1,3 +1,4 @@ <{{view.tagName}} class='num posts-map posts {{view.likesHeat}}' title='{{view.title}}'> + {{raw-plugin-outlet name="topic-list-before-reply-count"}} {{number topic.replyCount noTitle="true"}} diff --git a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs index d38f958ab3..3ad017f622 100644 --- a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs @@ -45,7 +45,7 @@ {{#if hasLikes}} - {{number topic.like_count}} + {{number topic.like_count}} {{d-icon "heart"}} {{/if}} {{/if}} @@ -54,7 +54,7 @@ {{#if hasOpLikes}} - {{number topic.op_like_count}} + {{number topic.op_like_count}} {{d-icon "heart"}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs b/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs index 8a58906d19..83bdf44a11 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/mobile-nav.hbs @@ -1,5 +1,7 @@ {{#if selectedHtml}} -
  • {{{selectedHtml}}}
  • +
  • {{{selectedHtml}}} + {{d-icon "caret-down"}} +
  • {{/if}}
    \ No newline at end of file + diff --git a/app/assets/javascripts/discourse/templates/tag-groups.hbs b/app/assets/javascripts/discourse/templates/tag-groups.hbs index 139874e75d..a4c0ec21de 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups.hbs @@ -7,10 +7,10 @@
  • {{tagGroup.name}}
  • {{/each}} - + {{outlet}}
    -
    \ No newline at end of file + diff --git a/app/assets/javascripts/discourse/templates/tags/show.hbs b/app/assets/javascripts/discourse/templates/tags/show.hbs index 5ad165da8c..18659601d1 100644 --- a/app/assets/javascripts/discourse/templates/tags/show.hbs +++ b/app/assets/javascripts/discourse/templates/tags/show.hbs @@ -16,7 +16,7 @@ {{d-button action="renameTag" actionParam=tag icon="pencil" class="admin-tag"}} {{else}} {{#if canCreateTopic}} - + {{/if}} {{/if}} @@ -30,7 +30,7 @@ {{else}}

    {{#link-to 'tags'}}{{i18n "tagging.tags"}}{{/link-to}} - {{fa-icon "angle-right"}} + {{d-icon "angle-right"}} {{discourse-tag-bound tagRecord=tag style="simple"}} {{#each additionalTags as |tag|}} & diff --git a/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs b/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs index 2c7ea0c947..762ed1e996 100644 --- a/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs +++ b/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs @@ -1,7 +1,7 @@ {{~#if canBulkSelect}} {{~#if showBulkToggle}} - + {{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}} {{/if ~}} {{~#if bulkSelectEnabled}} @@ -12,6 +12,6 @@ {{/if ~}} {{view.localizedName}} {{~#if view.isSorting}} - + {{d-icon sortIcon}} {{/if ~}} diff --git a/app/assets/javascripts/discourse/templates/topic-list-header.raw.hbs b/app/assets/javascripts/discourse/templates/topic-list-header.raw.hbs index 2faae1a5d6..9f4646b3ad 100644 --- a/app/assets/javascripts/discourse/templates/topic-list-header.raw.hbs +++ b/app/assets/javascripts/discourse/templates/topic-list-header.raw.hbs @@ -1,7 +1,7 @@ {{#if bulkSelectEnabled}} {{#if canBulkSelect}} - + {{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/topic-status.raw.hbs b/app/assets/javascripts/discourse/templates/topic-status.raw.hbs index 621415dfa1..cfcbe836cf 100644 --- a/app/assets/javascripts/discourse/templates/topic-status.raw.hbs +++ b/app/assets/javascripts/discourse/templates/topic-status.raw.hbs @@ -3,9 +3,9 @@ {{/if ~}} {{~#each view.statuses as |status|~}} {{~#if status.href ~}} - +{{d-icon status.icon}} {{~else ~}} -<{{status.openTag}} title='{{status.title}}' class='topic-status'> +<{{status.openTag}} title='{{status.title}}' class='topic-status'>{{d-icon status.icon}} {{~/if ~}} {{~/each}} {{~#if view.renderDiv ~}} diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 7cdd7568d9..db748edd81 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -10,10 +10,10 @@ {{#if model.postStream.loaded}} {{#if model.postStream.firstPostPresent}} - {{#topic-title cancelled="cancelEditingTopic" save="finishedEditingTopic"}} + {{#topic-title cancelled="cancelEditingTopic" save="finishedEditingTopic" model=model}} {{#if editingTopic}} {{#if model.isPrivateMessage}} - {{fa-icon "envelope"}} + {{d-icon "envelope"}} {{/if}} {{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}} @@ -36,7 +36,7 @@

    {{#unless model.is_warning}} - {{fa-icon "envelope"}} + {{d-icon "envelope"}} {{/unless}} @@ -48,7 +48,7 @@ {{/if}} {{#if model.details.can_edit}} - {{fa-icon "pencil"}} + {{d-icon "pencil"}} {{/if}}

    @@ -230,7 +230,7 @@ {{#if currentUser.show_queued_posts}} {{#link-to "queued-posts"}} - {{fa-icon "check"}} + {{d-icon "check"}} {{i18n "queue.view_pending"}} {{/link-to}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index 12a26aa3a0..fab58ece1f 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -17,7 +17,7 @@ {{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}} {{#if canBulkInvite}} {{csv-uploader uploading=uploading}} - {{fa-icon "question-circle"}} + {{d-icon "question-circle"}} {{/if}} {{#if showBulkActionButtons}} {{#if rescindedAll}} diff --git a/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs index 02a8b005dd..4c353fbd06 100644 --- a/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs @@ -13,7 +13,7 @@ {{#each options.groups as |group|}}
  • - + {{d-icon "users"}} {{group.name}} {{group.full_name}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 7970c3f9a0..5634f64102 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -39,13 +39,13 @@ {{#if model.can_send_private_message_to_user}}
  • - {{fa-icon "envelope"}} + {{d-icon "envelope"}} {{i18n 'user.private_message'}}
  • {{/if}} {{#if currentUser.staff}} -
  • {{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}
  • +
  • {{d-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}
  • {{/if}} {{plugin-outlet name="user-profile-controls" connectorTagName="li" @@ -53,7 +53,7 @@ {{#if collapsedInfo}} {{#if viewingSelf}} -
  • {{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}
  • +
  • {{d-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}
  • {{/if}} {{/if}} @@ -68,9 +68,9 @@ {{/if}} {{plugin-outlet name="user-post-names" args=(hash model=model)}}

    - {{#if model.location}}{{fa-icon "map-marker"}} {{model.location}}{{/if}} + {{#if model.location}}{{d-icon "map-marker"}} {{model.location}}{{/if}} {{#if model.website_name}} - {{fa-icon "globe"}} + {{d-icon "globe"}} {{#if linkWebsite}} {{model.website_name}} {{else}} @@ -83,7 +83,7 @@
    {{#if model.isSuspended}}
    - {{fa-icon "ban"}} + {{d-icon "ban"}} {{i18n 'user.suspended_notice' date=model.suspendedTillDate}}
    {{i18n 'user.suspended_reason'}} {{model.suspend_reason}}
    @@ -167,22 +167,22 @@ {{#if showNotificationsTab}}
  • {{#link-to 'userNotifications'}} - {{fa-icon "comment" class="glyph"}}{{i18n 'user.notifications'}} + {{d-icon "comment" class="glyph"}}{{i18n 'user.notifications'}} {{/link-to}}
  • {{/if}} {{#if showPrivateMessages}} -
  • {{#link-to 'userPrivateMessages'}}{{fa-icon "envelope-o"}}{{i18n 'user.private_messages'}}{{/link-to}}
  • +
  • {{#link-to 'userPrivateMessages'}}{{d-icon "envelope-o"}}{{i18n 'user.private_messages'}}{{/link-to}}
  • {{/if}} {{#if canInviteToForum}} -
  • {{#link-to 'userInvited'}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}
  • +
  • {{#link-to 'userInvited'}}{{d-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}
  • {{/if}} {{#if showBadges}} -
  • {{#link-to 'user.badges'}}{{fa-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}
  • +
  • {{#link-to 'user.badges'}}{{d-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}
  • {{/if}} {{plugin-outlet name="user-main-nav" connectorTagName='li' args=(hash model=model)}} {{#if model.can_edit}} -
  • {{#link-to 'preferences'}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}
  • +
  • {{#link-to 'preferences'}}{{d-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}
  • {{/if}} {{/mobile-nav}} diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs index b0fa84ba14..165e0cfbcf 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/templates/user/activity.hbs @@ -8,18 +8,18 @@
  • {{#link-to 'userActivity.replies'}} - {{i18n 'user_action_groups.5'}} + {{d-icon "reply" class="glyph"}}{{i18n 'user_action_groups.5'}} {{/link-to}}
  • {{#link-to 'userActivity.likesGiven'}} - {{i18n 'user_action_groups.1'}} + {{d-icon "heart" class="glyph"}}{{i18n 'user_action_groups.1'}} {{/link-to}}
  • {{#if showBookmarks}}
  • {{#link-to 'userActivity.bookmarks'}} - {{i18n 'user_action_groups.3'}} + {{d-icon "bookmark" class="glyph"}}{{i18n 'user_action_groups.3'}} {{/link-to}}
  • {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index ece0c407b7..3e19ee722a 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -26,7 +26,7 @@ {{#if group.has_messages}}
  • {{#link-to 'userPrivateMessages.group' group.name}} - + {{d-icon "group" class="glyph"}} {{capitalize-string group.name}} {{/link-to}}
  • @@ -44,7 +44,7 @@
    {{#if site.mobileView}} diff --git a/app/assets/javascripts/discourse/templates/user/notifications.hbs b/app/assets/javascripts/discourse/templates/user/notifications.hbs index ce0128519f..19e549d6c1 100644 --- a/app/assets/javascripts/discourse/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/user/notifications.hbs @@ -5,17 +5,17 @@
  • {{#link-to 'userNotifications.responses'}} - + {{d-icon "reply" class="glyph"}} {{i18n 'user_action_groups.6'}} {{/link-to}}
  • {{#link-to 'userNotifications.likesReceived'}} - {{i18n 'user_action_groups.2'}} + {{d-icon "heart" class="glyph"}}{{i18n 'user_action_groups.2'}} {{/link-to}}
  • -
  • {{#link-to 'userNotifications.mentions'}}{{i18n 'user_action_groups.7'}}{{/link-to}}
  • -
  • {{#link-to 'userNotifications.edits'}}{{i18n 'user_action_groups.11'}}{{/link-to}}
  • +
  • {{#link-to 'userNotifications.mentions'}}{{d-icon "at" class="glyph"}}{{i18n 'user_action_groups.7'}}{{/link-to}}
  • +
  • {{#link-to 'userNotifications.edits'}}{{d-icon "pencil" class="glyph"}}{{i18n 'user_action_groups.11'}}{{/link-to}}
  • {{/mobile-nav}} {{#if model}} diff --git a/app/assets/javascripts/discourse/templates/user/summary.hbs b/app/assets/javascripts/discourse/templates/user/summary.hbs index 19150c2be8..d28eef4a30 100644 --- a/app/assets/javascripts/discourse/templates/user/summary.hbs +++ b/app/assets/javascripts/discourse/templates/user/summary.hbs @@ -54,7 +54,7 @@ {{format-date reply.createdAt format="tiny" noTitle="true"}} {{#if reply.like_count}} · - {{fa-icon 'heart'}}  + {{d-icon 'heart'}}  {{/if}}
    @@ -79,7 +79,7 @@ {{format-date topic.createdAt format="tiny" noTitle="true"}} {{#if topic.like_count}} · - {{fa-icon 'heart'}}  + {{d-icon 'heart'}}  {{/if}}
    @@ -127,7 +127,7 @@ {{#each model.most_replied_to_users as |user|}}
  • {{#user-info user=user}} - {{fa-icon "reply"}} + {{d-icon "reply"}} {{number user.count}} {{/user-info}}
  • @@ -147,7 +147,7 @@ {{#each model.most_liked_by_users as |user|}}
  • {{#user-info user=user}} - {{fa-icon "heart"}} + {{d-icon "heart"}} {{/user-info}}
  • @@ -164,7 +164,7 @@ {{#each model.most_liked_users as |user|}}
  • {{#user-info user=user}} - {{fa-icon "heart"}} + {{d-icon "heart"}} {{/user-info}}
  • diff --git a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 index df852621b9..0e8212f35a 100644 --- a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 +++ b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 @@ -1,6 +1,6 @@ import { createWidget } from 'discourse/widgets/widget'; import { avatarFor } from 'discourse/widgets/post'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { h } from 'virtual-dom'; import { dateNode } from 'discourse/helpers/node'; import { userPath } from 'discourse/lib/url'; diff --git a/app/assets/javascripts/discourse/widgets/button.js.es6 b/app/assets/javascripts/discourse/widgets/button.js.es6 index f3893f1f69..ece396abaa 100644 --- a/app/assets/javascripts/discourse/widgets/button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/button.js.es6 @@ -1,7 +1,8 @@ import { createWidget } from 'discourse/widgets/widget'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; -export default createWidget('button', { + +const ButtonClass = { tagName: 'button.widget-button', buildClasses(attrs) { @@ -59,4 +60,10 @@ export default createWidget('button', { } return this.sendWidgetAction(attrs.action); } -}); +}; + +export default createWidget('button', ButtonClass); + +createWidget('flat-button', jQuery.extend(ButtonClass, { + tagName: 'button.widget-button.btn-flat' +})); diff --git a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 index 6f572388ab..11036d8541 100644 --- a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 @@ -2,7 +2,7 @@ import PostCooked from 'discourse/widgets/post-cooked'; import DecoratorHelper from 'discourse/widgets/decorator-helper'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import DiscourseURL from 'discourse/lib/url'; createWidget('post-link-arrow', { diff --git a/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 b/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 index 8a083df660..65ae654c5d 100644 --- a/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 @@ -1,6 +1,6 @@ import { applyDecorators, createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import DiscourseURL from 'discourse/lib/url'; import RawHtml from 'discourse/widgets/raw-html'; import renderTags from 'discourse/lib/render-tags'; diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 0dc7716211..600bd75308 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -1,9 +1,10 @@ import { createWidget } from 'discourse/widgets/widget'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { avatarImg } from 'discourse/widgets/post'; import DiscourseURL from 'discourse/lib/url'; import { wantsNewWindow } from 'discourse/lib/intercept-click'; import { applySearchAutocomplete } from "discourse/lib/search"; +import { ajax } from 'discourse/lib/ajax'; import { h } from 'virtual-dom'; @@ -249,7 +250,29 @@ export default createWidget('header', { }, linkClickedEvent(attrs) { - if (!(attrs && attrs.searchContextEnabled)) this.closeAll(); + + let searchContextEnabled = false; + if (attrs) { + searchContextEnabled = attrs.searchContextEnabled; + + const { searchLogId, searchResultId, searchResultType } = attrs; + if (searchLogId && searchResultId && searchResultType) { + + ajax('/search/click', { + type: 'POST', + data: { + search_log_id: searchLogId, + search_result_id: searchResultId, + search_result_type: searchResultType + } + }); + } + } + + if (!searchContextEnabled) { + this.closeAll(); + } + this.updateHighlight(); }, @@ -306,8 +329,12 @@ export default createWidget('header', { // If we're viewing a topic, only intercept search if there are cloaked posts if (showSearch && currentPath.match(/^topic\./)) { - showSearch = ($('.topic-post .cooked, .small-action:not(.time-gap)').length < - this.register.lookup('controller:topic').get('model.postStream.stream.length')); + const controller = this.register.lookup('controller:topic'); + const total = controller.get('model.postStream.stream.length') || 0; + const chunkSize = controller.get('model.chunk_size') || 0; + + showSearch = (total > chunkSize) && + $('.topic-post .cooked, .small-action:not(.time-gap)').length < total; } if (state.searchVisible) { diff --git a/app/assets/javascripts/discourse/widgets/home-logo.js.es6 b/app/assets/javascripts/discourse/widgets/home-logo.js.es6 index ad4c59b26a..c226d944eb 100644 --- a/app/assets/javascripts/discourse/widgets/home-logo.js.es6 +++ b/app/assets/javascripts/discourse/widgets/home-logo.js.es6 @@ -1,6 +1,6 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { wantsNewWindow } from 'discourse/lib/intercept-click'; import DiscourseURL from 'discourse/lib/url'; diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 index 4f6beef6bf..c6426a8623 100644 --- a/app/assets/javascripts/discourse/widgets/link.js.es6 +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -1,6 +1,6 @@ import { wantsNewWindow } from 'discourse/lib/intercept-click'; import { createWidget } from 'discourse/widgets/widget'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { h } from 'virtual-dom'; import DiscourseURL from 'discourse/lib/url'; diff --git a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 index eb868b0343..4fb293745a 100644 --- a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 @@ -1,4 +1,4 @@ -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 index 2786f7df73..11eaf277dc 100644 --- a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 @@ -1,3 +1,4 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; import { ajax } from 'discourse/lib/ajax'; import { isValidLink } from 'discourse/lib/click-track'; import { number } from 'discourse/lib/formatter'; @@ -140,7 +141,7 @@ export default class PostCooked { $blockQuote.showHtml(div, 'fast', finished); }).catch((e) => { if (e.jqXHR.status === 404) { - $blockQuote.showHtml($("
    "), 'fast', finished); + $blockQuote.showHtml($(`
    ${iconHTML('trash-o')}
    `), 'fast', finished); } }); } else { @@ -178,7 +179,7 @@ export default class PostCooked { // Only add the expand/contract control if it's not a full post let expandContract = ""; if (!$aside.data('full')) { - expandContract = ``; + expandContract = iconHTML(desc, { title: "post.expand_collapse" }); $('.title', $aside).css('cursor', 'pointer'); } $('.quote-controls', $aside).html(expandContract + navLink); diff --git a/app/assets/javascripts/discourse/widgets/post-edits-indicator.js.es6 b/app/assets/javascripts/discourse/widgets/post-edits-indicator.js.es6 index 531e439685..7d8f492807 100644 --- a/app/assets/javascripts/discourse/widgets/post-edits-indicator.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-edits-indicator.js.es6 @@ -1,5 +1,5 @@ import { createWidget } from 'discourse/widgets/widget'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { longDate } from 'discourse/lib/formatter'; import { h } from 'virtual-dom'; diff --git a/app/assets/javascripts/discourse/widgets/post-links.js.es6 b/app/assets/javascripts/discourse/widgets/post-links.js.es6 index 9182191451..69340bc86b 100644 --- a/app/assets/javascripts/discourse/widgets/post-links.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-links.js.es6 @@ -1,4 +1,4 @@ -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { replaceEmoji } from 'discourse/widgets/emoji'; diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index 06c30eb7dc..b7f97b697e 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -230,7 +230,7 @@ export default createWidget('post-menu', { if (builder) { const buttonAtts = builder(attrs, this.state, this.siteSettings); if (buttonAtts) { - return this.attach('button', buttonAtts); + return this.attach('flat-button', buttonAtts); } } }, @@ -360,7 +360,7 @@ export default createWidget('post-menu', { return this.sendWidgetAction('toggleLike'); } - const $heart = $(`[data-post-id=${attrs.id}] .fa-heart`); + const $heart = $(`[data-post-id=${attrs.id}] .d-icon-heart`); $heart.closest('button').addClass('has-like'); if (!Ember.testing) { diff --git a/app/assets/javascripts/discourse/widgets/post-small-action.js.es6 b/app/assets/javascripts/discourse/widgets/post-small-action.js.es6 index b128b246ce..680a625911 100644 --- a/app/assets/javascripts/discourse/widgets/post-small-action.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-small-action.js.es6 @@ -1,6 +1,6 @@ import { createWidget } from 'discourse/widgets/widget'; import RawHtml from 'discourse/widgets/raw-html'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { h } from 'virtual-dom'; import { actionDescriptionHtml } from 'discourse/components/small-action'; import { avatarFor } from 'discourse/widgets/post'; diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index c6c9f581f4..016a2c4030 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -1,7 +1,7 @@ import PostCooked from 'discourse/widgets/post-cooked'; import DecoratorHelper from 'discourse/widgets/decorator-helper'; import { createWidget, applyDecorators } from 'discourse/widgets/widget'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { transformBasicPost } from 'discourse/lib/transform-post'; import { h } from 'virtual-dom'; import DiscourseURL from 'discourse/lib/url'; @@ -88,7 +88,7 @@ createWidget('post-avatar', { html(attrs) { let body; if (!attrs.user_id) { - body = h('i', { className: 'fa fa-trash-o deleted-user-avatar' }); + body = iconNode('trash-o', { class: 'deleted-user-avatar' }); } else { body = avatarFor.call(this, this.settings.size, { template: attrs.avatar_template, diff --git a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 index f514b9ac8b..2bf64d738d 100644 --- a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 +++ b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 @@ -1,4 +1,4 @@ -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index 1c43153798..60441867e1 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -1,4 +1,4 @@ -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { avatarFor } from 'discourse/widgets/post'; diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index 41216e414e..c195c962f6 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -3,7 +3,8 @@ import { dateNode } from 'discourse/helpers/node'; import RawHtml from 'discourse/widgets/raw-html'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; +import highlightText from 'discourse/lib/highlight-text'; class Highlighted extends RawHtml { constructor(html, term) { @@ -12,26 +13,30 @@ class Highlighted extends RawHtml { } decorate($html) { - if (this.term) { - // special case ignore "l" which is used for magic sorting - const words = _.reject(this.term.split(/\s+/), t => t === 'l'); - $html.highlight(words, { className: 'search-highlight' }); - } + highlightText($html, this.term); } } -function createSearchResult(type, linkField, fn) { +function createSearchResult({ type, linkField, builder }) { return createWidget(`search-result-${type}`, { html(attrs) { return attrs.results.map(r => { + + let searchResultId; + if (type === "topic") { + searchResultId = r.get('topic_id'); + } return h('li', this.attach('link', { href: r.get(linkField), - contents: () => fn.call(this, r, attrs.term), + contents: () => builder.call(this, r, attrs.term), className: 'search-link', - searchContextEnabled: this.attrs.searchContextEnabled + searchResultId, + searchResultType: type, + searchContextEnabled: attrs.searchContextEnabled, + searchLogId: attrs.searchLogId })); }); - } + }, }); } @@ -47,27 +52,43 @@ function postResult(result, link, term) { return html; } -createSearchResult('user', 'path', function(u) { - return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', h('span.user-results', h('b', u.username)), ' ', h('span.user-results', u.name ? u.name : '') ]; +createSearchResult({ + type: 'user', + linkField: 'path', + builder(u) { + return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', h('span.user-results', h('b', u.username)), ' ', h('span.user-results', u.name ? u.name : '') ]; + } }); -createSearchResult('topic', 'url', function(result, term) { - const topic = result.topic; - const link = h('span.topic', [ - this.attach('topic-status', { topic, disableActions: true }), - h('span.topic-title', new Highlighted(topic.get('fancyTitle'), term)), - this.attach('category-link', { category: topic.get('category'), link: false }) - ]); +createSearchResult({ + type: 'topic', + linkField: 'url', + builder(result, term) { + const topic = result.topic; + const link = h('span.topic', [ + this.attach('topic-status', { topic, disableActions: true }), + h('span.topic-title', new Highlighted(topic.get('fancyTitle'), term)), + this.attach('category-link', { category: topic.get('category'), link: false }) + ]); - return postResult.call(this, result, link, term); + return postResult.call(this, result, link, term); + } }); -createSearchResult('post', 'url', function(result, term) { - return postResult.call(this, result, I18n.t('search.post_format', result), term); +createSearchResult({ + type: 'post', + linkField: 'url', + builder(result, term) { + return postResult.call(this, result, I18n.t('search.post_format', result), term); + } }); -createSearchResult('category', 'url', function (c) { - return this.attach('category-link', { category: c, link: false }); +createSearchResult({ + type: 'category', + linkField: 'url', + builder(c) { + return this.attach('category-link', { category: c, link: false }); + } }); createWidget('search-menu-results', { @@ -102,7 +123,8 @@ createWidget('search-menu-results', { return [ h('ul', this.attach(rt.componentName, { - searchContextEnabled: this.attrs.searchContextEnabled, + searchContextEnabled: attrs.searchContextEnabled, + searchLogId: attrs.results.grouped_search_result.search_log_id, results: rt.results, term: attrs.term })), diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index ba68383e59..d5e9b12e9a 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -127,11 +127,13 @@ export default createWidget('search-menu', { if (searchData.loading) { results.push(h('div.searching', h('div.spinner'))); } else { - results.push(this.attach('search-menu-results', { term: searchData.term, - noResults: searchData.noResults, - results: searchData.results, - invalidTerm: searchData.invalidTerm, - searchContextEnabled: searchData.contextEnabled })); + results.push(this.attach('search-menu-results', { + term: searchData.term, + noResults: searchData.noResults, + results: searchData.results, + invalidTerm: searchData.invalidTerm, + searchContextEnabled: searchData.contextEnabled + })); } } diff --git a/app/assets/javascripts/discourse/widgets/time-gap.js.es6 b/app/assets/javascripts/discourse/widgets/time-gap.js.es6 index f2ce1cfdb9..30cfd24483 100644 --- a/app/assets/javascripts/discourse/widgets/time-gap.js.es6 +++ b/app/assets/javascripts/discourse/widgets/time-gap.js.es6 @@ -1,6 +1,6 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; function description(attrs) { const daysSince = attrs.daysSince; diff --git a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 index 75fad312f4..64ca6395c0 100644 --- a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 @@ -32,7 +32,7 @@ createWidget('topic-admin-menu-button', { // We don't show the button when expanded on the right side if (!(attrs.rightSide && state.expanded)) { result.push(this.attach('button', { - className: 'btn ' + (attrs.fixed ? " show-topic-admin" : ""), + className: 'btn toggle-admin-menu' + (attrs.fixed ? " show-topic-admin" : ""), title: 'topic_admin_menu', icon: 'wrench', action: 'showAdminMenu', diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index c489eb7ec2..6e77bd18e4 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -17,11 +17,16 @@ function renderParticipants(userFilters, participants) { createWidget('topic-map-show-links', { tagName: 'div.link-summary', - html(attrs) { - return h('a', I18n.t('topic_map.links_shown', { totalLinks: attrs.totalLinks })); + html() { + return h('span', this.attach('button', { + title: 'topic_map.links_shown', + icon: 'chevron-down', + action: 'showLinks', + className: 'btn' + })); }, - click() { + showLinks() { this.sendWidgetAction('showAllLinks'); } }); @@ -181,7 +186,7 @@ createWidget('topic-map-expanded', { ]; if (!state.allLinksShown && links.length < attrs.topicLinks.length) { - showAllLinksContent.push(this.attach('topic-map-show-links', { totalLinks: attrs.topicLinks.length })); + showAllLinksContent.push(this.attach('topic-map-show-links')); } const section = h('section.links', showAllLinksContent); diff --git a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 index bd37c8b4d8..6dddbb2272 100644 --- a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 @@ -2,6 +2,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { topicLevels, buttonDetails } from 'discourse/lib/notification-levels'; import { h } from 'virtual-dom'; import RawHTML from 'discourse/widgets/raw-html'; +import { iconNode } from 'discourse-common/lib/icon-library'; createWidget('notification-option', { buildKey: attrs => `topic-notifications-button-${attrs.id}`, @@ -9,7 +10,7 @@ createWidget('notification-option', { html(attrs) { return h('a', [ - h('span.icon', { className: `fa fa-${attrs.icon} ${attrs.key}`}), + iconNode(attrs.icon, { class: `icon ${attrs.key}`, tagName: 'span' }), h('div', [ h('span.title', I18n.t(`topic.notifications.${attrs.key}.title`)), h('span.desc', I18n.t(`topic.notifications.${attrs.key}.description`)), @@ -42,7 +43,7 @@ export default createWidget('topic-notifications-button', { const details = buttonDetails(level); const button = { - className: `btn`, + className: `btn toggle-notification-options`, label: null, icon: details.icon, action: 'toggleDropdown', @@ -52,7 +53,7 @@ export default createWidget('topic-notifications-button', { if (this.attrs.showFullTitle) { button.label = `topic.notifications.${details.key}.title`; } else { - button.className = 'btn notifications-dropdown'; + button.className = 'btn toggle-notifications-options notifications-dropdown'; } return this.attach('button', button); diff --git a/app/assets/javascripts/discourse/widgets/topic-status.js.es6 b/app/assets/javascripts/discourse/widgets/topic-status.js.es6 index d0b8ac5ef6..763ce7e216 100644 --- a/app/assets/javascripts/discourse/widgets/topic-status.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-status.js.es6 @@ -1,5 +1,5 @@ import { createWidget } from 'discourse/widgets/widget'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import { h } from 'virtual-dom'; import { escapeExpression } from 'discourse/lib/utilities'; diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index f71e7b892d..bccb90f80f 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -1,7 +1,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { relativeAge } from 'discourse/lib/formatter'; -import { iconNode } from 'discourse/helpers/fa-icon-node'; +import { iconNode } from 'discourse-common/lib/icon-library'; import RawHtml from 'discourse/widgets/raw-html'; const SCROLLAREA_HEIGHT = 300; diff --git a/app/assets/javascripts/markdown-it-bundle.js b/app/assets/javascripts/markdown-it-bundle.js index 51fe107f81..81c05f719f 100644 --- a/app/assets/javascripts/markdown-it-bundle.js +++ b/app/assets/javascripts/markdown-it-bundle.js @@ -1,14 +1,16 @@ //= require markdown-it.js -//= require ./pretty-text/engines/markdown-it/helpers -//= require ./pretty-text/engines/markdown-it/mentions -//= require ./pretty-text/engines/markdown-it/quotes -//= require ./pretty-text/engines/markdown-it/emoji -//= require ./pretty-text/engines/markdown-it/onebox -//= require ./pretty-text/engines/markdown-it/bbcode-block -//= require ./pretty-text/engines/markdown-it/bbcode-inline -//= require ./pretty-text/engines/markdown-it/code -//= require ./pretty-text/engines/markdown-it/category-hashtag -//= require ./pretty-text/engines/markdown-it/censored -//= require ./pretty-text/engines/markdown-it/table -//= require ./pretty-text/engines/markdown-it/paragraph -//= require ./pretty-text/engines/markdown-it/newline +//= require ./pretty-text/engines/discourse-markdown/helpers +//= require ./pretty-text/engines/discourse-markdown/mentions +//= require ./pretty-text/engines/discourse-markdown/quotes +//= require ./pretty-text/engines/discourse-markdown/emoji +//= require ./pretty-text/engines/discourse-markdown/onebox +//= require ./pretty-text/engines/discourse-markdown/bbcode-block +//= require ./pretty-text/engines/discourse-markdown/bbcode-inline +//= require ./pretty-text/engines/discourse-markdown/code +//= require ./pretty-text/engines/discourse-markdown/category-hashtag +//= require ./pretty-text/engines/discourse-markdown/censored +//= require ./pretty-text/engines/discourse-markdown/table +//= require ./pretty-text/engines/discourse-markdown/paragraph +//= require ./pretty-text/engines/discourse-markdown/newline +//= require ./pretty-text/engines/discourse-markdown/html-img +//= require ./pretty-text/engines/discourse-markdown/text-post-process diff --git a/app/assets/javascripts/pretty-text-bundle.js b/app/assets/javascripts/pretty-text-bundle.js index 691fad8e9a..a572344df8 100644 --- a/app/assets/javascripts/pretty-text-bundle.js +++ b/app/assets/javascripts/pretty-text-bundle.js @@ -3,12 +3,10 @@ //= require ./pretty-text/censored-words //= require ./pretty-text/emoji/data //= require ./pretty-text/emoji -//= require ./pretty-text/engines/discourse-markdown //= require ./pretty-text/engines/discourse-markdown-it -//= require_tree ./pretty-text/engines/discourse-markdown //= require xss.min -//= require better_markdown.js //= require ./pretty-text/xss //= require ./pretty-text/white-lister //= require ./pretty-text/sanitizer //= require ./pretty-text/oneboxer +//= require ./pretty-text/inline-oneboxer diff --git a/app/assets/javascripts/pretty-text/emoji.js.es6 b/app/assets/javascripts/pretty-text/emoji.js.es6.erb similarity index 88% rename from app/assets/javascripts/pretty-text/emoji.js.es6 rename to app/assets/javascripts/pretty-text/emoji.js.es6.erb index 1675e6e5e2..136c290426 100644 --- a/app/assets/javascripts/pretty-text/emoji.js.es6 +++ b/app/assets/javascripts/pretty-text/emoji.js.es6.erb @@ -1,7 +1,7 @@ import { emojis, aliases, translations, tonableEmojis } from 'pretty-text/emoji/data'; // bump up this number to expire all emojis -export const IMAGE_VERSION = "5"; +export const IMAGE_VERSION = "<%= Emoji::EMOJI_VERSION %>"; const extendedEmoji = {}; @@ -10,10 +10,8 @@ export function registerEmoji(code, url) { extendedEmoji[code] = url; } -export function emojiList() { - const result = emojis.slice(0); - _.each(extendedEmoji, (v,k) => result.push(k)); - return result; +export function extendedEmojiList() { + return extendedEmoji; } const emojiHash = {}; @@ -54,8 +52,7 @@ export function isCustomEmoji(code, opts) { export function buildEmojiUrl(code, opts) { let url; - code = code.toLowerCase(); - + code = String(code).toLowerCase(); if (extendedEmoji.hasOwnProperty(code)) { url = extendedEmoji[code]; } @@ -64,7 +61,7 @@ export function buildEmojiUrl(code, opts) { url = opts.customEmoji[code]; } - const noToneMatch = code.match(/(.?[\w-]*)?:?/); + const noToneMatch = code.match(/([^:]+):?/); if (noToneMatch && !url && (emojiHash.hasOwnProperty(noToneMatch[1]) || aliasHash.hasOwnProperty(noToneMatch[1]))) { url = opts.getURL(`/images/emoji/${opts.emojiSet}/${code.replace(/:t/, '/')}.png`); } @@ -116,10 +113,9 @@ export function emojiSearch(term, options) { }; export function isSkinTonableEmoji(term) { - let match = term.match(/^:?(.*?):?$/); + const match = _.compact(term.split(":"))[0]; if (match) { - return tonableEmojis.indexOf(match[1]) !== -1; - } else { - return tonableEmojis.indexOf(term) !== -1; + return tonableEmojis.indexOf(match) !== -1; } + return false; } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 index 9b9a971828..1174be579c 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 @@ -1,4 +1,4 @@ -import { default as WhiteLister, whiteListFeature } from 'pretty-text/white-lister'; +import { default as WhiteLister } from 'pretty-text/white-lister'; import { sanitize } from 'pretty-text/sanitizer'; import guid from 'pretty-text/guid'; @@ -10,10 +10,10 @@ function deprecate(feature, name){ }; } -function createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions) { +function createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions, whiteListed) { let helper = {}; helper.markdownIt = true; - helper.whiteList = info => whiteListFeature(featureName, info); + helper.whiteList = info => whiteListed.push([featureName, info]); helper.registerInline = deprecate(featureName,'registerInline'); helper.replaceBlock = deprecate(featureName,'replaceBlock'); helper.addPreProcessor = deprecate(featureName,'addPreProcessor'); @@ -71,11 +71,16 @@ class Ruler { // block bb code ruler for parsing of quotes / code / polls function setupBlockBBCode(md) { - md.block.bbcode_ruler = new Ruler(); + md.block.bbcode = { ruler: new Ruler() }; } function setupInlineBBCode(md) { - md.inline.bbcode_ruler = new Ruler(); + md.inline.bbcode = { ruler: new Ruler() }; +} + +function setupTextPostProcessRuler(md) { + const TextPostProcessRuler = requirejs('pretty-text/engines/discourse-markdown/text-post-process').TextPostProcessRuler; + md.core.textPostProcess = { ruler: new TextPostProcessRuler() }; } function renderHoisted(tokens, idx, options) { @@ -99,11 +104,60 @@ function setupHoister(md) { md.renderer.rules.html_raw = renderHoisted; } +const IMG_SIZE_REGEX = /^([1-9]+[0-9]*)x([1-9]+[0-9]*)(\s*,\s*([1-9][0-9]?)%)?$/; +function renderImage(tokens, idx, options, env, slf) { + var token = tokens[idx]; + + let alt = slf.renderInlineAsText(token.children, options, env); + + let split = alt.split('|'); + if (split.length > 1) { + let match; + let info = split.splice(split.length-1)[0]; + + if (match = info.match(IMG_SIZE_REGEX)) { + if (match[1] && match[2]) { + alt = split.join('|'); + + let width = match[1]; + let height = match[2]; + + if (match[4]) { + let percent = parseFloat(match[4]) / 100.0; + width = parseInt(width * percent); + height = parseInt(height * percent); + } + + if (token.attrIndex('width') === -1) { + token.attrs.push(['width', width]); + } + + if (token.attrIndex('height') === -1) { + token.attrs.push(['height', height]); + } + } + + } + } + + token.attrs[token.attrIndex('alt')][1] = alt; + return slf.renderToken(tokens, idx, options); +} + +function setupImageDimensions(md) { + md.renderer.rules.image = renderImage; +} + +let Helpers; + export function setup(opts, siteSettings, state) { if (opts.setup) { return; } + // we got to require this late cause bundle is not loaded in pretty-text + Helpers = Helpers || requirejs('pretty-text/engines/discourse-markdown/helpers'); + opts.markdownIt = true; let optionCallbacks = []; @@ -116,6 +170,7 @@ export function setup(opts, siteSettings, state) { const check = /discourse-markdown\/|markdown-it\//; let features = []; + let whiteListed = []; Object.keys(require._eak_seen).forEach(entry => { if (check.test(entry)) { @@ -124,7 +179,7 @@ export function setup(opts, siteSettings, state) { const featureName = entry.split('/').reverse()[0]; features.push(featureName); - module.setup(createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions)); + module.setup(createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions, whiteListed)); } } }); @@ -146,6 +201,10 @@ export function setup(opts, siteSettings, state) { delete opts[entry]; }); + copy.helpers = { + textReplace: Helpers.textReplace + }; + opts.discourse = copy; getOptions.f = () => opts.discourse; @@ -154,14 +213,16 @@ export function setup(opts, siteSettings, state) { html: true, breaks: opts.discourse.features.newline, xhtmlOut: false, - linkify: true, + linkify: opts.discourse.features.linkify, typographer: siteSettings.enable_markdown_typographer }); setupUrlDecoding(opts.engine); setupHoister(opts.engine); + setupImageDimensions(opts.engine); setupBlockBBCode(opts.engine); setupInlineBBCode(opts.engine); + setupTextPostProcessRuler(opts.engine); pluginCallbacks.forEach(([feature, callback])=>{ if (opts.discourse.features[feature]) { @@ -173,10 +234,16 @@ export function setup(opts, siteSettings, state) { opts.markdownIt = true; opts.setup = true; - if (!opts.discourse.sanitizer) { + if (!opts.discourse.sanitizer || !opts.sanitizer) { const whiteLister = new WhiteLister(opts.discourse); + + whiteListed.forEach(([feature, info]) => { + whiteLister.whiteListFeature(feature, info); + }); + opts.sanitizer = opts.discourse.sanitizer = (!!opts.discourse.sanitize) ? a=>sanitize(a, whiteLister) : a=>a; } + } export function cook(raw, opts) { diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6 deleted file mode 100644 index 3303cc34b9..0000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6 +++ /dev/null @@ -1,597 +0,0 @@ -import guid from 'pretty-text/guid'; -import { default as WhiteLister, whiteListFeature } from 'pretty-text/white-lister'; -import { escape } from 'pretty-text/sanitizer'; - -var parser = window.BetterMarkdown, - MD = parser.Markdown, - DialectHelpers = parser.DialectHelpers, - hoisted; - -let currentOpts; - -const emitters = []; -const preProcessors = []; -const parseNodes = []; - -function findEndPos(text, start, stop, args, offset) { - let endPos, nextStart; - do { - endPos = text.indexOf(stop, offset); - if (endPos === -1) { return -1; } - nextStart = text.indexOf(start, offset); - offset = endPos + stop.length; - } while (nextStart !== -1 && nextStart < endPos); - return endPos; -} - -class DialectHelper { - constructor() { - this._dialect = MD.dialects.Discourse = DialectHelpers.subclassDialect(MD.dialects.Gruber); - this._setup = false; - } - - escape(str) { - return escape(str); - } - - getOptions() { - return currentOpts; - } - - registerInlineFeature(featureName, start, fn) { - this._dialect.inline[start] = function() { - if (!currentOpts.features[featureName]) { return; } - return fn.apply(this, arguments); - }; - } - - addPreProcessorFeature(featureName, fn) { - preProcessors.push(raw => { - if (!currentOpts.features[featureName]) { return raw; } - return fn(raw, hoister); - }); - } - - /** - The simplest kind of replacement possible. Replace a stirng token with JsonML. - - For example to replace all occurrances of :) with a smile image: - - ```javascript - helper.inlineReplace(':)', text => ['img', {src: '/images/smile.png'}]); - ``` - **/ - inlineReplaceFeature(featureName, token, emitter) { - this.registerInline(token, (text, match, prev) => { - if (!currentOpts.features[featureName]) { return; } - return [token.length, emitter.call(this, token, match, prev)]; - }); - } - - /** - After the parser has been executed, change the contents of a HTML tag. - - Let's say you want to replace the contents of all code tags to prepend - "EVIL TROUT HACKED YOUR CODE!": - - ```javascript - helper.postProcessTag('code', contents => `EVIL TROUT HACKED YOUR CODE!\n\n${contents}`); - ``` - **/ - postProcessTagFeature(featureName, tag, emitter) { - this.onParseNode(event => { - if (!currentOpts.features[featureName]) { return; } - const node = event.node; - if (node[0] === tag) { - node[node.length-1] = emitter(node[node.length-1]); - } - }); - } - - /** - Matches inline using a regular expression. The emitter function is passed - the matches from the regular expression. - - For example, this auto links URLs: - - ```javascript - helper.inlineRegexp({ - matcher: /((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm, - spaceBoundary: true, - start: 'http', - - emitter(matches) { - const url = matches[1]; - return ['a', {href: url}, url]; - } - }); - ``` - **/ - inlineRegexpFeature(featureName, args) { - this.registerInline(args.start, function(text, match, prev) { - if (!currentOpts.features[featureName]) { return; } - if (invalidBoundary(args, prev)) { return; } - - args.matcher.lastIndex = 0; - const m = args.matcher.exec(text); - if (m) { - const result = args.emitter.call(this, m); - if (result) { - return [m[0].length, result]; - } - } - }); - } - - /** - Handles inline replacements surrounded by tokens. - - For example, to handle markdown style bold. Note we use `concat` on the array because - the contents are JsonML too since we didn't pass `rawContents` as true. This supports - recursive markup. - - ```javascript - helper.inlineBetween({ - between: '**', - wordBoundary: true. - emitter(contents) { - return ['strong'].concat(contents); - } - }); - ``` - **/ - inlineBetweenFeature(featureName, args) { - const start = args.start || args.between; - const stop = args.stop || args.between; - const startLength = start.length; - - this.registerInline(start, function(text, match, prev) { - if (!currentOpts.features[featureName]) { return; } - if (invalidBoundary(args, prev)) { return; } - - const endPos = findEndPos(text, start, stop, args, startLength); - if (endPos === -1) { return; } - var between = text.slice(startLength, endPos); - - // If rawcontents is set, don't process inline - if (!args.rawContents) { - between = this.processInline(between); - } - - var contents = args.emitter.call(this, between); - if (contents) { - return [endPos+stop.length, contents]; - } - }); - } - - /** - Replaces a block of text between a start and stop. As opposed to inline, these - might span multiple lines. - - Here's an example that takes the content between `[code]` ... `[/code]` and - puts them inside a `pre` tag: - - ```javascript - helper.replaceBlock({ - start: /(\[code\])([\s\S]*)/igm, - stop: '[/code]', - rawContents: true, - - emitter(blockContents) { - return ['p', ['pre'].concat(blockContents)]; - } - }); - ``` - **/ - replaceBlockFeature(featureName, args) { - function blockFunc(block, next) { - if (!currentOpts.features[featureName]) { return; } - - const linebreaks = currentOpts.traditionalMarkdownLinebreaks; - if (linebreaks && args.skipIfTradtionalLinebreaks) { return; } - - args.start.lastIndex = 0; - const result = []; - const match = (args.start).exec(block); - if (!match) { return; } - - const lastChance = () => !next.some(blk => blk.match(args.stop)); - - // shave off start tag and leading text, if any. - const pos = args.start.lastIndex - match[0].length; - const leading = block.slice(0, pos); - const trailing = match[2] ? match[2].replace(/^\n*/, "") : ""; - - // The other leading block should be processed first! eg a code block wrapped around a code block. - if (args.withoutLeading && args.withoutLeading.test(leading)) { - return; - } - - // just give up if there's no stop tag in this or any next block - args.stop.lastIndex = block.length - trailing.length; - if (!args.stop.exec(block) && lastChance()) { return; } - if (leading.length > 0) { - var parsedLeading = this.processBlock(MD.mk_block(leading), []); - if (parsedLeading && parsedLeading[0]) { - result.push(parsedLeading[0]); - } - } - if (trailing.length > 0) { - next.unshift(MD.mk_block(trailing, block.trailing, - block.lineNumber + countLines(leading) + (match[2] ? match[2].length : 0) - trailing.length)); - } - - // go through the available blocks to find the matching stop tag. - const contentBlocks = []; - let nesting = 0; - let actualEndPos = -1; - let currentBlock; - - blockloop: - while (currentBlock = next.shift()) { - - // collect all the start and stop tags in the current block - args.start.lastIndex = 0; - const startPos = []; - let m; - while (m = (args.start).exec(currentBlock)) { - startPos.push(args.start.lastIndex - m[0].length); - args.start.lastIndex = args.start.lastIndex - (m[2] ? m[2].length : 0); - } - args.stop.lastIndex = 0; - const endPos = []; - while (m = (args.stop).exec(currentBlock)) { - endPos.push(args.stop.lastIndex - m[0].length); - } - - // go through the available end tags: - let ep = 0; - let sp = 0; - while (ep < endPos.length) { - if (sp < startPos.length && startPos[sp] < endPos[ep]) { - // there's an end tag, but there's also another start tag first. we need to go deeper. - sp++; nesting++; - } else if (nesting > 0) { - // found an end tag, but we must go up a level first. - ep++; nesting--; - } else { - // found an end tag and we're at the top: done! -- or: start tag and end tag are - // identical, (i.e. startPos[sp] == endPos[ep]), so we don't do nesting at all. - actualEndPos = endPos[ep]; - break blockloop; - } - } - - if (lastChance()) { - // when lastChance() becomes true the first time, currentBlock contains the last - // end tag available in the input blocks but it's not on the right nesting level - // or we would have terminated the loop already. the only thing we can do is to - // treat the last available end tag as tho it were matched with our start tag - // and let the emitter figure out how to render the garbage inside. - actualEndPos = endPos[endPos.length - 1]; - break; - } - - // any left-over start tags still increase the nesting level - nesting += startPos.length - sp; - contentBlocks.push(currentBlock); - } - - const stopLen = currentBlock.match(args.stop)[0].length; - const before = currentBlock.slice(0, actualEndPos).replace(/\n*$/, ""); - const after = currentBlock.slice(actualEndPos + stopLen).replace(/^\n*/, ""); - if (before.length > 0) contentBlocks.push(MD.mk_block(before, "", currentBlock.lineNumber)); - if (after.length > 0) next.unshift(MD.mk_block(after, currentBlock.trailing, currentBlock.lineNumber + countLines(before))); - - const emitterResult = args.emitter.call(this, contentBlocks, match); - if (emitterResult) { result.push(emitterResult); } - return result; - }; - - if (args.priority) { - blockFunc.priority = args.priority; - } - - this.registerBlock(args.start.toString(), blockFunc); - } - - /** - After the parser has been executed, post process any text nodes in the HTML document. - This is useful if you want to apply a transformation to the text. - - If you are generating HTML from the text, it is preferable to use the replacer - functions and do it in the parsing part of the pipeline. This function is best for - simple transformations or transformations that have to happen after all earlier - processing is done. - - For example, to convert all text to upper case: - - ```javascript - helper.postProcessText(function (text) { - return text.toUpperCase(); - }); - ``` - **/ - postProcessTextFeature(featureName, fn) { - emitters.push(function () { - if (!currentOpts.features[featureName]) { return; } - return fn.apply(this, arguments); - }); - } - - onParseNodeFeature(featureName, fn) { - parseNodes.push(function () { - if (!currentOpts.features[featureName]) { return; } - return fn.apply(this, arguments); - }); - } - - registerBlockFeature(featureName, name, fn) { - const blockFunc = function() { - if (!currentOpts.features[featureName]) { return; } - return fn.apply(this, arguments); - }; - - blockFunc.priority = fn.priority; - this._dialect.block[name] = blockFunc; - } - - applyFeature(featureName, module) { - helper.registerInline = (code, fn) => helper.registerInlineFeature(featureName, code, fn); - helper.replaceBlock = args => helper.replaceBlockFeature(featureName, args); - helper.addPreProcessor = fn => helper.addPreProcessorFeature(featureName, fn); - helper.inlineReplace = (token, emitter) => helper.inlineReplaceFeature(featureName, token, emitter); - helper.postProcessTag = (token, emitter) => helper.postProcessTagFeature(featureName, token, emitter); - helper.inlineRegexp = args => helper.inlineRegexpFeature(featureName, args); - helper.inlineBetween = args => helper.inlineBetweenFeature(featureName, args); - helper.postProcessText = fn => helper.postProcessTextFeature(featureName, fn); - helper.onParseNode = fn => helper.onParseNodeFeature(featureName, fn); - helper.registerBlock = (name, fn) => helper.registerBlockFeature(featureName, name, fn); - - module.setup(this); - } - - setup() { - if (this._setup) { return; } - this._setup = true; - - Object.keys(require._eak_seen).forEach(entry => { - if (entry.indexOf('discourse-markdown') !== -1) { - const module = requirejs(entry); - if (module && module.setup) { - const featureName = entry.split('/').reverse()[0]; - helper.whiteList = info => whiteListFeature(featureName, info); - - this.applyFeature(featureName, module); - helper.whiteList = undefined; - } - } - }); - - MD.buildBlockOrder(this._dialect.block); - var index = this._dialect.block.__order__.indexOf("code"); - if (index > -1) { - this._dialect.block.__order__.splice(index, 1); - this._dialect.block.__order__.unshift("code"); - } - MD.buildInlinePatterns(this._dialect.inline); - } -}; - -const helper = new DialectHelper(); - -export function cook(raw, opts) { - currentOpts = opts; - - hoisted = {}; - - if (!currentOpts.enableExperimentalMarkdownIt) { - raw = hoistCodeBlocksAndSpans(raw); - preProcessors.forEach(p => raw = p(raw)); - } - - const whiteLister = new WhiteLister(opts); - - let result; - - if (currentOpts.enableExperimentalMarkdownIt) { - result = opts.sanitizer( - requirejs('pretty-text/engines/markdown-it/instance').default(opts).render(raw), - whiteLister - ); - } else { - const tree = parser.toHTMLTree(raw, 'Discourse'); - result = opts.sanitizer(parser.renderJsonML(parseTree(tree, opts)), whiteLister); - } - - // If we hoisted out anything, put it back - const keys = Object.keys(hoisted); - if (keys.length) { - let found = true; - - const unhoist = function(key) { - result = result.replace(new RegExp(key, "g"), function() { - found = true; - return hoisted[key]; - }); - }; - - while (found) { - found = false; - keys.forEach(unhoist); - } - } - - return result.trim(); -} - -export function setup() { - helper.setup(); -} - -function processTextNodes(node, event, emitter) { - if (node.length < 2) { return; } - - if (node[0] === '__RAW') { - const hash = guid(); - hoisted[hash] = node[1]; - node[1] = hash; - return; - } - - for (var j=1; j fn(event)); - - for (var j=0; j$/.exec(n[1])) { - // Remove paragraphs around comment-only nodes. - tree[i] = n[1]; - } else { - parseTree(n, options, path, insideCounts); - } - - insideCounts[tagName] = insideCounts[tagName] - 1; - } - - // If raw nodes are in paragraphs, pull them up - if (tree.length === 2 && tree[0] === 'p' && tree[1] instanceof Array && tree[1][0] === "__RAW") { - var text = tree[1][1]; - tree[0] = "__RAW"; - tree[1] = text; - } - - path.pop(); - } - return tree; -} - -// Returns true if there's an invalid word boundary for a match. -function invalidBoundary(args, prev) { - if (!(args.wordBoundary || args.spaceBoundary || args.spaceOrTagBoundary)) { return false; } - - var last = prev[prev.length - 1]; - if (typeof last !== "string") { return false; } - - if (args.wordBoundary && (!last.match(/\W$/))) { return true; } - if (args.spaceBoundary && (!last.match(/\s$/))) { return true; } - if (args.spaceOrTagBoundary && (!last.match(/(\s|\>|\()$/))) { return true; } -} - -function countLines(str) { - let index = -1, count = 0; - while ((index = str.indexOf("\n", index + 1)) !== -1) { count++; } - return count; -} - -function hoister(t, target, replacement) { - const regexp = new RegExp(target.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), "g"); - if (t.match(regexp)) { - const hash = guid(); - t = t.replace(regexp, hash); - hoisted[hash] = replacement; - } - return t; -} - -function outdent(t) { - return t.replace(/^([ ]{4}|\t)/gm, ""); -} - -function removeEmptyLines(t) { - return t.replace(/^\n+/, "").replace(/\s+$/, ""); -} - -function hideBackslashEscapedCharacters(t) { - return t.replace(/\\\\/g, "\u1E800").replace(/\\`/g, "\u1E8001"); -} - -function showBackslashEscapedCharacters(t) { - return t.replace(/\u1E8001/g, "\\`").replace(/\u1E800/g, "\\\\"); -} - -function hoistCodeBlocksAndSpans(text) { - // replace all "\`" with a single character - text = hideBackslashEscapedCharacters(text); - - // /!\ the order is important /!\ - - // fenced code blocks (AKA GitHub code blocks) - text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) { - const hash = guid(); - hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content))); - return before + "```" + language + "\n" + hash + "\n```"; - }); - - // markdown code blocks - text = text.replace(/(^\n*|\n\n)((?:(?:[ ]{4}|\t).*\n*)+)/g, function(match, before, content, index) { - // make sure we aren't in a list - var previousLine = text.slice(0, index).trim().match(/.*$/); - if (previousLine && previousLine[0].length) { - previousLine = previousLine[0].trim(); - if (/^(?:\*|\+|-|\d+\.)\s+/.test(previousLine)) { - return match; - } - } - // we can safely hoist the code block - const hash = guid(); - hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content)))); - return before + " " + hash + "\n"; - }); - - //
    ...
    code blocks - text = text.replace(/(\s|^)
    ([\s\S]*?)<\/pre>/ig, function(_, before, content) {
    -    const hash = guid();
    -    hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
    -    return before + "
    " + hash + "
    "; - }); - - // code spans (double & single `) - ["``", "`"].forEach(function(delimiter) { - var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g"); - text = text.replace(regexp, function(_, before, content, after) { - const hash = guid(); - hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim())); - return before + delimiter + hash + delimiter + after; - }); - }); - - // replace back all weird character with "\`" - return showBackslashEscapedCharacters(text); -} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/auto-link.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/auto-link.js.es6 deleted file mode 100644 index 78ebe441fd..0000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/auto-link.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -// This addition handles auto linking of text. When included, it will parse out links and create -// ``s for them. - -const urlReplacerArgs = { - matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/, - spaceOrTagBoundary: true, - - emitter(matches) { - const url = matches[1]; - let href = url; - - // Don't autolink a markdown link to something - if (url.match(/\]\[\d$/)) { return; } - - // If we improperly caught a markdown link abort - if (url.match(/\(http/)) { return; } - - if (url.match(/^www/)) { href = "http://" + url; } - return ['a', { href }, url]; - } -}; - -export function setup(helper) { - if (helper.markdownIt) { return; } - helper.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs)); - helper.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs)); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-block.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 similarity index 59% rename from app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-block.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 index 51050f9206..c2706b7e66 100644 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-block.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 @@ -11,6 +11,8 @@ function trailingSpaceOnly(src, start, max) { return true; } +const ATTR_REGEX = /^\s*=(.+)$|((([a-z0-9]*)\s*)=)(["“”'].*["“”']|\S+)/ig; + // parse a tag [test a=1 b=2] to a data structure // {tag: "test", attrs={a: "1", b: "2"} export function parseBBCodeTag(src, start, max, multiline) { @@ -71,30 +73,21 @@ export function parseBBCodeTag(src, start, max, multiline) { // trivial parser that is going to have to be rewritten at some point if (raw) { - // reading a key 0, reading a val = 1 - let readingKey = true; - let startSplit = 0; - let key; + let match, key, val; - for(i=0; i 0) { - val = val.replace(/^["'](.*)["']$/, '$1'); - attrs[key] = val; - } - } - readingKey = !readingKey; - startSplit = i+1; + val = match[1] || match[5]; + + if (val) { + val = val.trim(); + val = val.replace(/^["'“”](.*)["'“”]$/, '$1'); + attrs[key] = val; } } } @@ -109,48 +102,25 @@ export function parseBBCodeTag(src, start, max, multiline) { } } -function applyBBCode(state, startLine, endLine, silent, md) { +function findBlockCloseTag(state, openTag, startLine, endLine) { - var nextLine, - old_parent, old_line_max, rule, - start = state.bMarks[startLine] + state.tShift[startLine], - initial = start, - max = state.eMarks[startLine]; - - // [ === 91 - if (91 !== state.src.charCodeAt(start)) { return false; } - - let info = parseBBCodeTag(state.src, start, max, true); - - if (!info || info.closing) { - return false; - } - - let ruleInfo = md.block.bbcode_ruler.getRuleForTag(info.tag); - if (!ruleInfo) { return false; } - - rule = ruleInfo.rule; - - // Since start is found, we can report success here in validation mode - if (silent) { return true; } - - // Search for the end of the block - nextLine = startLine; - - let closeTag; - let nesting = 0; + let nesting = 0, + line = startLine-1, + start, + closeTag, + max; for (;;) { - nextLine++; - if (nextLine >= endLine) { + line++; + if (line >= endLine) { // unclosed bbcode block should not be autoclosed by end of document. - return false; + return; } - start = state.bMarks[nextLine] + state.tShift[nextLine]; - max = state.eMarks[nextLine]; + start = state.bMarks[line] + state.tShift[line]; + max = state.eMarks[line]; - if (start < max && state.sCount[nextLine] < state.blkIndent) { + if (start < max && state.sCount[line] < state.blkIndent) { // non-empty line with negative indent should stop the list: // - ``` // test @@ -161,35 +131,111 @@ function applyBBCode(state, startLine, endLine, silent, md) { // bbcode close [ === 91 if (91 !== state.src.charCodeAt(start)) { continue; } - if (state.sCount[nextLine] - state.blkIndent >= 4) { - // closing fence should be indented less than 4 spaces + if (state.sCount[line] - state.blkIndent >= 4) { + // closing bbcode less than 4 spaces continue; } closeTag = parseBBCodeTag(state.src, start, max, true); - if (closeTag && closeTag.closing && closeTag.tag === info.tag) { + if (closeTag && closeTag.closing && closeTag.tag === openTag.tag) { if (nesting === 0) { + closeTag.line = line; + closeTag.block = true; break; } nesting--; } - if (closeTag && !closeTag.closing && closeTag.tag === info.tag) { + if (closeTag && !closeTag.closing && closeTag.tag === openTag.tag) { nesting++; } closeTag = null; } + return closeTag; +} + +function findInlineCloseTag(state, openTag, start, max) { + + let closeTag; + let possibleTag = false; + + for(let j=max-1;j>start;j--){ + if (!possibleTag) { + if (state.src.charCodeAt(j) === 93 /* ] */) { + possibleTag = true; + continue; + } + if(!isWhiteSpace(state.src.charCodeAt(j))) { + break; + } + } else { + if (state.src.charCodeAt(j) === 91 /* [ */) { + closeTag = parseBBCodeTag(state.src, j, max); + if (!closeTag || closeTag.tag !== openTag.tag || !closeTag.closing) { + closeTag = null; + } + closeTag.start = j; + break; + } + } + } + + return closeTag; +} + +function applyBBCode(state, startLine, endLine, silent, md) { + + var nextLine, + oldParent, oldLineMax, rule, + start = state.bMarks[startLine] + state.tShift[startLine], + initial = start, + max = state.eMarks[startLine]; + + // [ === 91 + if (91 !== state.src.charCodeAt(start)) { return false; } + + let info = parseBBCodeTag(state.src, start, max); + + if (!info || info.closing) { + return false; + } + + let ruleInfo = md.block.bbcode.ruler.getRuleForTag(info.tag); + if (!ruleInfo) { return false; } + + rule = ruleInfo.rule; + + // Since start is found, we can report success here in validation mode + if (silent) { return true; } + + // Search for the end of the block + nextLine = startLine; + + // We might have a single inline bbcode + + let closeTag = findInlineCloseTag(state, info, start + info.length, max); + + if (!closeTag) { + if (!trailingSpaceOnly(state.src, start + info.length, max)) { + return false; + } + closeTag = findBlockCloseTag(state, info, nextLine+1, endLine); + } + if (!closeTag) { return false; } - old_parent = state.parentType; - old_line_max = state.lineMax; + nextLine = closeTag.line || startLine; + + oldParent = state.parentType; + oldLineMax = state.lineMax; // this will prevent lazy continuations from ever going past our end marker + // which can happen if we are parsing a bbcode block state.lineMax = nextLine; if (rule.replace) { @@ -200,7 +246,7 @@ function applyBBCode(state, startLine, endLine, silent, md) { } else { if (rule.before) { - rule.before.call(this, state, info.attrs, md, state.src.slice(initial, initial + info.length + 1)); + rule.before.call(this, state, info, state.src.slice(initial, initial + info.length + 1)); } let wrapTag; @@ -236,30 +282,39 @@ function applyBBCode(state, startLine, endLine, silent, md) { let lastToken = state.tokens[state.tokens.length-1]; lastToken.map = [ startLine, nextLine ]; - state.md.block.tokenize(state, startLine + 1, nextLine); + if (closeTag.block) { + state.md.block.tokenize(state, startLine + 1, nextLine); + } else { + let token = state.push('paragraph_open', 'p', 1); + token.map = [startLine, startLine]; + + token = state.push('inline', '', 0); + token.children = []; + token.map = [startLine, startLine]; + token.content = state.src.slice(start + info.length, closeTag.start); + + state.push('paragraph_close', 'p', -1); + } if (rule.wrap) { state.push('wrap_bbcode', wrapTag, -1); } if (rule.after) { - rule.after.call(this, state, lastToken, md, state.src.slice(start-2, start + closeTag.length - 1)); + rule.after.call(this, state, lastToken, state.src.slice(start-2, start + closeTag.length - 1)); } } - state.parentType = old_parent; - state.lineMax = old_line_max; + state.parentType = oldParent; + state.lineMax = oldLineMax; state.line = nextLine+1; return true; } export function setup(helper) { - if (!helper.markdownIt) { return; } - - helper.registerPlugin(md => { - const ruler = md.block.bbcode_ruler; + const ruler = md.block.bbcode.ruler; ruler.push('code', { tag: 'code', diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-inline.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 similarity index 74% rename from app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-inline.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 index 2184adb7ff..e508509298 100644 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-inline.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 @@ -1,4 +1,4 @@ -import { parseBBCodeTag } from 'pretty-text/engines/markdown-it/bbcode-block'; +import { parseBBCodeTag } from 'pretty-text/engines/discourse-markdown/bbcode-block'; function tokanizeBBCode(state, silent, ruler) { @@ -57,6 +57,7 @@ function tokanizeBBCode(state, silent, ruler) { let token = state.push('text', '' , 0); token.content = state.src.slice(pos, pos+tagInfo.length); + token.meta = 'bbcode'; state.delimiters.push({ bbInfo: tagInfo, @@ -105,10 +106,15 @@ function processBBCode(state, silent) { let tag, className; if (typeof tagInfo.rule.wrap === 'function') { - if (!tagInfo.rule.wrap(token, tagInfo)) { - return false; + let content = ""; + for (let j = startDelim.token+1; j < endDelim.token; j++) { + let inner = state.tokens[j]; + if (inner.type === 'text' && inner.meta !== 'bbcode') { + content += inner.content; + } } - tag = token.tag; + tagInfo.rule.wrap(token, state.tokens[endDelim.token], tagInfo, content); + continue; } else { let split = tagInfo.rule.wrap.split('.'); tag = split[0]; @@ -135,9 +141,6 @@ function processBBCode(state, silent) { } export function setup(helper) { - - if (!helper.markdownIt) { return; } - helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']); helper.registerOptions(opts => { @@ -145,7 +148,7 @@ export function setup(helper) { }); helper.registerPlugin(md => { - const ruler = md.inline.bbcode_ruler; + const ruler = md.inline.bbcode.ruler; md.inline.ruler.push('bbcode-inline', (state,silent) => tokanizeBBCode(state,silent,ruler)); md.inline.ruler2.before('text_collapse', 'bbcode-inline', processBBCode); @@ -160,19 +163,34 @@ export function setup(helper) { } }); + const simpleUrlRegex = /^http[s]?:\/\//; ruler.push('url', { tag: 'url', - replace: function(state, tagInfo, content) { - let token; + wrap: function(startToken, endToken, tagInfo, content) { + const url = (tagInfo.attrs['_default'] || content).trim(); - token = state.push('link_open', 'a', 1); - token.attrs = [['href', content], ['data-bbcode', 'true']]; + if (simpleUrlRegex.test(url)) { + startToken.type = 'link_open'; + startToken.tag = 'a'; + startToken.attrs = [['href', url], ['data-bbcode', 'true']]; + startToken.content = ''; + startToken.nesting = 1; - token = state.push('text', '', 0); - token.content = content; + endToken.type = 'link_close'; + endToken.tag = 'a'; + endToken.content = ''; + endToken.nesting = -1; + } else { + // just strip the bbcode tag + endToken.content = ''; + startToken.content = ''; - token = state.push('link_close', 'a', -1); - return true; + // edge case, we don't want this detected as a onebox if auto linked + // this ensures it is not stripped + startToken.type = 'html_inline'; + } + + return false; } }); @@ -180,9 +198,10 @@ export function setup(helper) { tag: 'email', replace: function(state, tagInfo, content) { let token; + let email = tagInfo.attrs['_default'] || content; token = state.push('link_open', 'a', 1); - token.attrs = [['href', 'mailto:' + content], ['data-bbcode', 'true']]; + token.attrs = [['href', 'mailto:' + email], ['data-bbcode', 'true']]; token = state.push('text', '', 0); token.content = content; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 deleted file mode 100644 index d011849752..0000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 +++ /dev/null @@ -1,170 +0,0 @@ -export function register(helper, codeName, args, emitter) { - // Optional second param for args - if (typeof args === "function") { - emitter = args; - args = {}; - } - - helper.replaceBlock({ - start: new RegExp("\\[" + codeName + "(=[^\\[\\]]+)?\\]([\\s\\S]*)", "igm"), - stop: new RegExp("\\[\\/" + codeName + "\\]", "igm"), - emitter(blockContents, matches) { - - - const options = helper.getOptions(); - while (blockContents.length && (typeof blockContents[0] === "string" || blockContents[0] instanceof String)) { - blockContents[0] = String(blockContents[0]).replace(/^\s+/, ''); - if (!blockContents[0].length) { - blockContents.shift(); - } else { - break; - } - } - - let contents = []; - if (blockContents.length) { - const nextContents = blockContents.slice(1); - blockContents = this.processBlock(blockContents[0], nextContents); - - nextContents.forEach(nc => { - blockContents = blockContents.concat(this.processBlock(nc, [])); - }); - - blockContents.forEach(bc => { - if (typeof bc === "string" || bc instanceof String) { - var processed = this.processInline(String(bc)); - if (processed.length) { - contents.push(['p'].concat(processed)); - } - } else { - contents.push(bc); - } - }); - } - if (!args.singlePara && contents.length === 1 && contents[0] instanceof Array && contents[0][0] === "para") { - contents[0].shift(); - contents = contents[0]; - } - const result = emitter(contents, matches[1] ? matches[1].replace(/^=|\"/g, '') : null, options); - return args.noWrap ? result : ['p', result]; - } - }); -}; - -export function builders(helper) { - function replaceBBCode(tag, emitter, opts) { - const start = `[${tag}]`; - const stop = `[/${tag}]`; - - opts = opts || {}; - opts = _.merge(opts, { start, stop, emitter }); - helper.inlineBetween(opts); - - opts = _.merge(opts, { start: start.toUpperCase(), stop: stop.toUpperCase(), emitter }); - helper.inlineBetween(opts); - } - - return { - replaceBBCode, - - register(codeName, args, emitter) { - register(helper, codeName, args, emitter); - }, - - rawBBCode(tag, emitter) { - replaceBBCode(tag, emitter, { rawContents: true }); - }, - - removeEmptyLines(contents) { - const result = []; - for (let i=0; i < contents.length; i++) { - if (contents[i] !== "\n") { result.push(contents[i]); } - } - return result; - }, - - replaceBBCodeParamsRaw(tag, emitter) { - var opts = { - rawContents: true, - emitter(contents) { - const m = /^([^\]]+)\]([\S\s]*)$/.exec(contents); - if (m) { return emitter.call(this, m[1], m[2]); } - } - }; - - helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" })); - - tag = tag.toUpperCase(); - helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" })); - } - }; -} - -export function setup(helper) { - - if (helper.markdownIt) { return; } - - helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']); - - const { replaceBBCode, rawBBCode, removeEmptyLines, replaceBBCodeParamsRaw } = builders(helper); - - replaceBBCode('b', contents => ['span', {'class': 'bbcode-b'}].concat(contents)); - replaceBBCode('i', contents => ['span', {'class': 'bbcode-i'}].concat(contents)); - replaceBBCode('u', contents => ['span', {'class': 'bbcode-u'}].concat(contents)); - replaceBBCode('s', contents => ['span', {'class': 'bbcode-s'}].concat(contents)); - - replaceBBCode('ul', contents => ['ul'].concat(removeEmptyLines(contents))); - replaceBBCode('ol', contents => ['ol'].concat(removeEmptyLines(contents))); - replaceBBCode('li', contents => ['li'].concat(removeEmptyLines(contents))); - - rawBBCode('img', href => ['img', {href}]); - rawBBCode('email', contents => ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]); - - replaceBBCode('url', contents => { - if (!Array.isArray(contents)) { return; } - - const first = contents[0]; - if (contents.length === 1 && Array.isArray(first) && first[0] === 'a') { - // single-line bbcode links shouldn't be oneboxed, so we mark this as a bbcode link. - if (typeof first[1] !== 'object') { first.splice(1, 0, {}); } - first[1]['data-bbcode'] = true; - } - return ['concat'].concat(contents); - }); - - replaceBBCodeParamsRaw('url', function(param, contents) { - const url = param.replace(/(^")|("$)/g, ''); - return ['a', {'href': url}].concat(this.processInline(contents)); - }); - - replaceBBCodeParamsRaw("email", function(param, contents) { - return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents); - }); - - helper.onParseNode(event => { - if (!Array.isArray(event.node)) { return; } - const result = [event.node[0]]; - const nodes = event.node.slice(1); - for (let i = 0; i < nodes.length; i++) { - if (Array.isArray(nodes[i]) && nodes[i][0] === 'concat') { - for (let j = 1; j < nodes[i].length; j++) { result.push(nodes[i][j]); } - } else { - result.push(nodes[i]); - } - } - for (let i = 0; i < result.length; i++) { event.node[i] = result[i]; } - }); - - helper.replaceBlock({ - start: /(\[code\])([\s\S]*)/igm, - stop: /\[\/code\]/igm, - rawContents: true, - - emitter(blockContents) { - const options = helper.getOptions(); - const inner = blockContents.join("\n"); - const defaultCodeLang = options.defaultCodeLang; - return ['p', ['pre', ['code', {'class': `lang-${defaultCodeLang}`}, inner]]]; - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 deleted file mode 100644 index a79a983b08..0000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 +++ /dev/null @@ -1,73 +0,0 @@ -import guid from 'pretty-text/guid'; - -/** - markdown-js doesn't ensure that em/strong codes are present on word boundaries. - So we create our own handlers here. -**/ - -// From PageDown -const aLetter = /[a-zA-Z0-9\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/; - - -function unhoist(obj,from,to){ - let unhoisted = 0; - const regex = new RegExp(from, "g"); - - if(_.isArray(obj)){ - for (let i=0; i= 0) { - const newText = this.processInline(text.substring(match.length, finish+1)); - const unhoisted_length = unhoist(newText,hash,match[0]); - const array = typeof tag === "string" ? [tag].concat(newText) : [tag[0], [tag[1]].concat(newText)]; - return [(finish + match.length + 1) - unhoisted_length, array]; - } - }); - } - - replaceMarkdown('***', ['strong','em']); - replaceMarkdown('___', ['strong','em']); - replaceMarkdown('**', 'strong'); - replaceMarkdown('__', 'strong'); - replaceMarkdown('*', 'em'); - replaceMarkdown('_', 'em'); -}; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 index c33eeff65f..488a528211 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 @@ -1,20 +1,55 @@ +function addHashtag(buffer, matches, state) { + const options = state.md.options.discourse; + const slug = matches[1]; + const categoryHashtagLookup = options.categoryHashtagLookup; + const result = categoryHashtagLookup && categoryHashtagLookup(slug); + + let token; + + if (result) { + token = new state.Token('link_open', 'a', 1); + token.attrs = [['class', 'hashtag'], ['href', result[0]]]; + token.block = false; + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = '#'; + buffer.push(token); + + token = new state.Token('span_open', 'span', 1); + token.block = false; + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = result[1]; + buffer.push(token); + + buffer.push(new state.Token('span_close', 'span', -1)); + + buffer.push(new state.Token('link_close', 'a', -1)); + } else { + + token = new state.Token('span_open', 'span', 1); + token.attrs = [['class', 'hashtag']]; + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = matches[0]; + buffer.push(token); + + token = new state.Token('span_close', 'span', -1); + buffer.push(token); + } +} + export function setup(helper) { + helper.registerPlugin(md=>{ - if (helper.markdownIt) { return; } + const rule = { + matcher: /#([\w-:]{1,101})/, + onMatch: addHashtag + }; - helper.inlineRegexp({ - start: '#', - matcher: /^#([\w-:]{1,101})/i, - spaceOrTagBoundary: true, - - emitter(matches) { - const options = helper.getOptions(); - const [hashtag, slug] = matches; - const categoryHashtagLookup = options.categoryHashtagLookup; - const result = categoryHashtagLookup && categoryHashtagLookup(slug); - - return result ? ['a', { class: 'hashtag', href: result[0] }, '#', ["span", {}, result[1]]] - : ['span', { class: 'hashtag' }, hashtag]; - } + md.core.textPostProcess.ruler.push('category-hashtag', rule); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 index d3ed549fe0..91a0d297ba 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 @@ -1,18 +1,40 @@ -import { censor } from 'pretty-text/censored-words'; -import { registerOption } from 'pretty-text/pretty-text'; +import { censorFn } from 'pretty-text/censored-words'; -registerOption((siteSettings, opts) => { - opts.features.censored = true; - opts.censoredWords = siteSettings.censored_words; - opts.censoredPattern = siteSettings.censored_pattern; -}); +function recurse(tokens, apply) { + let i; + for(i=0;i { - const options = helper.getOptions(); - return censor(text, options.censoredWords, options.censoredPattern); + recurse(state.tokens, token => { + if (token.content) { + token.content = censor(token.content); + } + }); +} + +export function setup(helper) { + helper.registerOptions((opts, siteSettings) => { + opts.censoredPattern = siteSettings.censored_pattern; + }); + + helper.registerPlugin(md => { + const words = md.options.discourse.censoredWords; + const patterns = md.options.discourse.censoredPattern; + + if ((words && words.length > 0) || (patterns && patterns.length > 0)) { + const replacement = String.fromCharCode(9632); + const censor = censorFn(words, patterns, replacement); + md.core.ruler.push('censored', state => censorTree(state, censor)); + } }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 index 048b27a36d..a8def62ce2 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 @@ -1,27 +1,36 @@ -import { escape } from 'pretty-text/sanitizer'; -import { registerOption } from 'pretty-text/pretty-text'; +// we need a custom renderer for code blocks cause we have a slightly non compliant +// format with special handling for text and so on -// Support for various code blocks const TEXT_CODE_CLASSES = ["text", "pre", "plain"]; -function codeFlattenBlocks(blocks) { - let result = ""; - blocks.forEach(function(b) { - result += b; - if (b.trailing) { result += b.trailing; } - }); - return result; + +function render(tokens, idx, options, env, slf, md) { + let token = tokens[idx], + info = token.info ? md.utils.unescapeAll(token.info) : '', + langName = md.options.discourse.defaultCodeLang, + className, + escapedContent = md.utils.escapeHtml(token.content); + + if (info) { + // strip off any additional languages + info = info.trim().split(/\s+/g)[0]; + } + + const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses; + if (acceptableCodeClasses && info && acceptableCodeClasses.indexOf(info) !== -1) { + langName = info; + } + + className = TEXT_CODE_CLASSES.indexOf(info) !== -1 ? 'lang-nohighlight' : 'lang-' + langName; + + return `
    ${escapedContent}
    \n`; } -registerOption((siteSettings, opts) => { - opts.features.code = true; - opts.defaultCodeLang = siteSettings.default_code_lang; - opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); -}); - export function setup(helper) { - - if (helper.markdownIt) { return; } + helper.registerOptions((opts, siteSettings) => { + opts.defaultCodeLang = siteSettings.default_code_lang; + opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); + }); helper.whiteList({ custom(tag, name, value) { @@ -34,50 +43,7 @@ export function setup(helper) { } }); - helper.replaceBlock({ - start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm, - stop: /^```$/gm, - withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match - emitter(blockContents, matches) { - const opts = helper.getOptions(); - - let codeLang = opts.defaultCodeLang; - const acceptableCodeClasses = opts.acceptableCodeClasses; - if (acceptableCodeClasses && matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) { - codeLang = matches[1]; - } - - if (TEXT_CODE_CLASSES.indexOf(matches[1]) !== -1) { - return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]]; - } else { - return ['p', ['pre', ['code', {'class': 'lang-' + codeLang}, codeFlattenBlocks(blockContents) ]]]; - } - } - }); - - helper.replaceBlock({ - start: /(]*\>)([\s\S]*)/igm, - stop: /<\/pre>/igm, - rawContents: true, - skipIfTradtionalLinebreaks: true, - - emitter(blockContents) { - return ['p', ['pre', codeFlattenBlocks(blockContents)]]; - } - }); - - // Ensure that content in a code block is fully escaped. This way it's not white listed - // and we can use HTML and Javascript examples. - helper.onParseNode(function(event) { - const node = event.node, - path = event.path; - - if (node[0] === 'code') { - const regexp = (path && path[path.length-1] && path[path.length-1][0] && path[path.length-1][0] === "pre") ? - / +$/g : /^ +| +$/g; - - const contents = node[node.length-1]; - node[node.length-1] = escape(contents.replace(regexp,'')); - } + helper.registerPlugin(md=>{ + md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 index 60864a2263..0472216a65 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 @@ -1,117 +1,243 @@ -import { registerOption } from 'pretty-text/pretty-text'; import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji'; import { translations } from 'pretty-text/emoji/data'; -let _unicodeReplacements; -let _unicodeRegexp; -export function setUnicodeReplacements(replacements) { - _unicodeReplacements = replacements; - if (replacements) { - // We sort and reverse to match longer emoji sequences first - _unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g"); - } -}; +const MAX_NAME_LENGTH = 60; -function escapeRegExp(s) { - return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&'); -} +let translationTree = null; -function checkPrev(prev) { - if (prev && prev.length) { - const lastToken = prev[prev.length-1]; - if (lastToken && lastToken.charAt) { - const lastChar = lastToken.charAt(lastToken.length-1); - if (!/\W/.test(lastChar)) return false; - } - } - return true; -} +// This allows us to efficiently search for aliases +// We build a data structure that allows us to quickly +// search through our N next chars to see if any match +// one of our alias emojis. +// +function buildTranslationTree() { + let tree = []; + let lastNode; -registerOption((siteSettings, opts, state) => { - opts.features.emoji = !!siteSettings.enable_emoji; - opts.emojiSet = siteSettings.emoji_set || ""; - opts.customEmoji = state.customEmoji; -}); + Object.keys(translations).forEach(function(key){ + let i; + let node = tree; -export function setup(helper) { + for(i=0;i { - if (t[0] === ':') { - translationsWithColon[t] = translations[t]; - } else { - const replacement = translations[t]; - helper.inlineReplace(t, (token, match, prev) => { - return checkPrev(prev) ? imageFor(replacement) : token; - }); - } - }); - const translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(t => `(${escapeRegExp(t)})`).join("|")); - - helper.registerInline(':', (text, match, prev) => { - const endPos = text.indexOf(':', 1); - const firstSpace = text.search(/\s/); - if (!checkPrev(prev)) { return; } - - // If there is no trailing colon, check our translations that begin with colons - if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) { - translationColonRegexp.lastIndex = 0; - const m = translationColonRegexp.exec(text); - if (m && m[0] && text.indexOf(m[0]) === 0) { - // Check outer edge - const lastChar = text.charAt(m[0].length); - if (lastChar && !/\s/.test(lastChar)) return; - const contents = imageFor(translationsWithColon[m[0]]); - if (contents) { - return [m[0].length, contents]; + for (j=0;j 0) { + let prev = content.charCodeAt(pos-1); + if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) { return; } + } - let between; - const emojiNameMatch = text.match(/(?:.*?)(:(?!:).?[\w-]*(?::t\d)?:)/); - if (emojiNameMatch) { - between = emojiNameMatch[0].slice(1, -1); - } else { - between = text.slice(1, -1); + pos++; + if (content.charCodeAt(pos) === 58) { + return; + } + + let length = 0; + while(length < MAX_NAME_LENGTH) { + length++; + + if (content.charCodeAt(pos+length) === 58) { + // check for t2-t6 + if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) { + length += 3; + } + break; } - const contents = imageFor(between); - if (contents) { - return [text.indexOf(between, 1) + between.length + 1, contents]; + if (pos+length > content.length) { + return; } - }); + } - helper.addPreProcessor(text => { - if (_unicodeReplacements) { - _unicodeRegexp.lastIndex = 0; + if (length === MAX_NAME_LENGTH) { + return; + } - let m; - while ((m = _unicodeRegexp.exec(text)) !== null) { - let replacement = ":" + _unicodeReplacements[m[0]] + ":"; - const before = text.charAt(m.index-1); - if (!/\B/.test(before)) { - replacement = "\u200b" + replacement; + return content.substr(pos, length); +} + +// straight forward :smile: to emoji image +function getEmojiTokenByName(name, state) { + + let info; + if (info = imageFor(name, state.md.options.discourse)) { + let token = new state.Token('emoji', 'img', 0); + token.attrs = [['src', info.url], + ['title', info.title], + ['class', info.classes], + ['alt', info.title]]; + + return token; + } +} + +function getEmojiTokenByTranslation(content, pos, state) { + + translationTree = translationTree || buildTranslationTree(); + + let currentTree = translationTree; + + let i; + let search = true; + let found = false; + let start = pos; + + while(search) { + + search = false; + let code = content.charCodeAt(pos); + + for (i=0;i 0) { + let leading = content.charAt(start-1); + if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) { + return; + } + } + + // check trailing for punct or space + if (pos < content.length) { + let trailing = content.charCodeAt(pos); + if (!state.md.utils.isSpace(trailing)){ + return; + } + } + + let token = getEmojiTokenByName(found, state); + if (token) { + return { pos, token }; + } +} + +function applyEmoji(content, state, emojiUnicodeReplacer) { + let i; + let result = null; + let contentToken = null; + + let start = 0; + + if (emojiUnicodeReplacer) { + content = emojiUnicodeReplacer(content); + } + + let endToken = content.length; + + for (i=0; i0) { + contentToken = new state.Token('text', '', 0); + contentToken.content = content.slice(start,i); + result.push(contentToken); + } + + result.push(token); + endToken = start = i + offset; + } + } + + if (endToken < content.length) { + contentToken = new state.Token('text', '', 0); + contentToken.content = content.slice(endToken); + result.push(contentToken); + } + + return result; +} + +export function setup(helper) { + helper.registerOptions((opts, siteSettings, state)=>{ + opts.features.emoji = !!siteSettings.enable_emoji; + opts.emojiSet = siteSettings.emoji_set || ""; + opts.customEmoji = state.customEmoji; + }); + + helper.registerPlugin((md)=>{ + md.core.ruler.push('emoji', state => md.options.discourse.helpers.textReplace( + state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer)) + ); }); } diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6 similarity index 52% rename from app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6 index b016b5a86c..c689d5c633 100644 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6 @@ -13,66 +13,6 @@ export default null; // emitter: emitter // }); -export function inlineRegexRule(md, options) { - - const start = options.start.charCodeAt(0); - const maxLength = (options.maxLength || 500) + 1; - - return function(state, silent) { - const pos = state.pos; - - if (state.src.charCodeAt(pos) !== start || silent) { - return false; - } - - // test prev - if (pos > 0) { - let prev = state.src.charCodeAt(pos-1); - if (!md.utils.isWhiteSpace(prev) && !md.utils.isPunctChar(String.fromCharCode(prev))) { - return false; - } - } - - // skip if in a link - if (options.skipInLink && state.tokens) { - let i; - for(i=state.tokens.length-1;i>=0;i--) { - let token = state.tokens[i]; - let type = token.type; - if (type === 'link_open' || (type === 'html_inline' && token.content.substr(0,2).toLowerCase() === "")) { - break; - } - } - } - - const substr = state.src.slice(pos, Math.min(pos + maxLength,state.posMax)); - - const matches = options.matcher.exec(substr); - if (!matches) { - return false; - } - - // got to test trailing boundary - const finalPos = pos+matches[0].length; - if (finalPos < state.posMax) { - const trailing = state.src.charCodeAt(finalPos); - if (!md.utils.isSpace(trailing) && !md.utils.isPunctChar(String.fromCharCode(trailing))) { - return false; - } - } - - if (options.emitter(matches, state)) { - state.pos = Math.min(state.posMax, finalPos); - return true; - } - - return false; - - }; -} // based off https://github.com/markdown-it/markdown-it-emoji/blob/master/dist/markdown-it-emoji.js // diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js.es6 new file mode 100644 index 0000000000..02adc2cf1e --- /dev/null +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html-img.js.es6 @@ -0,0 +1,71 @@ +// special handling for IMG tags on a line by themeselves +// we always have to handle it as so it is an inline +// see: https://talk.commonmark.org/t/newline-and-img-tags/2511 + +const REGEX = /^\s*$/i; + +function rule(state, startLine, endLine) { + + var nextLine, token, lineText, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + // if it's indented more than 3 spaces, it should be a code block + if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } + + if (!state.md.options.html) { return false; } + + if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } + let pos1 = state.src.charCodeAt(pos+1); + if (pos1 !== 73 /* I */ && pos1 !== 105 /* i */) { return false; } + + lineText = state.src.slice(pos, max); + + if (!REGEX.test(lineText)) { + return false; + } + + let lines = []; + lines.push(lineText); + + nextLine = startLine + 1; + for (; nextLine < endLine; nextLine++) { + pos = state.bMarks[nextLine] + state.tShift[nextLine]; + max = state.eMarks[nextLine]; + lineText = state.src.slice(pos, max); + + if (lineText.trim() === "") { + break; + } + + if (!REGEX.test(lineText)) { + break; + } + + lines.push(lineText); + } + + state.line = nextLine; + let oldParentType = state.parentType; + state.parentType = 'paragraph'; + + token = state.push('paragraph_open', 'p', 1); + token.map = [startLine, state.line]; + + token = state.push('inline', '', 0); + token.content = lines.join('\n'); + token.map = [startLine, state.line]; + token.children = []; + + token = state.push('paragraph_close', 'p', -1); + state.parentType = oldParentType; + + return true; +} + + +export function setup(helper) { + helper.registerPlugin(md=>{ + md.block.ruler.before('html_block', 'html_img', rule, {alt: ['fence']}); + }); +} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 deleted file mode 100644 index 1d8f21a205..0000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 +++ /dev/null @@ -1,52 +0,0 @@ -const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'details', - 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', - 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output', - 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video', 'summary']; - -function splitAtLast(tag, block, next, first) { - const endTag = ``; - let endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag); - - if (endTagIndex !== -1) { - endTagIndex += endTag.length; - - const trailing = block.substr(endTagIndex).replace(/^\s+/, ''); - if (trailing.length) { - next.unshift(trailing); - } - - return [ block.substr(0, endTagIndex) ]; - } -}; - -export function setup(helper) { - - if (helper.markdownIt) { return; } - - // If a row begins with HTML tags, don't parse it. - helper.registerBlock('html', function(block, next) { - let split, pos; - - // Fix manual blockquote paragraphing even though it's not strictly correct - // PERF NOTE: /\S+
    = 0) { - if(block.substring(0, pos).search(/\s/) === -1) { - split = splitAtLast('blockquote', block, next, true); - if (split) { return this.processInline(split[0]); } - } - } - - const m = /^\s*<\/?([^>]+)\>/.exec(block); - if (m && m[1]) { - const tag = m[1].split(/\s/); - if (tag && tag[0] && BLOCK_TAGS.indexOf(tag[0]) !== -1) { - split = splitAtLast(tag[0], block, next); - if (split) { - if (split.length === 1 && split[0] === block) { return; } - return split; - } - return [ block.toString() ]; - } - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 index 84be9e5f32..54a5284372 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 @@ -1,51 +1,48 @@ -/** - Supports our custom @mention syntax for calling out a user in a post. - It will add a special class to them, and create a link if the user is found in a - local map. -**/ +function addMention(buffer, matches, state) { + let username = matches[1] || matches[2]; + let mentionLookup = state.md.options.discourse.mentionLookup; + let getURL = state.md.options.discourse.getURL; + + let type = mentionLookup && mentionLookup(username); + + let tag = 'a'; + let className = 'mention'; + let href = null; + + if (type === 'user') { + href = getURL('/u/') + username.toLowerCase(); + } else if (type === 'group') { + href = getURL('/groups/') + username; + className = 'mention-group'; + } else { + tag = 'span'; + } + + let token = new state.Token('mention_open', tag, 1); + token.attrs = [['class', className]]; + if (href) { + token.attrs.push(['href', href]); + } + + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = '@'+username; + + buffer.push(token); + + token = new state.Token('mention_close', tag, -1); + buffer.push(token); +} + export function setup(helper) { + helper.registerPlugin(md => { - if (helper.markdownIt) { return; } + const rule = { + matcher: /@(\w[\w.-]{0,58}\w)|@(\w)/, + onMatch: addMention + }; - // We have to prune @mentions that are within links. - helper.onParseNode(event => { - const node = event.node, - path = event.path; - - if (node[1] && node[1]["class"] === 'mention') { - const parent = path[path.length - 1]; - - // If the parent is an 'a', remove it - if (parent && parent[0] === 'a') { - const name = node[2]; - node.length = 0; - node[0] = "__RAW"; - node[1] = name; - } - } - }); - - helper.inlineRegexp({ - start: '@', - // NOTE: since we can't use SiteSettings here (they loads later in process) - // we are being less strict to account for more cases than allowed - matcher: /^@(\w[\w.-]{0,59})\b/i, - wordBoundary: true, - - emitter(matches) { - const mention = matches[0].trim(); - const name = matches[1]; - const opts = helper.getOptions(); - const mentionLookup = opts.mentionLookup; - - const type = mentionLookup && mentionLookup(name); - if (type === "user") { - return ['a', {'class': 'mention', href: opts.getURL("/u/") + name.toLowerCase()}, mention]; - } else if (type === "group") { - return ['a', {'class': 'mention-group', href: opts.getURL("/groups/") + name}, mention]; - } else { - return ['span', {'class': 'mention'}, mention]; - } - } + md.core.textPostProcess.ruler.push('mentions', rule); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 index a453445a2c..f1eb2ba759 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 @@ -1,30 +1,53 @@ -// Support for the newline behavior in markdown that most expect. Look through all text nodes -// in the tree, replace any new lines with `br`s. +// see: https://github.com/markdown-it/markdown-it/issues/375 +// +// we use a custom paragraph rule cause we have to signal when a +// link starts with a space, so we can bypass a onebox +// this is a freedom patch, so careful, may break on updates + + +function newline(state, silent) { + var token, pmax, max, pos = state.pos; + + if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } + + pmax = state.pending.length - 1; + max = state.posMax; + + // ' \n' -> hardbreak + // Lookup in pending chars is bad practice! Don't copy to other rules! + // Pending string is stored in concat mode, indexed lookups will cause + // convertion to flat mode. + if (!silent) { + if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { + if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { + state.pending = state.pending.replace(/ +$/, ''); + token = state.push('hardbreak', 'br', 0); + } else { + state.pending = state.pending.slice(0, -1); + token = state.push('softbreak', 'br', 0); + } + + } else { + token = state.push('softbreak', 'br', 0); + } + } + + pos++; + + // skip heading spaces for next line + while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) { + if (token) { + token.leading_space = true; + } + pos++; + } + + state.pos = pos; + return true; +}; export function setup(helper) { - - if (helper.markdownIt) { return; } - - helper.postProcessText((text, event) => { - const { options, insideCounts } = event; - if (options.traditionalMarkdownLinebreaks || (insideCounts.pre > 0)) { return; } - - if (text === "\n") { - // If the tag is just a new line, replace it with a `
    ` - return [['br']]; - } else { - // If the text node contains new lines, perhaps with text between them, insert the - // `
    ` tags. - const split = text.split(/\n+/); - if (split.length) { - const replacement = []; - for (var i=0; i 0) { replacement.push(split[i]); } - if (i !== split.length-1) { replacement.push(['br']); } - } - - return replacement; - } - } + helper.registerPlugin(md => { + md.inline.ruler.at('newline', newline); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 index 875321911f..eff7ee4ae2 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 @@ -1,71 +1,116 @@ import { lookupCache } from 'pretty-text/oneboxer'; +import { cachedInlineOnebox } from 'pretty-text/inline-oneboxer'; -// Given a node in the document and its parent, determine whether it is on its own line or not. -function isOnOneLine(link, parent) { - if (!parent) { return false; } +const ONEBOX = 1; +const INLINE = 2; - const siblings = parent.slice(1); - if ((!siblings) || (siblings.length < 1)) { return false; } - - const idx = siblings.indexOf(link); - if (idx === -1) { return false; } - - if (idx > 0) { - const prev = siblings[idx-1]; - if (prev[0] !== 'br') { return false; } +function applyOnebox(state, silent) { + if (silent || !state.tokens) { + return; } - if (idx < siblings.length) { - const next = siblings[idx+1]; - if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; } - } + for(let i=1;i { - const node = event.node, - path = event.path; + if (j > children.length-3) { + continue; + } - // We only care about links - if (node[0] !== 'a') { return; } + if (j === 0 && token.leading_space) { + mode = INLINE; + } else if (j > 0) { + let prevSibling = children[j-1]; + if (prevSibling.tag !== 'br' || prevSibling.leading_space) { + mode = INLINE; + } + } - const parent = path[path.length - 1]; + // look ahead for soft or hard break + let text = children[j+1]; + let close = children[j+2]; + let lookahead = children[j+3]; - // We don't onebox bbcode - if (node[1]['data-bbcode']) { - delete node[1]['data-bbcode']; - return; - } + if (lookahead && lookahead.tag !== 'br') { + mode = INLINE; + } - // We don't onebox mentions - if (node[1]['class'] === 'mention') { return; } + // check attrs only include a href + let attrs = child.attrs; - // Don't onebox links within a list - for (var i=0; i { + md.core.ruler.after('linkify', 'onebox', applyOnebox); }); } diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/paragraph.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js.es6 similarity index 100% rename from app/assets/javascripts/pretty-text/engines/markdown-it/paragraph.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js.es6 diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 deleted file mode 100644 index e8a4a9a131..0000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 +++ /dev/null @@ -1,83 +0,0 @@ -import { register } from 'pretty-text/engines/discourse-markdown/bbcode'; -import { registerOption } from 'pretty-text/pretty-text'; -import { performEmojiUnescape } from 'pretty-text/emoji'; - -registerOption((siteSettings, opts) => { - opts.enableEmoji = siteSettings.enable_emoji; - opts.emojiSet = siteSettings.emoji_set; -}); - - -export function setup(helper) { - - if (helper.markdownIt) { return; } - - register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => { - - const params = {'class': 'quote'}; - let username = null; - const opts = helper.getOptions(); - - if (bbParams) { - const paramsSplit = bbParams.split(/\,\s*/); - username = paramsSplit[0]; - - paramsSplit.forEach(function(p,i) { - if (i > 0) { - var assignment = p.split(':'); - if (assignment[0] && assignment[1]) { - const escaped = helper.escape(assignment[0]); - // don't escape attributes, makes no sense - if (escaped === assignment[0]) { - params['data-' + assignment[0]] = helper.escape(assignment[1].trim()); - } - } - } - }); - } - - let avatarImg; - const postNumber = parseInt(params['data-post'], 10); - const topicId = parseInt(params['data-topic'], 10); - - if (options.lookupAvatarByPostNumber) { - // client-side, we can retrieve the avatar from the post - avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId); - } else if (options.lookupAvatar) { - // server-side, we need to lookup the avatar from the username - avatarImg = options.lookupAvatar(username); - } - - // If there's no username just return a simple quote - if (!username) { - return ['p', ['aside', params, ['blockquote'].concat(contents)]]; - } - - const header = ['div', {'class': 'title'}, - ['div', {'class': 'quote-controls'}], - avatarImg ? ['__RAW', avatarImg] : "", - username ? `${username}:` : "" ]; - - if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) { - const topicInfo = options.getTopicInfo(topicId); - if (topicInfo) { - var href = topicInfo.href; - if (postNumber > 0) { href += "/" + postNumber; } - // get rid of username said stuff - header.pop(); - - let title = topicInfo.title; - - if (opts.enableEmoji) { - title = performEmojiUnescape(topicInfo.title, { - getURL: opts.getURL, emojiSet: opts.emojiSet - }); - } - - header.push(['a', {'href': href}, title]); - } - } - - return ['aside', params, header, ['blockquote'].concat(contents)]; - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 similarity index 93% rename from app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index 05afd6bc9e..06de98068f 100644 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 @@ -3,9 +3,10 @@ import { performEmojiUnescape } from 'pretty-text/emoji'; const rule = { tag: 'quote', - before: function(state, attrs, md) { + before: function(state, tagInfo) { - let options = md.options.discourse; + const attrs = tagInfo.attrs; + let options = state.md.options.discourse; let quoteInfo = attrs['_default']; let username, postNumber, topicId, avatarImg, full; @@ -26,7 +27,7 @@ const rule = { continue; } - if (split[i].indexOf(/full:\s*true/) === 0) { + if (/full:\s*true/.test(split[i])) { full = true; continue; } @@ -121,14 +122,12 @@ const rule = { export function setup(helper) { - if (!helper.markdownIt) { return; } - helper.registerOptions((opts, siteSettings) => { opts.enableEmoji = siteSettings.enable_emoji; opts.emojiSet = siteSettings.emoji_set; }); helper.registerPlugin(md=>{ - md.block.bbcode_ruler.push('quotes', rule); + md.block.bbcode.ruler.push('quotes', rule); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 index 1b148e6843..c3760799e0 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 @@ -1,35 +1,28 @@ -import { registerOption } from 'pretty-text/pretty-text'; - -function tableFlattenBlocks(blocks) { - let result = ""; - - blocks.forEach(b => { - result += b; - if (b.trailing) { result += b.trailing; } - }); - - // bypass newline insertion - return result.replace(/[\n\r]/g, " "); -}; - -registerOption((siteSettings, opts) => { - opts.features.table = !!siteSettings.allow_html_tables; -}); - export function setup(helper) { + // this is built in now + // TODO: sanitizer needs fixing, does not properly support this yet - if (helper.markdownIt) { return; } + // we need a custom callback for style handling + helper.whiteList({ + custom: function(tag,attr,val) { + if (tag !== 'th' && tag !== 'td') { + return false; + } - helper.whiteList(['table', 'table.md-table', 'tbody', 'thead', 'tr', 'th', 'td']); + if (attr !== 'style') { + return false; + } - helper.replaceBlock({ - start: /(]*>)([\S\s]*)/igm, - stop: /<\/table>/igm, - rawContents: true, - priority: 1, - - emitter(contents) { - return ['table', {"class": "md-table"}, tableFlattenBlocks.apply(this, [contents])]; + return (val === 'text-align:right' || val === 'text-align:left' || val === 'text-align:center'); } }); + + helper.whiteList([ + 'table', + 'tbody', + 'thead', + 'tr', + 'th', + 'td', + ]); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/text-post-process.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/text-post-process.js.es6 new file mode 100644 index 0000000000..38bea8abf1 --- /dev/null +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/text-post-process.js.es6 @@ -0,0 +1,127 @@ + +export class TextPostProcessRuler { + constructor() { + this.rules = []; + } + + getRules() { + return this.rules; + } + + // TODO error handling + getMatcher() { + if (this.matcher) { return this.matcher; } + + this.matcherIndex = []; + + let rules = this.rules.map((r) => + "(" + r.rule.matcher.toString().slice(1,-1) + ")" + ); + + let i; + let regexString = ""; + let last = 1; + + // this code is a bit tricky, our matcher may have multiple capture groups + // we want to dynamically determine how many + for(i=0; i0) { + regexString = regexString + '|'; + } + regexString = regexString + rules[i]; + + let regex = new RegExp(regexString + '|(x)'); + last = 'x'.match(regex).length - 1; + } + + this.matcher = new RegExp(rules.join('|'), 'g'); + return this.matcher; + } + + applyRule(buffer, match, state) { + let i; + for(i=0; i 0) { + if (!allowedBoundary(content, match.index-1, state.md.utils)) { + continue; + } + } + + // check forward boundary as well + if (match.index + match[0].length < content.length) { + if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) { + continue; + } + } + + if (match.index > pos) { + result = result || []; + let token = new state.Token('text', '', 0); + token.content = content.slice(pos, match.index); + result.push(token); + } + + result = result || []; + + ruler.applyRule(result, match, state); + + pos = match.index + match[0].length; + } + + if (result && pos < content.length) { + let token = new state.Token('text', '', 0); + token.content = content.slice(pos); + result.push(token); + } + + return result; +} + + +export function setup(helper) { + helper.registerPlugin(md => { + const ruler = md.core.textPostProcess.ruler; + const replacer = (content, state) => textPostProcess(content, state, ruler); + + md.core.ruler.push('text-post-process', state => + md.options.discourse.helpers.textReplace(state, replacer, true) + ); + }); +} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/category-hashtag.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/category-hashtag.js.es6 deleted file mode 100644 index b2c1e8ab44..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/category-hashtag.js.es6 +++ /dev/null @@ -1,106 +0,0 @@ -import { textReplace } from 'pretty-text/engines/markdown-it/helpers'; - -function addHashtag(buffer, matches, state) { - const options = state.md.options.discourse; - const [hashtag, slug] = matches; - const categoryHashtagLookup = options.categoryHashtagLookup; - const result = categoryHashtagLookup && categoryHashtagLookup(slug); - - let token; - - if (result) { - token = new state.Token('link_open', 'a', 1); - token.attrs = [['class', 'hashtag'], ['href', result[0]]]; - token.block = false; - buffer.push(token); - - token = new state.Token('text', '', 0); - token.content = '#'; - buffer.push(token); - - token = new state.Token('span_open', 'span', 1); - token.block = false; - buffer.push(token); - - token = new state.Token('text', '', 0); - token.content = result[1]; - buffer.push(token); - - buffer.push(new state.Token('span_close', 'span', -1)); - - buffer.push(new state.Token('link_close', 'a', -1)); - } else { - - token = new state.Token('span_open', 'span', 1); - token.attrs = [['class', 'hashtag']]; - buffer.push(token); - - token = new state.Token('text', '', 0); - token.content = hashtag; - buffer.push(token); - - token = new state.Token('span_close', 'span', -1); - buffer.push(token); - } -} - -const REGEX = /#([\w-:]{1,101})/gi; - -function allowedBoundary(content, index, utils) { - let code = content.charCodeAt(index); - return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code))); -} - -function applyHashtag(content, state) { - let result = null, - match, - pos = 0; - - while (match = REGEX.exec(content)) { - // check boundary - if (match.index > 0) { - if (!allowedBoundary(content, match.index-1, state.md.utils)) { - continue; - } - } - - // check forward boundary as well - if (match.index + match[0].length < content.length) { - if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) { - continue; - } - } - - if (match.index > pos) { - result = result || []; - let token = new state.Token('text', '', 0); - token.content = content.slice(pos, match.index); - result.push(token); - } - - result = result || []; - addHashtag(result, match, state); - - pos = match.index + match[0].length; - } - - if (result && pos < content.length) { - let token = new state.Token('text', '', 0); - token.content = content.slice(pos); - result.push(token); - } - - return result; -} - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerPlugin(md=>{ - - md.core.ruler.push('category-hashtag', state => textReplace( - state, applyHashtag, true /* skip all links */ - )); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/censored.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/censored.js.es6 deleted file mode 100644 index 3d5cb8d931..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/censored.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -import { censorFn } from 'pretty-text/censored-words'; - -function recurse(tokens, apply) { - let i; - for(i=0;i { - if (token.content) { - token.content = censor(token.content); - } - }); -} - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerOptions((opts, siteSettings) => { - opts.censoredWords = siteSettings.censored_words; - opts.censoredPattern = siteSettings.censored_pattern; - }); - - helper.registerPlugin(md => { - const words = md.options.discourse.censoredWords; - const patterns = md.options.discourse.censoredPattern; - - if ((words && words.length > 0) || (patterns && patterns.length > 0)) { - const replacement = String.fromCharCode(9632); - const censor = censorFn(words, patterns, replacement); - md.core.ruler.push('censored', state => censorTree(state, censor)); - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/code.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/code.js.es6 deleted file mode 100644 index c8d94967a1..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/code.js.es6 +++ /dev/null @@ -1,51 +0,0 @@ -// we need a custom renderer for code blocks cause we have a slightly non compliant -// format with special handling for text and so on - -const TEXT_CODE_CLASSES = ["text", "pre", "plain"]; - - -function render(tokens, idx, options, env, slf, md) { - let token = tokens[idx], - info = token.info ? md.utils.unescapeAll(token.info) : '', - langName = md.options.discourse.defaultCodeLang, - className, - escapedContent = md.utils.escapeHtml(token.content); - - if (info) { - // strip off any additional languages - info = info.split(/\s+/g)[0]; - } - - const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses; - if (acceptableCodeClasses && info && acceptableCodeClasses.indexOf(info) !== -1) { - langName = info; - } - - className = TEXT_CODE_CLASSES.indexOf(info) !== -1 ? 'lang-nohighlight' : 'lang-' + langName; - - return `
    ${escapedContent}
    \n`; -} - -export function setup(helper) { - if (!helper.markdownIt) { return; } - - helper.registerOptions((opts, siteSettings) => { - opts.defaultCodeLang = siteSettings.default_code_lang; - opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); - }); - - helper.whiteList({ - custom(tag, name, value) { - if (tag === 'code' && name === 'class') { - const m = /^lang\-(.+)$/.exec(value); - if (m) { - return helper.getOptions().acceptableCodeClasses.indexOf(m[1]) !== -1; - } - } - } - }); - - helper.registerPlugin(md=>{ - md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/emoji.js.es6 deleted file mode 100644 index dd85d24f34..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/emoji.js.es6 +++ /dev/null @@ -1,247 +0,0 @@ -import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji'; -import { translations } from 'pretty-text/emoji/data'; -import { textReplace } from 'pretty-text/engines/markdown-it/helpers'; - -const MAX_NAME_LENGTH = 60; - -let translationTree = null; - -// This allows us to efficiently search for aliases -// We build a data structure that allows us to quickly -// search through our N next chars to see if any match -// one of our alias emojis. -// -function buildTranslationTree() { - let tree = []; - let lastNode; - - Object.keys(translations).forEach(function(key){ - let i; - let node = tree; - - for(i=0;i 0) { - let prev = content.charCodeAt(pos-1); - if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) { - return; - } - } - - pos++; - if (content.charCodeAt(pos) === 58) { - return; - } - - let length = 0; - while(length < MAX_NAME_LENGTH) { - length++; - - if (content.charCodeAt(pos+length) === 58) { - // check for t2-t6 - if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) { - length += 3; - } - break; - } - - if (pos+length > content.length) { - return; - } - } - - if (length === MAX_NAME_LENGTH) { - return; - } - - return content.substr(pos, length); -} - -// straight forward :smile: to emoji image -function getEmojiTokenByName(name, state) { - - let info; - if (info = imageFor(name, state.md.options.discourse)) { - let token = new state.Token('emoji', 'img', 0); - token.attrs = [['src', info.url], - ['title', info.title], - ['class', info.classes], - ['alt', info.title]]; - - return token; - } -} - -function getEmojiTokenByTranslation(content, pos, state) { - - translationTree = translationTree || buildTranslationTree(); - - let currentTree = translationTree; - - let i; - let search = true; - let found = false; - let start = pos; - - while(search) { - - search = false; - let code = content.charCodeAt(pos); - - for (i=0;i 0) { - let leading = content.charAt(start-1); - if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) { - return; - } - } - - // check trailing for punct or space - if (pos < content.length) { - let trailing = content.charCodeAt(pos); - if (!state.md.utils.isSpace(trailing)){ - return; - } - } - - let token = getEmojiTokenByName(found, state); - if (token) { - return { pos, token }; - } -} - -function applyEmoji(content, state, emojiUnicodeReplacer) { - let i; - let result = null; - let contentToken = null; - - let start = 0; - - if (emojiUnicodeReplacer) { - content = emojiUnicodeReplacer(content); - } - - let endToken = content.length; - - for (i=0; i0) { - contentToken = new state.Token('text', '', 0); - contentToken.content = content.slice(start,i); - result.push(contentToken); - } - - result.push(token); - endToken = start = i + offset; - } - } - - if (endToken < content.length) { - contentToken = new state.Token('text', '', 0); - contentToken.content = content.slice(endToken); - result.push(contentToken); - } - - return result; -} - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerOptions((opts, siteSettings, state)=>{ - opts.features.emoji = !!siteSettings.enable_emoji; - opts.emojiSet = siteSettings.emoji_set || ""; - opts.customEmoji = state.customEmoji; - }); - - helper.registerPlugin((md)=>{ - md.core.ruler.push('emoji', state => textReplace( - state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer)) - ); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/mentions.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/mentions.js.es6 deleted file mode 100644 index 602af3c15a..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/mentions.js.es6 +++ /dev/null @@ -1,88 +0,0 @@ -const regex = /^(\w[\w.-]{0,59})\b/i; - -function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) { - - let pos = state.pos; - - // 64 = @ - if (silent || state.src.charCodeAt(pos) !== 64) { - return false; - } - - if (pos > 0) { - let prev = state.src.charCodeAt(pos-1); - if (!isWhiteSpace(prev) && !isPunctChar(String.fromCharCode(prev))) { - return false; - } - } - - // skip if in a link - if (state.tokens) { - let last = state.tokens[state.tokens.length-1]; - if (last) { - if (last.type === 'link_open') { - return false; - } - if (last.type === 'html_inline' && last.content.substr(0,2) === " { - md.inline.ruler.push('mentions', (state,silent)=> applyMentions( - state, - silent, - md.utils.isWhiteSpace, - md.utils.isPunctChar, - md.options.discourse.mentionLookup, - md.options.discourse.getURL - )); - }); -} - diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/newline.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/newline.js.es6 deleted file mode 100644 index f1eb2ba759..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/newline.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -// see: https://github.com/markdown-it/markdown-it/issues/375 -// -// we use a custom paragraph rule cause we have to signal when a -// link starts with a space, so we can bypass a onebox -// this is a freedom patch, so careful, may break on updates - - -function newline(state, silent) { - var token, pmax, max, pos = state.pos; - - if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } - - pmax = state.pending.length - 1; - max = state.posMax; - - // ' \n' -> hardbreak - // Lookup in pending chars is bad practice! Don't copy to other rules! - // Pending string is stored in concat mode, indexed lookups will cause - // convertion to flat mode. - if (!silent) { - if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { - if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { - state.pending = state.pending.replace(/ +$/, ''); - token = state.push('hardbreak', 'br', 0); - } else { - state.pending = state.pending.slice(0, -1); - token = state.push('softbreak', 'br', 0); - } - - } else { - token = state.push('softbreak', 'br', 0); - } - } - - pos++; - - // skip heading spaces for next line - while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) { - if (token) { - token.leading_space = true; - } - pos++; - } - - state.pos = pos; - return true; -}; - -export function setup(helper) { - helper.registerPlugin(md => { - md.inline.ruler.at('newline', newline); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/onebox.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/onebox.js.es6 deleted file mode 100644 index 5f04b72b1d..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/onebox.js.es6 +++ /dev/null @@ -1,89 +0,0 @@ -import { lookupCache } from 'pretty-text/oneboxer'; - -function applyOnebox(state, silent) { - if (silent || !state.tokens || state.tokens.length < 3) { - return; - } - - let i; - for(i=1;i 0) { - let prevSibling = token.children[j-1]; - - if (prevSibling.tag !== 'br' || prevSibling.leading_space) { - continue; - } - } - - // look ahead for soft or hard break - let text = token.children[j+1]; - let close = token.children[j+2]; - let lookahead = token.children[j+3]; - - if (lookahead && lookahead.tag !== 'br') { - continue; - } - - // check attrs only include a href - let attrs = child["attrs"]; - - if (!attrs || attrs.length !== 1 || attrs[0][0] !== "href") { - continue; - } - - // we already know text matches cause it is an auto link - - if (!close || close.type !== "link_close") { - continue; - } - - // we already determined earlier that 0 0 was href - let cached = lookupCache(attrs[0][1]); - - if (cached) { - // replace link with 2 blank text nodes and inline html for onebox - child.type = 'html_raw'; - child.content = cached; - child.inline = true; - - text.type = 'html_raw'; - text.content = ''; - text.inline = true; - - close.type = 'html_raw'; - close.content = ''; - close.inline = true; - - } else { - // decorate... - attrs.push(["class", "onebox"]); - } - } - } - } - } -} - - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerPlugin(md => { - md.core.ruler.after('linkify', 'onebox', applyOnebox); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/table.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/table.js.es6 deleted file mode 100644 index 4bb5ef92d6..0000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/table.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - // this is built in now - // TODO: sanitizer needs fixing, does not properly support this yet - - // we need a custom callback for style handling - helper.whiteList({ - custom: function(tag,attr,val) { - if (tag !== 'th' && tag !== 'td') { - return false; - } - - if (attr !== 'style') { - return false; - } - - return (val === 'text-align:right' || val === 'text-align:left' || val === 'text-align:center'); - } - }); - - helper.whiteList([ - 'table', - 'tbody', - 'thead', - 'tr', - 'th', - 'td', - ]); -} diff --git a/app/assets/javascripts/pretty-text/inline-oneboxer.js.es6 b/app/assets/javascripts/pretty-text/inline-oneboxer.js.es6 new file mode 100644 index 0000000000..b8be79f60e --- /dev/null +++ b/app/assets/javascripts/pretty-text/inline-oneboxer.js.es6 @@ -0,0 +1,19 @@ +let _cache = {}; + +export function applyInlineOneboxes(inline, ajax) { + return ajax("/inline-onebox", { + data: { urls: Object.keys(inline) }, + }).then(result => { + result['inline-oneboxes'].forEach(onebox => { + _cache[onebox.url] = onebox; + let links = inline[onebox.url] || []; + links.forEach(link => { + link.text(onebox.title); + }); + }); + }); +}; + +export function cachedInlineOnebox(url) { + return _cache[url]; +} diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index f2c4633d20..68fd360dc6 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -1,13 +1,10 @@ -import { cook, setup } from 'pretty-text/engines/discourse-markdown'; import { cook as cookIt, setup as setupIt } from 'pretty-text/engines/discourse-markdown-it'; -import { sanitize } from 'pretty-text/sanitizer'; -import WhiteLister from 'pretty-text/white-lister'; -const _registerFns = []; -const identity = value => value; - -export function registerOption(fn) { - _registerFns.push(fn); +export function registerOption() { + // TODO next major version deprecate this + // if (window.console) { + // window.console.log("registerOption is deprecated"); + // } } export function buildOptions(state) { @@ -22,14 +19,14 @@ export function buildOptions(state) { getCurrentUser, currentUser, lookupAvatarByPostNumber, - emojiUnicodeReplacer + emojiUnicodeReplacer, + lookupInlineOnebox, + previewing, + linkify, + censoredWords } = state; - if (!siteSettings.enable_experimental_markdown_it) { - setup(); - } - - const features = { + let features = { 'bold-italics': true, 'auto-link': true, 'mentions': true, @@ -38,9 +35,14 @@ export function buildOptions(state) { 'html': true, 'category-hashtag': true, 'onebox': true, + 'linkify': linkify !== false, 'newline': !siteSettings.traditional_markdown_linebreaks }; + if (state.features) { + features = _.merge(features, state.features); + } + const options = { sanitize: true, getURL, @@ -55,45 +57,41 @@ export function buildOptions(state) { lookupAvatarByPostNumber, mentionLookup: state.mentionLookup, emojiUnicodeReplacer, + lookupInlineOnebox, + censoredWords, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, - markdownIt: siteSettings.enable_experimental_markdown_it + markdownIt: true, + previewing }; - if (siteSettings.enable_experimental_markdown_it) { - setupIt(options, siteSettings, state); - } else { - // TODO deprecate this - _registerFns.forEach(fn => fn(siteSettings, options, state)); - } + // note, this will mutate options due to the way the API is designed + // may need a refactor + setupIt(options, siteSettings, state); return options; } export default class { constructor(opts) { - this.opts = opts || {}; - this.opts.features = this.opts.features || {}; - this.opts.sanitizer = (!!this.opts.sanitize) ? (this.opts.sanitizer || sanitize) : identity; - // We used to do a failsafe call to setup here - // under new engine we always expect setup to be called by buildOptions. - // setup(); + if (!opts) { + opts = buildOptions({ siteSettings: {}}); + } + this.opts = opts; + } + + disableSanitizer() { + this.opts.sanitizer = this.opts.discourse.sanitizer = ident => ident; } cook(raw) { if (!raw || raw.length === 0) { return ""; } let result; - - if (this.opts.markdownIt) { - result = cookIt(raw, this.opts); - } else { - result = cook(raw, this.opts); - } - + result = cookIt(raw, this.opts); return result ? result : ""; } sanitize(html) { - return this.opts.sanitizer(html, new WhiteLister(this.opts)); + return this.opts.sanitizer(html).trim(); } }; diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index e1c1d56a4a..30f8f23c96 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -1,120 +1,118 @@ -const masterList = {}; -const masterCallbacks = {}; - -const _whiteLists = {}; -const _callbacks = {}; - -function concatUniq(src, elems) { - src = src || []; - if (!Array.isArray(elems)) { - elems = [elems]; - } - return src.concat(elems.filter(e => src.indexOf(e) === -1)); -} +// to match: +// abcd +// abcd[test] +// abcd[test=bob] +const WHITELIST_REGEX = /([^\[]+)(\[([^=]+)(=(.*))?\])?/; export default class WhiteLister { constructor(options) { - options = options || { - features: { - default: true - } - }; - options.features.default = true; + this._enabled = { "default": true }; + this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || []; + this._rawFeatures = [["default", DEFAULT_LIST]]; - this._featureKeys = Object.keys(options.features).filter(f => options.features[f]); - this._key = this._featureKeys.join(':'); - this._features = options.features; - this._options = options; + this._cache = null; + + if (options && options.features) { + Object.keys(options.features).forEach(f => { + if (options.features[f]) { + this._enabled[f] = true; + } + }); + } } - getCustom() { - if (!_callbacks[this._key]) { - const callbacks = []; - this._featureKeys.forEach(f => { - (masterCallbacks[f] || []).forEach(cb => callbacks.push(cb)); - }); - _callbacks[this._key] = callbacks; - } + whiteListFeature(feature, info) { + this._rawFeatures.push([feature, info]); + } - return _callbacks[this._key]; + disable(feature) { + this._enabled[feature] = false; + this._cache = null; + } + + enable(feature) { + this._enabled[feature] = true; + this._cache = null; + } + + _buildCache() { + const tagList = {}; + const attrList = {}; + const custom = []; + + this._rawFeatures.forEach(([name, info]) => { + if (!this._enabled[name]) return; + + if (info.custom) { + custom.push(info.custom); + return; + } + + if (typeof info === "string") { + info = [info]; + } + + (info || []).forEach(tag => { + const classes = tag.split('.'); + const tagWithAttr = classes.shift(); + + const m = WHITELIST_REGEX.exec(tagWithAttr); + if (m) { + + const [,tagname,,attr,,val] = m; + tagList[tagname] = []; + + let attrs = attrList[tagname] = attrList[tagname] || {}; + if (classes.length > 0) { + attrs["class"] = (attrs["class"] || []).concat(classes); + } + + if (attr) { + let attrInfo = attrs[attr] = attrs[attr] || []; + + if (val) { + attrInfo.push(val); + } else { + attrs[attr] = ["*"]; + } + } + } + }); + }); + + + this._cache = {custom, whiteList: {tagList, attrList}}; + } + + _ensureCache() { + if (!this._cache) { this._buildCache(); } } getWhiteList() { - if (!_whiteLists[this._key]) { - const tagList = {}; - let attrList = {}; + this._ensureCache(); + return this._cache.whiteList; + } - // merge whitelists for these features - this._featureKeys.forEach(f => { - const info = masterList[f] || {}; - Object.keys(info).forEach(t => { - tagList[t] = []; - attrList[t] = attrList[t] || {}; - - const attrs = info[t]; - Object.keys(attrs).forEach(a => attrList[t][a] = concatUniq(attrList[t][a], attrs[a])); - }); - }); - - _whiteLists[this._key] = { tagList, attrList }; - } - return _whiteLists[this._key]; + getCustom() { + this._ensureCache(); + return this._cache.custom; } getAllowedHrefSchemes() { - return this._options.allowedHrefSchemes || []; + return this._allowedHrefSchemes; } } -// Builds our object that represents whether something is sanitized for a particular feature. -export function whiteListFeature(feature, info) { - const featureInfo = {}; - - // we can supply a callback instead - if (info.custom) { - masterCallbacks[feature] = masterCallbacks[feature] || []; - masterCallbacks[feature].push(info.custom); - return; - } - - if (typeof info === "string") { info = [info]; } - - (info || []).forEach(tag => { - const classes = tag.split('.'); - const tagName = classes.shift(); - const m = /\[([^\]]+)]/.exec(tagName); - if (m) { - const [full, inside] = m; - const stripped = tagName.replace(full, ''); - const vals = inside.split('='); - - featureInfo[stripped] = featureInfo[stripped] || {}; - if (vals.length === 2) { - const [name, value] = vals; - featureInfo[stripped][name] = value; - } else { - featureInfo[stripped][inside] = '*'; - } - } - - featureInfo[tagName] = featureInfo[tagName] || {}; - if (classes.length) { - featureInfo[tagName]['class'] = concatUniq(featureInfo[tagName]['class'], classes); - } - }); - - masterList[feature] = featureInfo; -} - // Only add to `default` when you always want your whitelist to occur. In other words, // don't change this for a plugin or a feature that can be disabled -whiteListFeature('default', [ +const DEFAULT_LIST = [ 'a.attachment', 'a.hashtag', 'a.mention', 'a.mention-group', 'a.onebox', + 'a.inline-onebox-loading', 'a[data-bbcode]', 'a[name]', 'a[rel=nofollow]', @@ -174,4 +172,4 @@ whiteListFeature('default', [ 'sub', 'sup', 'ul', -]); +]; diff --git a/app/assets/javascripts/wizard-vendor.js b/app/assets/javascripts/wizard-vendor.js index 2236d33669..ecb8d2bd7e 100644 --- a/app/assets/javascripts/wizard-vendor.js +++ b/app/assets/javascripts/wizard-vendor.js @@ -3,3 +3,5 @@ //= require jquery.ui.widget.js //= require jquery.fileupload.js //= require sweetalert.js +//= require virtual-dom +//= require virtual-dom-amd diff --git a/app/assets/javascripts/wizard/templates/components/invite-list-user.hbs b/app/assets/javascripts/wizard/templates/components/invite-list-user.hbs index b957827ad1..c31d80349a 100644 --- a/app/assets/javascripts/wizard/templates/components/invite-list-user.hbs +++ b/app/assets/javascripts/wizard/templates/components/invite-list-user.hbs @@ -2,5 +2,5 @@ {{roleName}} diff --git a/app/assets/javascripts/wizard/templates/components/invite-list.hbs b/app/assets/javascripts/wizard/templates/components/invite-list.hbs index 6b38377668..718ef5bd16 100644 --- a/app/assets/javascripts/wizard/templates/components/invite-list.hbs +++ b/app/assets/javascripts/wizard/templates/components/invite-list.hbs @@ -15,6 +15,6 @@ {{combo-box value=inviteRole content=roles nameProperty="label" width="200px"}}
    diff --git a/app/assets/javascripts/wizard/templates/components/radio-button.hbs b/app/assets/javascripts/wizard/templates/components/radio-button.hbs index 3973d91324..e93347513a 100644 --- a/app/assets/javascripts/wizard/templates/components/radio-button.hbs +++ b/app/assets/javascripts/wizard/templates/components/radio-button.hbs @@ -2,7 +2,7 @@ {{#if icon}} - {{fa-icon icon}} + {{d-icon icon}} {{/if}} {{label}} diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs index 209f344e0f..05f1cb2951 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs @@ -7,7 +7,7 @@ {{i18n "wizard.uploading"}} {{else}} {{i18n "wizard.upload"}} - {{fa-icon "picture-o"}} + {{d-icon "picture-o"}} {{/if}} diff --git a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs index 9c945cf60a..b7371dc1b5 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs @@ -39,7 +39,7 @@ {{#if showNextButton}} {{/if}} diff --git a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 index ca42183bb6..3ea019a062 100644 --- a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 +++ b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 @@ -1,7 +1,7 @@ import startApp from 'wizard/test/helpers/start-app'; var wizard; -module("Acceptance: wizard", { +QUnit.module("Acceptance: wizard", { beforeEach() { wizard = startApp(); }, diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index de075cb545..6ad6b69dce 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -36,8 +36,8 @@ $mobile-breakpoint: 700px; tr {text-align: left;} td, th {padding: 8px;} td { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; + border-top: 1px solid $primary-low; } th { text-align: left; @@ -51,8 +51,8 @@ $mobile-breakpoint: 700px; background-color: lighten($primary, 80%); } - i.fa-chevron-down, - i.fa-chevron-up { + .d-icon-chevron-down, + .d-icon-chevron-up { margin-left: 0.5em; } } @@ -94,11 +94,11 @@ td.flaggers td { .site-text { cursor: pointer; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; margin-bottom: 0.5em; &.overridden { - background-color: dark-light-diff($highlight, $secondary, 50%, -60%); + background-color: $highlight-medium; } h3 { @@ -112,7 +112,7 @@ td.flaggers td { .site-text-value { margin: 0.5em 5em 0.5em 0; max-height: 100px; - color: dark-light-diff($primary, $secondary, 40%, -10%); + color: $primary-medium; } } @@ -136,7 +136,7 @@ td.flaggers td { font-size: 0.857em; float: right; margin-right: 10px; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; padding: 2px 5px; border-radius: 5px; color: $primary; @@ -222,16 +222,16 @@ td.flaggers td { } .admin-controls { - background-color: dark-light-diff($primary, $secondary, 90%, -65%); + background-color: $primary-low; padding: 10px 10px 3px 0; @include clearfix; .nav.nav-pills { li.active { a { - border-color: dark-light-diff($primary, $secondary, 90%, -90%); - background-color: dark-light-diff($primary, $secondary, 40%, -10%); + border-color: $primary-low; + background-color: $primary-medium; &:hover { - background-color: dark-light-diff($primary, $secondary, 40%, -10%); + background-color: $primary-medium; } } } @@ -396,7 +396,7 @@ td.flaggers td { // Todo: set this properly - it needs to be >= the menu height min-height: 875px; margin-left: 0; - border-left: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); + border-left: solid 1px $primary-low; padding: 30px 0 30px 30px; @media (max-width: $mobile-breakpoint) { padding: 30px 0; @@ -461,7 +461,7 @@ td.flaggers td { .setting-controls { float: left; } - .input-setting-string { + .input-setting-string, .input-setting-textarea { box-sizing: border-box; height: 30px; width: 100%; @@ -469,13 +469,16 @@ td.flaggers td { width: 100%; } } + .input-setting-textarea { + height: 150px; + } .input-setting-list { @media (max-width: $mobile-breakpoint) { width: 100%; } padding: 1px; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3); transition: border linear 0.2s, box-shadow linear 0.2s; @@ -531,8 +534,8 @@ td.flaggers td { } .setting.overridden.string { - input[type=text] { - background-color: dark-light-diff($highlight, $secondary, 50%, -60%); + input[type=text], textarea { + background-color: $highlight-medium; } } } @@ -543,7 +546,7 @@ section.details { color: $primary; padding: 5px 10px; margin: 30px 0 5px 0; - border-bottom: 5px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 5px solid $primary-low; } } @@ -586,7 +589,7 @@ section.details { &.highlight-danger { background-color: scale-color($danger, $lightness: 50%); } - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; &:before, &:after { display: table; content: ""; @@ -706,14 +709,14 @@ section.details { font-size: 1em; line-height: 16px; padding: 4px; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } .badge-query-plan { font-size: 0.857em; line-height: 13px; padding: 4px; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } .count-warning { @@ -776,8 +779,8 @@ section.details { background-color: scale-color($danger, $lightness: 70%); } - .message { background-color: dark-light-diff($highlight, $secondary, 50%, -70%); } - .message:hover { background-color: dark-light-diff($highlight, $secondary, 60%, -60%); } + .message { background-color: $highlight-medium; } + .message:hover { background-color: $highlight-low; } .flagged-post-avatar { margin-right: 10px; @@ -866,7 +869,7 @@ section.details { } .update-nag { - i.fa { + .d-icon { font-size: 1.429em; } } @@ -901,10 +904,10 @@ table.api-keys { width: 100%; .title { - i.fa { + .d-icon { color: $primary; } - i.fa-heart { + .d-icon-heart { color: $love; } } @@ -912,7 +915,7 @@ table.api-keys { th { font-weight: normal; text-align: center; - background: dark-light-diff($primary, $secondary, 80%, -65%); + background: $primary-low; } th.title { text-align: left; @@ -962,7 +965,7 @@ table.api-keys { } &.detected-problems { - background: dark-light-diff($primary, $secondary, 90%, -75%); + background: $primary-low; margin-bottom: 20px; .look-here { @@ -987,7 +990,7 @@ table.api-keys { text-align: right; } .btn { - background: dark-light-diff($primary, $secondary, 80%, -85%); + background: $primary-low; } ul { margin-left: 0; @@ -1037,7 +1040,7 @@ table.api-keys { } .commits-widget { - border: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); + border: solid 1px $primary-low; height: 180px; margin-bottom: 36px; @@ -1060,7 +1063,7 @@ table.api-keys { color: $primary; font-weight: bold; height: 30px; - background: dark-light-diff($primary, $secondary, 80%, -75%); + background: $primary-low; cursor: pointer; h1 { @@ -1083,7 +1086,7 @@ table.api-keys { @extend .clearfix; line-height: 1.0em; padding: 6px 8px; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; .left { float: left; } @@ -1093,7 +1096,7 @@ table.api-keys { img { margin-top: 2px; - border: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); + border: solid 1px $primary-low; padding: 2px; background-color: $secondary; } @@ -1124,11 +1127,11 @@ table.api-keys { width: 6px; } ::-webkit-scrollbar-thumb { - background: dark-light-diff($primary, $secondary, 90%, -75%); + background: $primary-low; -webkit-border-radius: 3px; } ::-webkit-scrollbar-track { - border-left: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); + border-left: solid 1px $primary-low; } } @@ -1303,7 +1306,7 @@ table.api-keys { color: $primary; &:hover { color: $primary; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } @@ -1333,7 +1336,7 @@ table.api-keys { .heading-container { width: 100%; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } .col.heading { font-weight: bold; @@ -1349,7 +1352,7 @@ table.api-keys { .ember-list-item-view { width: 100%; - border-top: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); + border-top: solid 1px $primary-low; } } @@ -1360,10 +1363,10 @@ table.api-keys { } .tl3-requirements { - .fa-check { + .d-icon-check { color: $success; } - .fa-times { + .d-icon-times { color: $danger; } } @@ -1539,7 +1542,7 @@ tr.not-activated { .user-field { padding: 10px; margin-bottom: 10px; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; .form-display { width: 25%; @@ -1844,6 +1847,47 @@ table#user-badges { } } +.watched-word-box { + display: inline-block; + width: 250px; + margin-bottom: 1em; + float: left; +} + +.watched-words-list { + margin-top: 40px; +} +.watched-word { + display: inline-block; + cursor: pointer; + .d-icon { + margin-right: 0.25em; + color: dark-light-diff($primary, $secondary, 50%, -50%); + } + &:hover .d-icon { + color: $primary; + } +} +.watched-word-form { + display: inline-block; + .success-message { + margin-left: 1em; + } +} +.watched-words-uploader { + float: right; + text-align: right; + .instructions { + font-size: 12px; + } +} +.watched-words-detail { + .about { + margin-top: 24px; + margin-bottom: 40px; + } +} + // Mobile specific styles // Mobile view text-inputs need some padding .mobile-view .admin-contents { @@ -1929,11 +1973,11 @@ table#user-badges { .dbox20 { background: dark-light-diff($primary, $secondary, 20%, -20%); } .dbox30 { background: dark-light-diff($primary, $secondary, 30%, -30%); } .dbox40 { background: dark-light-diff($primary, $secondary, 40%, -40%); } -.dbox50 { background: dark-light-diff($primary, $secondary, 50%, -50%); } +.dbox50 { background: blend-primary-secondary(50%); } .dbox60 { background: dark-light-diff($primary, $secondary, 60%, -60%); } .dbox70 { background: dark-light-diff($primary, $secondary, 70%, -70%); } .dbox80 { background: dark-light-diff($primary, $secondary, 80%, -80%); } -.dbox90 { background: dark-light-diff($primary, $secondary, 90%, -90%); } +.dbox90 { background: $primary-low; } .dbox100 { background: dark-light-diff($primary, $secondary, 100%, -100%); } .dbox5 { background: dark-light-diff($primary, $secondary, 5%, -5%); } .dbox15 { background: dark-light-diff($primary, $secondary, 15%, -15%); } diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 9a62dcff91..1ba3bbc507 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -10,7 +10,7 @@ .field-error { margin-top: 10px; margin-bottom: 10px; - background-color: dark-light-diff($quaternary, $secondary, 70%, -70%); + background-color: $quaternary-low; padding: 5px; } @@ -98,7 +98,7 @@ li.mobile a { padding-right: 25px; } - .fa-mobile { + .d-icon-mobile { position: absolute; right: 10px; top: 3px; @@ -210,4 +210,3 @@ margin-top: 20px; } } - diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index cb1bccb1fc..73460e47c3 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -16,8 +16,8 @@ } } -html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-light-diff($primary, $secondary, 45%, -30%);} -.topic-list a.title.visited:not(.badge-notification) {color: dark-light-diff($primary, $secondary, 45%, -30%);} +html.anon .topic-list a.title:visited:not(.badge-notification) {color: $primary-medium; } +.topic-list a.title.visited:not(.badge-notification) {color: $primary-medium; } .topic-list { width: 100%; @@ -28,7 +28,7 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh vertical-align: top; margin-top: 2px; } - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-bottom: 1px solid $primary-low; &.last-visit { border-bottom: none; @@ -58,7 +58,7 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh } > tbody > tr:first-of-type { - border-top: 3px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 3px solid $primary-low; } th, @@ -72,7 +72,7 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; font-size: 1em; - button i.fa {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));} + button .d-icon {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));} } td { @@ -238,7 +238,7 @@ ol.category-breadcrumb { overflow-x: hidden; overflow-y: auto; position: absolute; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; background-color: $secondary; z-index: 100; @@ -247,7 +247,7 @@ ol.category-breadcrumb { overflow:hidden; margin-bottom: 0; } - a.badge-category { + a.badge-category, .dropdown-header { font-size: 0.929em; font-weight: bold; float: none; @@ -257,7 +257,7 @@ ol.category-breadcrumb { } } -.fa-thumb-tack.unpinned { +.d-icon-thumb-tack.unpinned { @include fa-icon-rotate(180deg, 1); color: $primary; /* because it is rotated, right becomes left! */ @@ -300,7 +300,7 @@ ol.category-breadcrumb { @include unselectable; font-size: 1.2em; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 5px; background: $secondary; position: absolute; @@ -326,7 +326,7 @@ ol.category-breadcrumb { font-size: 0.8em; } &:hover { - background-color: dark-light-diff($highlight, $secondary, 50%, -70%); + background-color: $highlight-medium; } } } diff --git a/app/assets/stylesheets/common/base/alert.scss b/app/assets/stylesheets/common/base/alert.scss index 2afb3d10ed..892151a3b2 100644 --- a/app/assets/stylesheets/common/base/alert.scss +++ b/app/assets/stylesheets/common/base/alert.scss @@ -30,15 +30,15 @@ -webkit-appearance: none; } &.alert-success { - background-color: dark-light-diff($success, $secondary, 50%, -60%); + background-color: $success-low; color: $primary; } &.alert-error { - background-color: rgba(dark-light-diff($danger, $secondary, 50%, -60%), .5); + background-color: rgba($danger-low, .5); color: $primary; } &.alert-info { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; color: $primary; &.clickable { color: $tertiary; diff --git a/app/assets/stylesheets/common/base/colorpicker.scss b/app/assets/stylesheets/common/base/colorpicker.scss index 6bcf8ae659..83f29f7853 100644 --- a/app/assets/stylesheets/common/base/colorpicker.scss +++ b/app/assets/stylesheets/common/base/colorpicker.scss @@ -18,7 +18,7 @@ max-width: 300px; .colorpicker { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; margin-right: 2px; width: 16px; height: 16px; diff --git a/app/assets/stylesheets/common/base/combobox.scss b/app/assets/stylesheets/common/base/combobox.scss index cd9f44b52c..4c80abc5c6 100644 --- a/app/assets/stylesheets/common/base/combobox.scss +++ b/app/assets/stylesheets/common/base/combobox.scss @@ -1,5 +1,5 @@ .select2-results .select2-highlighted { - background: dark-light-diff($highlight, $secondary, 50%, -80%); + background: $highlight-medium; color: $primary; } @@ -25,7 +25,7 @@ .select2-drop { background: $secondary; - i.fa { + .d-icon { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } @@ -36,7 +36,7 @@ .select2-container { border-radius: 3px; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; min-width: 200px; &.select2-dropdown-open { @@ -71,7 +71,7 @@ border-color: $tertiary; } .select2-drop { - color: dark-light-diff($primary, $secondary, -50%, 50%); + color: $primary; } .select2-drop-active { border: 1px solid $tertiary; diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 9f86c23d94..233519fcc8 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -3,19 +3,19 @@ position: absolute; width: 240px; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; ul { list-style: none; padding: 0; margin: 0; li { - .fa-users { + .d-icon-users { color: lighten($primary, 40%); padding: 0 2px; } - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; a { padding: 5px; @@ -32,10 +32,10 @@ vertical-align: middle; } &.selected { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; } @include hover { - background-color: dark-light-diff($highlight, $secondary, 90%, -80%); + background-color: $highlight-low; text-decoration: none; } } @@ -52,11 +52,13 @@ .d-editor-button-bar { -moz-box-sizing: border-box; box-sizing: border-box; - margin: 0px; padding: 5px; - border-bottom: 2px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 2px solid $primary-low; height: 33px; + .btn:hover { + color: $primary-low; + } } textarea { @@ -88,7 +90,7 @@ div.ac-wrap div.item a.remove, .remove-link { border-radius: 12px; width: 10px; display: inline-block; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; &:hover { background-color: scale-color($danger, $lightness: 75%); border: 1px solid scale-color($danger, $lightness: 30%); @@ -101,7 +103,7 @@ div.ac-wrap { overflow: auto; max-height: 150px; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 5px 4px 1px 4px; div.item { float: left; diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss index 6af69033aa..8a31885b8c 100644 --- a/app/assets/stylesheets/common/base/directory.scss +++ b/app/assets/stylesheets/common/base/directory.scss @@ -12,7 +12,7 @@ float: right; } .total-rows { - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); text-align: right; } .spinner { @@ -26,17 +26,17 @@ td, th { padding: 0.5em; text-align: left; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; .number, .time-read { font-size: 1.4em; - color: dark-light-diff($primary, $secondary, 50%, -20%); + color: $primary-medium; } } tr.me { td { - background-color: dark-light-diff($highlight, $secondary, 70%, -80%); + background-color: $highlight-low; .username a, .name, .title, .number, .time-read { color: dark-light-choose(scale-color($highlight, $lightness: -50%), scale-color($highlight, $lightness: 50%)); @@ -49,16 +49,16 @@ white-space: nowrap; width: 13%; - i.fa-heart { + .d-icon-heart { color: $love; margin-right: 0.5em; } - i.fa-chevron-down, i.fa-chevron-up { + .d-icon-chevron-down, .d-icon-chevron-up { margin-left: 0.5em; } &:hover { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } } } diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 1aeaf7cfe3..1960f4b603 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -314,7 +314,7 @@ body { animation: rotate-forever 1s infinite linear; height: 30px; width: 30px; - border: 4px solid dark-light-diff($primary, $secondary, 50%, -50%); + border: 4px solid blend-primary-secondary(50%); border-right-color: transparent; border-radius: 50%; @@ -329,7 +329,7 @@ body { .content-list { h3 { - color: dark-light-diff($primary, $secondary, 50%, -20%); + color: $primary-medium; font-size: 1.071em; padding-left: 5px; margin-bottom: 10px; @@ -340,10 +340,10 @@ body { margin: 0; li:first-of-type { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; } li { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } li a { @@ -352,7 +352,7 @@ body { color: $primary; &:hover { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; color: $primary; } @@ -380,7 +380,7 @@ span.relative-date { @keyframes background-fade-highlight { 0% { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; } 100% { background-color: transparent; diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index 10b1a6812b..df240bc38f 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -4,125 +4,117 @@ body img.emoji { vertical-align: middle; } -.wmd-emoji-button:before { - content: "\f118"; -} - -.emoji-modal { +.emoji-picker { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background-clip: padding-box; z-index: 10000; position: fixed; - left: 50%; - top: 50%; - width: 445px; - min-height: 264px; - margin-top: -132px; - margin-left: -222px; - background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); + display: none; + flex-direction: row; + height: 300px; + border-radius: 3px; + color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%)); + background-color: $secondary; + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); +} + +.emoji-picker .categories-column { display: flex; flex-direction: column; + flex: 1; + align-items: center; + justify-content: space-between; + border-right: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + min-width: 36px; +} + +.emoji-picker .category-icon { + display: block; + margin: 4px auto; + -webkit-filter: grayscale(100%); + filter: grayscale(100%); + + button.emoji { + margin: 0; + padding: 0; + } +} + +.emoji-picker .category-icon.current, .emoji-picker .category-icon:hover { + -webkit-filter: grayscale(0%); + filter: grayscale(0%); +} + +.emoji-picker .main-column { + display: flex; + flex-direction: column; + flex: 20; +} + +.emoji-picker .list { + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + padding: 0px; + flex: 1; + flex-direction: column; } -table.emoji-page td { - border: 1px solid transparent; - background-color: dark-light-choose(white, $secondary); - padding: 0 !important; -} - -.emoji-page a { +.emoji-picker .section-header { padding: 8px; - display: block; - border-radius: 20px; -} - -.emoji-page a:hover { - background-color: dark-light-choose(rgb(210, 236, 252), rgb(45, 19, 3)); -} - -.emoji-table-wrapper { - min-width: 442px; - min-height: 185px; - background-color: $secondary; -} - -.emoji-modal-wrapper { - z-index: 9999; - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - opacity: dark-light-choose(0.8, 0.5); - background-color: black; -} - -.emoji-modal .toolbar { - margin: 8px 0 5px; -} - -.emoji-modal .toolbar li { - display: inline; - padding-right: 1px; -} - -.emoji-modal .toolbar li a { - padding: 8px; - background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); -} - -.emoji-modal .toolbar li a.selected { - background-color: $secondary; -} - -.emoji-modal .nav span { - color: dark-light-choose(#aaa, #555); -} - -.emoji-modal .nav span.next { - margin-left: 10px; -} - -.emoji-modal .nav a { - color: dark-light-choose(#333, #ccc); -} - -.emoji-shortname { - display: inline-block; - max-width: 200px; - text-overflow: ellipsis; - overflow: hidden; - vertical-align: middle; -} - -.emoji-modal .footer { + margin-bottom: 4px; + padding-bottom: 4px; + border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + justify-content: space-between; display: flex; align-items: center; - justify-content: space-between; - flex-direction: row; - flex-grow: 2; - padding: 8px; } -.emoji-modal .info { - flex: 10; -} - -.emoji-modal .info span { - margin-left: 5px; - font-weight: bold; +.emoji-picker .section-header .title { color: $primary; } -.emoji-modal .nav { - margin-left: 10px; +.emoji-picker .section-header .clear-recent .fa{ + margin: 0; + padding: 0; + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); + + &:hover { + color: $primary; + } } -.emoji-modal .tones { +.emoji-picker .section-group { + flex-wrap: wrap; display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; + padding: 4px; } -.emoji-modal .tones-button { +.emoji-picker .footer { + align-items: center; + display: flex; + justify-content: space-between; + border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); +} + +.emoji-picker .info { + text-overflow: ellipsis; + max-width: 232px; + padding-left: 8px; + white-space: nowrap; + overflow: hidden; + font-weight: 700; + max-width: 125px; +} + +.emoji-picker .diversity-picker { + display: flex; + justify-content: flex-end; + padding: 8px; +} + +.emoji-picker .diversity-picker .diversity-scale { width: 20px; height: 20px; margin-left: 5px; @@ -133,14 +125,125 @@ table.emoji-page td { justify-content: center; cursor: pointer; } -.emoji-modal .tones-button.default { background: #ffcc4d; } -.emoji-modal .tones-button.light { background: #f7dece; } -.emoji-modal .tones-button.medium-light { background: #f3d2a2; } -.emoji-modal .tones-button.medium { background: #d5ab88; } -.emoji-modal .tones-button.medium-dark { background: #af7e57; } -.emoji-modal .tones-button.dark { background: #7c533e; } +.emoji-picker .diversity-picker .diversity-scale.default { background: #ffcc4d; } +.emoji-picker .diversity-picker .diversity-scale.light { background: #f7dece; } +.emoji-picker .diversity-picker .diversity-scale.medium-light { background: #f3d2a2; } +.emoji-picker .diversity-picker .diversity-scale.medium { background: #d5ab88; } +.emoji-picker .diversity-picker .diversity-scale.medium-dark { background: #af7e57; } +.emoji-picker .diversity-picker .diversity-scale.dark { background: #7c533e; } -.emoji-modal .tones-button i.fa { +.emoji-picker .diversity-picker .diversity-scale.selected i { + display: block; +} + +.emoji-picker .diversity-picker i { + display: none; +} + +.emoji-picker .diversity-picker .d-icon { color: #fff; + font-size: 12px; text-shadow: 0.5px 1.5px 0 rgba(0,0,0,0.3); } + +.emoji-picker button.emoji { + background: transparent; + background-position: center; + background-repeat: no-repeat; + border-radius: 0; + background-size: 20px 20px; + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 20px; + margin: 0; + padding: 0; + border: 0; + vertical-align: top; + width: 20px; + outline: none; + padding: 3px; + margin: 2px; +} + +.emoji-picker .section-group button.emoji:hover, .emoji-picker .results button.emoji:hover { + display: inline-block; + vertical-align: top; + border-radius: 50%; + background-color: lighten($tertiary, 40%); +} + +.wmd-emoji-button:before { + content: "\f118"; +} + +.emoji-picker-modal.fadeIn { + z-index: 9999; + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: .8; + background-color: black; +} + +.emoji-picker .filter { + background-color: none; + border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + padding: 5px; + display: flex; + position: relative; + align-items: center; +} + +.emoji-picker .filter .d-icon-search { + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); + font-size: 16px; + margin-left: 5px; + margin-right: 5px; +} + +.emoji-picker .filter input { + height: 24px; + margin: 0; + flex: 1; + border: none; + box-shadow: none; + padding-right: 24px; + outline: none; + color: $primary; + background: $secondary; + + &:focus { + border: none; + box-shadow: none; + } +} + +.emoji-picker .filter input::-ms-clear { + display: none; +} + +.emoji-picker .results { + display: none; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + padding: 4px; + flex: 1; +} + +.emoji-picker .filter .clear-filter { + position: absolute; + right: 5px; + top: 12px; + border: 0; + background: none; + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); + outline: none; + display: none; + + &:hover { + color: $primary; + } +} diff --git a/app/assets/stylesheets/common/base/exception.scss b/app/assets/stylesheets/common/base/exception.scss index c9a7087651..9bdc0df4bb 100644 --- a/app/assets/stylesheets/common/base/exception.scss +++ b/app/assets/stylesheets/common/base/exception.scss @@ -16,7 +16,7 @@ } .desc { margin-top: 16px; - .fa-check-circle { + .d-icon-check-circle { color: $success; } } diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index 5287a777c3..6c55db08f1 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -51,7 +51,7 @@ table.group-logs { width: 100%; th, tr { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } th { @@ -67,7 +67,7 @@ table.group-logs { cursor: pointer; i { - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); } } } @@ -77,7 +77,7 @@ table.group-members { table-layout: fixed; tr { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } th:first-child { @@ -90,7 +90,7 @@ table.group-members { } th { - border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 3px solid $primary-low; text-align: center; padding: 5px 0px 5px 5px; color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); @@ -104,7 +104,7 @@ table.group-members { &:hover { cursor: pointer; - background-color: dark-light-diff($primary, $secondary, 90%, -75%); + background-color: $primary-low; } } @@ -119,7 +119,7 @@ table.group-members { td { text-align: center; - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); padding: 0.8em 0; } } diff --git a/app/assets/stylesheets/common/base/groups.scss b/app/assets/stylesheets/common/base/groups.scss index f40697b01c..6dae404db0 100644 --- a/app/assets/stylesheets/common/base/groups.scss +++ b/app/assets/stylesheets/common/base/groups.scss @@ -8,16 +8,16 @@ width: 100%; th { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; padding: 5px 0px; text-align: left; } tr { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; td { - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); padding: 0.8em 0; } diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 1991799767..2245734e84 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -26,7 +26,7 @@ max-height: 40px; } - .fa-home { + .d-icon-home { font-size: 1.643em; } @@ -69,14 +69,14 @@ &:hover { color: $primary; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; border-top: 1px solid transparent; border-left: 1px solid transparent; border-right: 1px solid transparent; } &:active { color: $primary; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } } .drop-down-visible & { @@ -85,9 +85,9 @@ color: #7b7b7b; background-color: $secondary; cursor: default; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border-right: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; + border-left: 1px solid $primary-low; + border-right: 1px solid $primary-low; .badge-notification { top: -10px; @@ -113,7 +113,8 @@ } } } - [class^="fa fa-"] { + + .d-icon { width: 32px; height: 32px; font-size: 1.714em; @@ -147,7 +148,7 @@ .highlight-strong { - background-color: dark-light-diff($highlight, $secondary, 40%, -45%); + background-color: $highlight-medium; } .search-highlight { diff --git a/app/assets/stylesheets/common/base/history.scss b/app/assets/stylesheets/common/base/history.scss index 23a094624f..0de3abad17 100644 --- a/app/assets/stylesheets/common/base/history.scss +++ b/app/assets/stylesheets/common/base/history.scss @@ -80,7 +80,7 @@ background-color: lighten($highlight, 23%); padding: 3px 5px 5px 5px; } - .fa-ban { + .d-icon-ban { color: #f00; } .hidden-revision-either { diff --git a/app/assets/stylesheets/common/base/lightbox.scss b/app/assets/stylesheets/common/base/lightbox.scss index 61a93372f2..2570034d60 100644 --- a/app/assets/stylesheets/common/base/lightbox.scss +++ b/app/assets/stylesheets/common/base/lightbox.scss @@ -42,7 +42,7 @@ .informations { margin: 6px; padding-right: 20px; - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); font-size: 1em; } diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index deb79628d5..21e80fb44e 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -18,7 +18,7 @@ } .menu-panel { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; box-shadow: 0 2px 2px rgba(0,0,0, .25); background-color: $secondary; z-index: 1100; @@ -60,7 +60,7 @@ padding: 0.25em 0.5em; display: block; &:hover { - background-color: dark-light-diff($highlight, $secondary, 50%, -55%); + background-color: $highlight-medium; } } @@ -191,7 +191,7 @@ } &:hover a:not(.badge-notification) { - background-color: dark-light-diff($highlight, $secondary, 50%, -55%); + background-color: $highlight-medium; } button {margin-left: 5px;} @@ -210,7 +210,7 @@ .fa { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } .icon { color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); } li { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; padding: 0.25em 0.5em; i { float: left; @@ -218,7 +218,7 @@ padding-top: 2px; } span { color: $primary; } - &:hover { background-color: dark-light-diff($highlight, $secondary, 50%, -55%); } + &:hover { background-color: $highlight-medium; } a { padding: 0; } p { margin: 0; @@ -226,7 +226,7 @@ } } .is-warning { - i.fa-envelope-o { + .d-icon-envelope-o { &:before { content: "\f0e0"; } @@ -256,7 +256,7 @@ .notifications .logout { padding: 0.25em; &:hover { - background-color: dark-light-diff($highlight, $secondary, 50%, -55%); + background-color: $highlight-medium; } } @@ -269,7 +269,7 @@ div.menu-links-header { display: table-row; } a:hover { - background-color: dark-light-diff($highlight, $secondary, 50%, -55%); + background-color: $highlight-medium; } a { padding: 0.5em; @@ -301,7 +301,7 @@ div.menu-links-header { a { font-size: 1.1em; } - .fa-user { + .d-icon-user { margin-right: 0.2em; } } diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 8cf03acc16..f41175a732 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -13,11 +13,11 @@ .input-hint-text { margin-left: 0.5em; - color: dark-light-diff($secondary, $primary, 30%, -35%); + color: $secondary-medium; } .modal-header { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } .modal-backdrop { @@ -96,7 +96,7 @@ } .modal-footer { padding: 14px 15px 15px; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; } .modal-footer:before, .modal-footer:after { @@ -118,15 +118,15 @@ .modal { .nav { padding: 10px 30px 10px 15px; - background-color: dark-light-diff($secondary, $primary, 10%, -15%); + background-color: $secondary; li > a { font-size: 1em; } - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } - &.hidden { + &.hidden { display: none; } @@ -235,7 +235,7 @@ font-weight: normal; } &.btn-reply-here { - background: dark-light-diff($primary, $secondary, 90%, -60%); + background: $primary-low; text-shadow: none; color: $primary; } @@ -292,7 +292,7 @@ .permission { margin-left: 20px; } - .fa-times-circle { + .d-icon-times-circle { margin-left: 5px; } li { @@ -356,7 +356,7 @@ font-weight: bold; } &:focus { - outline: 2px solid dark-light-diff($primary, $secondary, 90%, -60%); + outline: 2px solid $primary-low; } } .incoming-email-tabs { @@ -367,7 +367,7 @@ textarea, .incoming-email-html-part { height: 95%; border: none; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; padding-top: 10px; } textarea { diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 5b718047c7..86e505e622 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -89,7 +89,7 @@ a.loading-onebox { } aside.onebox { - border: 5px solid dark-light-diff($primary, $secondary, 90%, -85%); + border: 5px solid $primary-low; padding: 12px 25px 12px 12px; font-size: 1em; diff --git a/app/assets/stylesheets/common/base/rtl.scss b/app/assets/stylesheets/common/base/rtl.scss index 045df633a6..71163ef835 100644 --- a/app/assets/stylesheets/common/base/rtl.scss +++ b/app/assets/stylesheets/common/base/rtl.scss @@ -7,7 +7,7 @@ right: 0 !important; } -// This is used to flip the .fa-caret-right icon +// This is used to flip the .d-icon-caret-right @mixin rotate( $degrees ) { -webkit-transform: rotate(#{$degrees}deg); -moz-transform: rotate(#{$degrees}deg); @@ -21,7 +21,7 @@ } // Get the right caret to point left -.rtl .fa-caret-right { +.rtl .d-icon-caret-right { @include rotate(180); } diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index 7fa038db9c..eeb82c5d04 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -108,7 +108,7 @@ } .search-advanced-options { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border: 1px solid $primary-low; padding: 10px; .control-group.pull-left { @@ -147,7 +147,7 @@ } margin: 10px 0 15px; max-width: 780px; - border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-bottom: 3px solid $primary-low; width: 100%; .sort-by { .desc { diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index f691175120..be2a28cc3f 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -171,6 +171,10 @@ header .discourse-tag {color: $tag-color } .tag-chooser { width: 100%; margin: 5px 0; + ul.select2-choices { + max-height: 40px; + overflow-y: auto; + } } .title-wrapper .tag-chooser { @@ -203,7 +207,7 @@ header .discourse-tag {color: $tag-color } } .autocomplete { - .fa-tag { + .d-icon-tag { color: dark-light-choose($primary, scale-color($primary, $lightness: 70%)); padding-right: 5px; } diff --git a/app/assets/stylesheets/common/base/topic-admin-menu.scss b/app/assets/stylesheets/common/base/topic-admin-menu.scss index 2c20a0fa5d..7816086dc9 100644 --- a/app/assets/stylesheets/common/base/topic-admin-menu.scss +++ b/app/assets/stylesheets/common/base/topic-admin-menu.scss @@ -12,7 +12,7 @@ background-color: $secondary; width: 205px; padding: 10px; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; z-index: 999; ul { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 216a691520..3f34809d20 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -1,6 +1,6 @@ .placeholder-avatar { display: inline-block; - background-color: dark-light-diff($primary, $secondary, 90%, -75%); + background-color: $primary-low; width: 45px; height: 45px; border-radius: 50%; @@ -8,7 +8,7 @@ .placeholder-text { display: inline-block; - background-color: dark-light-diff($primary, $secondary, 90%, -75%); + background-color: $primary-low; width: 100%; height: 1.5em; margin-bottom: 0.6em; @@ -92,7 +92,7 @@ aside.quote { } .cooked .highlight { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; padding: 2px; margin: -2px; } @@ -137,7 +137,7 @@ aside.quote { .quote-button { display: none; position: absolute; - background-color: dark-light-diff($primary, $secondary, 50%, -50%); + background-color: blend-primary-secondary(50%); color: dark-light-choose($secondary, $primary); padding: 10px; z-index: 401; @@ -149,7 +149,7 @@ aside.quote { } &:hover { - background-color: dark-light-diff($primary, $secondary, 40%, -40%); + background-color: $primary-medium; cursor: pointer; } } @@ -242,7 +242,7 @@ aside.quote { .post-info { &.via-email, &.whisper { margin-right: 5px; - i.fa { + .d-icon { font-size: 1em; } } @@ -273,7 +273,7 @@ pre { kbd { background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; border-radius: 3px; box-shadow: 0 1px 0 rgba(0,0,0, .8); color: $primary; @@ -395,15 +395,15 @@ blockquote > *:last-child { .topic-body { .cooked { font-style: italic; - color: dark-light-diff($primary, $secondary, 55%, -40%); + color: $primary-medium; } } } a.mention, a.mention-group { padding: 2px 4px; - color: dark-light-diff($primary, $secondary, 30%, -20%); - background: dark-light-diff($primary, $secondary, 95%, -60%); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); + background: $primary-low; border-radius: 8px; font-weight: bold; font-size: 0.93em; diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index bce213d819..5a6b735fc9 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -91,7 +91,7 @@ .has-pending-posts { padding: 0.5em; - background-color: dark-light-diff($highlight, $secondary, 50%, -70%); + background-color: $highlight-medium; a[href] { float: right; } @@ -143,7 +143,7 @@ .post-links { margin-top: 1em; padding-top: 1em; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-top: 1px solid $primary-low; } .expand-links { @@ -185,7 +185,7 @@ &:hover { color: $tertiary; i { - background: dark-light-diff($tertiary, $secondary, 85%, -65%); + background: $tertiary-low; } } } diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 7c25c2652a..7e98f3c2c0 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -2,7 +2,7 @@ .user-badge { padding: 3px 8px; color: $primary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border: 1px solid $primary-low; line-height: 19px; display: inline-block; background-color: $secondary; @@ -126,7 +126,7 @@ .badge-card { position: relative; display: inline-block; - background-color: dark-light-diff($primary, $secondary, 95%, -65%); + background-color: $primary-low; margin-right: 5px; margin-bottom: 10px; box-shadow: 1px 1px 3px rgba(0.0, 0.0, 0.0, 0.2); @@ -142,7 +142,7 @@ right: 5px; top: 5px; font-weight: bold; - color: dark-light-diff($primary, $secondary, 50%, -15%); + color: $primary-medium; font-size: 1.2em; } @@ -156,7 +156,7 @@ display: flex; align-items: center; justify-content: center; - background-color: dark-light-diff($primary, $secondary, 92%, -60%); + background-color: $primary-low; font-size: 3em; img { diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index a36fb62df4..70926d68a3 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -21,7 +21,7 @@ } .user-main { - i.fa-heart { + .d-icon-heart { color: $love !important; } .nav-pills { @@ -177,10 +177,10 @@ .user-nav { margin: 5px 0px; padding-top: 10px; - .fa { + .d-icon { margin-right: 5px; } - .fa.fa-comment { + .d-icon-comment { margin-right: 2px; } } @@ -227,7 +227,7 @@ width: 100%; height: 100%; display: block; - color: black; + color: $primary; } } } @@ -242,7 +242,7 @@ } .label { - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); } } @@ -254,7 +254,7 @@ } li { - border-left: dark-light-diff($primary, $secondary, 90%, -65%) solid 2px; + border-left: $primary-low solid 2px; padding: 5px 8px; margin: 10px 0; } @@ -294,7 +294,7 @@ and (max-width : 600px) { } .user-preferences .tags .select2-container-multi { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; width: 540px; border-radius: 0; .select2-choices { diff --git a/app/assets/stylesheets/common/components/badges.scss b/app/assets/stylesheets/common/components/badges.scss index 8273d97c8b..c26997e4d9 100644 --- a/app/assets/stylesheets/common/components/badges.scss +++ b/app/assets/stylesheets/common/components/badges.scss @@ -28,7 +28,7 @@ display: inline-flex; span.badge-category { - color: $primary !important; + color: $primary; padding: 3px; vertical-align: text-top; margin-top: -2px; //vertical alignment fix @@ -37,7 +37,7 @@ text-overflow: ellipsis; .extra-info-wrapper & { - color: $header-primary !important; + color: $header-primary; } } @@ -59,7 +59,7 @@ line-height: 15px; span.badge-category { - color: $primary !important; + color: $primary; display: inline-block; overflow: hidden; text-overflow: ellipsis; @@ -69,7 +69,7 @@ } .extra-info-wrapper & { - color: $header-primary !important; + color: $header-primary; } } @@ -149,7 +149,7 @@ .list-controls { .category-breadcrumb { - a.badge-category { + a.badge-category, .dropdown-header { display: inline-block; padding: 5px 8px; line-height: 20px; @@ -158,24 +158,25 @@ padding: 5px; width: 13px; - .fa-caret-right { + .d-icon-caret-right { margin-left: 2px; } } } - li.bar>.badge-category:not(.home):first-child { + li.bar>.dropdown-header:not(.home):first-child { border-left: 5px solid; + font-size: 1em; } - li.bar>.badge-category { - background: dark-light-diff($primary, $secondary, 90%, -65%) !important; - color: $primary !important; + li.bar>.dropdown-header { + background: $primary-low; + color: $primary; } - li.bullet>.badge-category { - background: dark-light-diff($primary, $secondary, 90%, -65%) !important; - color: $primary !important; + li.bullet>.dropdown-header { + background: $primary-low; + color: $primary; .badge-category-bg { width: 10px; @@ -189,7 +190,7 @@ } .category-dropdown-menu { - .badge-category { + .dropdown-header { &.home { margin-left: 4px; padding-left: 0; @@ -279,9 +280,9 @@ &.clicks { font-weight: normal; - background-color: dark-light-diff($primary, $secondary, 88%, -60%); + background-color: $primary-low; top: -1px; - color: dark-light-diff($primary, $secondary, 40%, -20%); + color: $primary-medium; position: relative; margin-left: 2px; border: none; @@ -308,8 +309,8 @@ padding: 4px 5px 2px 5px; color: $primary; text-shadow: 0 1px 0 rgba($primary, 0.1); - background-color: dark-light-diff($primary, $secondary, 90%, -65%); - border-color: dark-light-diff($primary, $secondary, 90%, -65%); + background-color: $primary-low; + border-color: $primary-low; font-size: 0.857em; box-shadow: inset 0 1px 0 rgba(0,0,0, 0.22); } diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index a1a3f4bc6d..f73c5e3ea0 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -46,25 +46,25 @@ border: none; color: $primary; font-weight: normal; - background: dark-light-diff($primary, $secondary, 90%, -65%); + background: $primary-low; &[href] { color: $primary; } &:hover { - background: dark-light-diff($primary, $secondary, 65%, -75%); + background: $primary-medium; color: $secondary; } &[disabled], &.disabled { - background: dark-light-diff($primary, $secondary, 90%, -60%); + background: $primary-low; &:hover { color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } cursor: not-allowed; } - i.fa { + .d-icon { opacity: 0.7; } - &.btn-primary i.fa { + &.btn-primary .d-icon { opacity: 1; } } @@ -189,3 +189,12 @@ font-size: 1.143em; line-height: 20px; } + +.btn-flat { + background: transparent; + border: 0; + outline: 0; + .d-icon { + opacity: 0.7; + } +} diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 59a34aad62..6beae3c591 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -32,7 +32,7 @@ transition: background .15s; &:hover { color: $quaternary; - background-color: dark-light-diff($quaternary, $secondary, 70%, -70%); + background-color: $quaternary-low; } } &.active > a, > a.active { @@ -49,9 +49,9 @@ @extend %nav; padding: 0; overflow: hidden; - background: dark-light-diff($primary, $secondary, 90%, -75%); + background: $primary-low; li { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 80%, -60%); + border-bottom: 1px solid $primary-low; position: relative; &:last-of-type { border-bottom: 0; diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 8d3d618390..b1a97021b0 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -1,5 +1,5 @@ .d-editor { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; } .d-editor-container { @@ -22,7 +22,7 @@ min-width: 400px; position: absolute; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 1em; top: 25px; @@ -71,7 +71,7 @@ height: 20px; margin-right: 8px; margin-left: 5px; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; display: inline-block; float: left; } @@ -82,13 +82,13 @@ height: 200px; &:disabled { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } } .d-editor-preview { color: $primary; - border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px dashed $primary-low; overflow: auto; cursor: default; margin-top: 8px; @@ -110,7 +110,7 @@ .composing-whisper { .d-editor-preview { font-style: italic; - color: dark-light-diff($primary, $secondary, 55%, -40%) !important; + color: $primary-medium !important; } } diff --git a/app/assets/stylesheets/common/foundation/base.scss b/app/assets/stylesheets/common/foundation/base.scss index 35b0418187..bf08280bb1 100644 --- a/app/assets/stylesheets/common/foundation/base.scss +++ b/app/assets/stylesheets/common/foundation/base.scss @@ -41,7 +41,7 @@ hr { height: 1px; margin: 1em 0; border: 0; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; padding: 0; } @@ -49,7 +49,6 @@ hr { // -------------------------------------------------- ul, -ol, dd { margin: 0 0 9px 25px; padding: 0; @@ -60,6 +59,11 @@ dd { clear: both; } +.cooked ul, .d-editor-preview ul { + margin: 0; + padding-left: 40px; +} + li { > ul, > ol { diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index f9c3ca2a5a..ed55c8f522 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -97,7 +97,7 @@ // Stuff we repeat @mixin post-aside { - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -85%); + border-left: 5px solid $primary-low; background-color: blend-primary-secondary(5%); } diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 43eb44cdb2..2d7c6186bb 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -78,3 +78,33 @@ $base-font-family: Helvetica, Arial, sans-serif !default; @return $dark-theme-result; } } + +// standard color transformations, use these if possible, and add any new dark-light-diffs here + +//primary +$primary-low: dark-light-diff($primary, $secondary, 90%, -65%); +$primary-medium: dark-light-diff($primary, $secondary, 50%, -20%); + +//secondary +$secondary-low: dark-light-diff($secondary, $primary, 50%, -50%); +$secondary-medium: dark-light-diff($secondary, $primary, 30%, -35%); + +//tertiary +$tertiary-low: dark-light-diff($tertiary, $secondary, 85%, -65%); + +//quaternary +$quaternary-low: dark-light-diff($quaternary, $secondary, 70%, -70%); + +//highlight +$highlight-low: dark-light-diff($highlight, $secondary, 70%, -80%); +$highlight-medium: dark-light-diff($highlight, $secondary, 50%, -55%); + +//danger +$danger-low: dark-light-diff($danger, $secondary, 50%, -40%); +$danger-medium: dark-light-diff($danger, $secondary, 30%, -60%); + +//success +$success-low: dark-light-diff($success, $secondary, 50%, -60%); + +//love +$love-low: dark-light-diff($love, $secondary, 85%, -60%); diff --git a/app/assets/stylesheets/common/input_tip.scss b/app/assets/stylesheets/common/input_tip.scss index fae0176e2a..5f02aeb111 100644 --- a/app/assets/stylesheets/common/input_tip.scss +++ b/app/assets/stylesheets/common/input_tip.scss @@ -7,7 +7,7 @@ padding: 5px 10px; z-index: 101; &.bad { - background: dark-light-diff($danger, $secondary, 20%, -40%); + background: $danger-medium; color: white; box-shadow: 1px 1px 5px rgba(0,0,0, .7); } diff --git a/app/assets/stylesheets/common/topic-entrance.scss b/app/assets/stylesheets/common/topic-entrance.scss index 847edc46a8..6e9e1aa209 100644 --- a/app/assets/stylesheets/common/topic-entrance.scss +++ b/app/assets/stylesheets/common/topic-entrance.scss @@ -1,6 +1,6 @@ #topic-entrance { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 5px; background: $secondary; box-shadow: 0 0px 2px rgba(0,0,0, .2); diff --git a/app/assets/stylesheets/desktop/category-list.scss b/app/assets/stylesheets/desktop/category-list.scss index 4b40fc3483..e6926808ed 100644 --- a/app/assets/stylesheets/desktop/category-list.scss +++ b/app/assets/stylesheets/desktop/category-list.scss @@ -90,9 +90,9 @@ tbody { tr { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-bottom: 1px solid $primary-low; &:first-of-type { - border-top: 3px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 3px solid $primary-low; } } diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index c8aa5306ea..197169c728 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -36,14 +36,14 @@ padding: 10px; box-shadow: 3px 3px 3px rgba(0,0,0, 0.34); - background: dark-light-diff($highlight, $secondary, 50%, -80%); + background: $highlight-medium; &.urgent { - background: dark-light-diff($danger, $secondary, 50%, -40%); + background: $danger-low; } &.education-message { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; } h3 { @@ -79,17 +79,17 @@ } .custom-body { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; p { max-width: 98%; } } .similar-topics { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; a[href] { - color: dark-light-diff($primary, $secondary, -10%, 10%); + color: $primary-medium; } .posts-count { @@ -140,21 +140,16 @@ width: 100%; z-index: 999; height: 0; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; bottom: 0; font-size: 1em; position: fixed; .toggler { - width: 15px; right: 1px; position: absolute; - font-size: 1.071em; + i { font-size: 1.1em; } color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); padding: 0 10px 5px 10px; - &:before { - font-family: "FontAwesome"; - content: "\f078"; - } } a.cancel { padding-left: 7px; @@ -184,7 +179,7 @@ &.draft { height: 40px !important; cursor: pointer; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; .draft-text { display: block; @@ -192,25 +187,13 @@ color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } - .toggler { - &:before { - font-family: "FontAwesome"; - content: "\f00d"; - } - } } &.saving { height: 40px !important; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; .saving-text { display: block; } - .toggler { - &:before { - font-family: "FontAwesome"; - content: "\f00d"; - } - } } .reply-area { max-width: 1500px; @@ -299,7 +282,7 @@ margin-right: 10px; float: left; &:disabled { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } } #topic-featured-link { @@ -308,14 +291,14 @@ width: 400px; } .d-editor-input:disabled { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } .d-editor-input, .d-editor-preview { color: $primary; } .d-editor-preview { - border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px dashed $primary-low; overflow: auto; visibility: visible; cursor: default; @@ -353,7 +336,7 @@ .show-admin-options { vertical-align: top; margin-top: 8px; - background: dark-light-diff($primary, $secondary, 90%, -60%); + background: $primary-low; &:hover { color: $secondary; background: $primary; diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 20e1ebc1ee..aa120ec8a9 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -31,7 +31,7 @@ body { } #main { a.star { - color: dark-light-diff($secondary, $primary, 80%, -20%); + color: $secondary-medium; &:before { font-family: "FontAwesome"; content: "\f005"; @@ -96,7 +96,7 @@ body { } } - i.fa-envelope { + .d-icon-envelope { color: $danger; } } @@ -213,14 +213,14 @@ body { width: 210px; height: auto; background-color:$secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); } input { &[type="text"], &[type="password"], &[type="datetime"], &[type="datetime-local"], &[type="date"], &[type="month"], &[type="time"], &[type="week"], &[type="number"], &[type="email"], &[type="url"], &[type="search"], &[type="tel"], &[type="color"] { background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); } @@ -244,7 +244,7 @@ body { select { width: 220px; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; &[multiple], &[size] { height: auto; } @@ -272,8 +272,8 @@ body { } input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { cursor: not-allowed; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); - border-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; + border-color: $primary-low; } input { &[type="radio"][disabled], &[type="checkbox"][disabled], &[type="radio"][readonly], &[type="checkbox"][readonly] { @@ -372,7 +372,7 @@ body { text-align: center; vertical-align: middle; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; } .input-prepend .add-on, .input-append .add-on, .input-prepend .btn, .input-append .btn { margin-left: -1px; diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss index 5c913eef7f..5bd0b3b9e6 100644 --- a/app/assets/stylesheets/desktop/group.scss +++ b/app/assets/stylesheets/desktop/group.scss @@ -48,7 +48,7 @@ } .group-edit { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 10px; .form-horizontal { diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index f4ce15a022..4c4d3da302 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -7,7 +7,7 @@ z-index: 1000; padding-top: 3px; height: 60px; - .fa-home { + .d-icon-home { padding:8px; font-size: 2.1em; } diff --git a/app/assets/stylesheets/desktop/history.scss b/app/assets/stylesheets/desktop/history.scss index 858796f21b..4519450e1a 100644 --- a/app/assets/stylesheets/desktop/history.scss +++ b/app/assets/stylesheets/desktop/history.scss @@ -12,10 +12,10 @@ .btn[disabled] { cursor: not-allowed; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; } .btn-danger[disabled] { - background-color: dark-light-diff($danger, $secondary, 30%, -60%); + background-color: $danger-medium; } } #display-modes { @@ -25,7 +25,7 @@ .btn { background-color:inherit; - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); } .btn-primary { color: $primary; @@ -35,7 +35,7 @@ #revision-details { padding: 5px; margin-top: 10px; - border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 3px solid $primary-low; } #revisions { word-wrap: break-word; diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index c345acd0cd..35b4f4d6f1 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -16,7 +16,7 @@ height: auto; margin: -250px 0 0 -305px; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; box-shadow: 0 3px 7px rgba(0,0,0, .8); background-clip: padding-box; diff --git a/app/assets/stylesheets/desktop/queued-posts.scss b/app/assets/stylesheets/desktop/queued-posts.scss index 6d306bae6e..363eca2dc8 100644 --- a/app/assets/stylesheets/desktop/queued-posts.scss +++ b/app/assets/stylesheets/desktop/queued-posts.scss @@ -31,7 +31,7 @@ } } .post-title { - color: dark-light-diff($primary, $secondary, 20%, -60%); + color: $primary-medium; font-weight: bold; .badge-wrapper { @@ -41,4 +41,3 @@ } } - diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 58932112a1..f877f044d6 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -23,8 +23,7 @@ float: none; } - - a.badge-category { + a.badge-category, .dropdown-header { padding: 3px 12px; font-size: 1.143em; } @@ -36,10 +35,10 @@ .topic-list { margin: 0 0 10px; - .fa-thumb-tack { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } - .fa-thumb-tack.unpinned { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + .d-icon-thumb-tack { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + .d-icon-thumb-tack.unpinned { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } a.title {color: $primary;} - .fa-bookmark { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + .d-icon-bookmark { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } th, td { padding: 12px 5px; @@ -51,7 +50,7 @@ } } th { - button i.fa {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + button .d-icon {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } > tbody > tr { @@ -61,16 +60,14 @@ } button.bulk-select { padding: 0; - background: transparent; - &:hover { - color: dark-light-diff($primary, $secondary, 90%, -60%); - } + font-size: 1.05em; + margin-right: 0.3em; } .star { width: 20px; padding-right: 0; - .fa-star { + .d-icon-star { position: relative; } + .main-link { @@ -115,7 +112,7 @@ .sortable { cursor: pointer; &:hover { - background-color: dark-light-diff($primary, $secondary, 90%, -75%); + background-color: $primary-low; } @include unselectable; } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 33a9bf1c5d..5229331141 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -53,17 +53,23 @@ section.post-menu-area { nav.post-controls { - .like-count { - font-size: inherit; - margin-right: -5px; - } + padding: 0; - padding: 0; - .highlight-action { - color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); - } - a, button { + .like-count { + font-size: inherit; + margin-right: -5px; + } + + .highlight-action { + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); + } + + a, button { color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); + + .d-icon { + opacity: 1.0; + } margin-right: 2px; display: inline-block; } @@ -94,7 +100,7 @@ nav.post-controls { font-size: inherit; span.badge-posts {color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } &:hover { - background: dark-light-diff($primary, $secondary, 90%, -65%); + background: $primary-low; span.badge-posts {color: $primary;} } i { @@ -122,7 +128,7 @@ nav.post-controls { margin-left: 3px; &.d-hover { - background: dark-light-diff($primary, $secondary, 90%, -60%); + background: $primary-low; color: $primary; } @@ -144,7 +150,7 @@ nav.post-controls { &.like.d-hover { color: $love; - background: dark-light-diff($love, $secondary, 85%, -60%) + background: $love-low; } &.has-like {color: $love;} @@ -179,7 +185,7 @@ nav.post-controls { background-color: $secondary; width: 205px; padding: 10px; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; position: absolute; text-align: left; bottom: -2px; @@ -201,7 +207,7 @@ nav.post-controls { width: 176px; margin-bottom: 5px; - i.fa { + .d-icon { width: 14px; margin-right: 14px; } @@ -255,7 +261,7 @@ nav.post-controls { } .post-date { color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } - .fa-arrow-up, .fa-arrow-down { margin-left: 5px; } + .d-icon-arrow-up, .d-icon-arrow-down { margin-left: 5px; } .reply:first-of-type .row { border-top: none; } .topic-meta-data { @@ -288,11 +294,11 @@ a.star { .topic-map { margin: 20px 0 0 0; background: blend-primary-secondary(5%); - border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border: 1px solid $primary-low; border-top: none; // would cause double top border section { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-top: 1px solid $primary-low; } h3 { @@ -379,7 +385,7 @@ a.star { font-weight: bold; font-size: 0.929em; } - i.fa-times { + .d-icon-times { color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } } @@ -402,11 +408,11 @@ a.star { padding: 0 23px; color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); background: blend-primary-secondary(5%); - border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-left: 1px solid $primary-low; + border-top: 1px solid $primary-low; &:hover { color: $primary; - background: dark-light-diff($primary, $secondary, 90%, -80%); + background: $primary-low; } &.collapsed { @@ -419,6 +425,16 @@ a.star { } } } + + .link-summary .btn { + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); + background: blend-primary-secondary(5%); + width: 100%; + &:hover { + color: $primary; + background: dark-light-diff($primary, $secondary, 90%, -80%); + } + } } #topic-footer-buttons { @@ -430,10 +446,10 @@ a.star { .btn { margin-bottom: 5px; margin-right: 10px; - .fa-bookmark.bookmarked { color: $tertiary; } + .d-icon-bookmark.bookmarked { color: $tertiary; } } - .bookmark.bookmarked .fa-bookmark { + .bookmark.bookmarked .d-icon-bookmark { color: $tertiary; } @@ -510,7 +526,7 @@ video { .topic-statuses { i { color: $header_primary; } - i.fa-envelope { color: $danger; } + .d-icon-envelope { color: $danger; } .unpinned { color: $header_primary; } } @@ -565,7 +581,7 @@ video { .moderator { .topic-body { - background-color: dark-light-diff($highlight, $secondary, 70%, -80%); + background-color: $highlight-low; } } @@ -615,7 +631,7 @@ blockquote { aside { .quote, .title, blockquote, .onebox, .onebox-result { background: blend-primary-secondary(5%); - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-left: 5px solid $primary-low; } aside.quote>blockquote, aside.quote>.title { @@ -635,12 +651,12 @@ $topic-avatar-width: 45px; float: left; position: relative; z-index: 2; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 1px solid $primary-low; padding: 12px $topic-body-width-padding 15px $topic-body-width-padding; } .topic-avatar { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 1px solid $primary-low; padding-top: 15px; width: $topic-avatar-width; float: left; @@ -653,7 +669,7 @@ $topic-avatar-width: 45px; .small-action { max-width: 755px; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 1px solid $primary-low; } .small-action.deleted { @@ -696,7 +712,7 @@ $topic-avatar-width: 45px; margin: 1px 0 0; list-style: none; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; box-shadow: 0 1px 5px rgba(0,0,0, .4); background-clip: padding-box; span { @@ -728,7 +744,7 @@ $topic-avatar-width: 45px; .dropdown-menu .active > a:hover { color: $primary; text-decoration: none; - background-color: dark-light-diff($highlight, $secondary, 50%, -70%); + background-color: $highlight-medium; } @@ -736,7 +752,7 @@ $topic-avatar-width: 45px; .dropdown-menu .disabled > a:hover { text-decoration: none; color: $primary; - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; cursor: default; } @@ -755,7 +771,7 @@ $topic-avatar-width: 45px; width: 200px; position: fixed; z-index: 1000; - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; border: 1px solid $tertiary; padding: 5px; margin-bottom: 5px; @@ -874,7 +890,7 @@ a.attachment:before { display: block; position: absolute; left: 767px; - color: rgba(dark-light-diff($primary, $secondary, 90%, -65%) , .8); + color: rgba($primary-low, .8); font: 6.429em/1 FontAwesome; content: "\f014"; z-index: -5; @@ -909,7 +925,7 @@ a.attachment:before { } span.highlighted { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; } .username.new-user a { diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index c5f88134b6..4adbbcd4d0 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -80,7 +80,7 @@ } .topic-status-info { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 1px solid $primary-low; padding-top: 10px; height: 20px; max-width: 757px; @@ -99,7 +99,7 @@ } #topic-progress-expanded { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 5px; background: $secondary; @@ -149,7 +149,7 @@ } background-color: $secondary; color: $tertiary; - border: 1px solid dark-light-diff($tertiary, $secondary, 85%, -65%); + border: 1px solid $tertiary-low; border-bottom: none; width: 145px; height: 34px; @@ -167,7 +167,7 @@ text-align: center; z-index: 1; } - i.fa { + .d-icon { position: absolute; right: 8px; bottom: 9px; @@ -183,8 +183,8 @@ top: 0; bottom: 0; width: 0; - border-right: 1px solid dark-light-diff($tertiary, $secondary, 85%, -65%); - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + border-right: 1px solid $tertiary-low; + background-color: $tertiary-low; transition: width .75s; } } diff --git a/app/assets/stylesheets/desktop/user-card.scss b/app/assets/stylesheets/desktop/user-card.scss index 2d48ec8e00..0920d97213 100644 --- a/app/assets/stylesheets/desktop/user-card.scss +++ b/app/assets/stylesheets/desktop/user-card.scss @@ -205,17 +205,12 @@ $user_card_background: $secondary; .names { float: left; - height: 60px; - position: relative; width: 45%; span { - position: absolute; - bottom: 0; display: block; width: 250px; } - } .badge-section { @@ -226,7 +221,7 @@ $user_card_background: $secondary; .user-badge { background: scale-color($user_card_background, $lightness: -5%); - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; color: $user_card_primary; } diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index db7302369e..07d645809f 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -48,6 +48,9 @@ font-size: 80%; line-height: 1.4em; } + .form-horizontal .instructions { + margin-left: 160px; + } .avatar { margin-left: 3px; } @@ -159,7 +162,7 @@ th { text-align: left; - border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 3px solid $primary-low; padding: 0 0 10px 0; color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; @@ -167,7 +170,7 @@ td { padding: 10px 0 10px 0; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } } @@ -179,7 +182,7 @@ } .user-invite-controls { - background-color: dark-light-diff($primary, $secondary, 90%, -75%); + background-color: $primary-low; padding: 5px 10px 0px 0; height: 35px; } @@ -211,8 +214,8 @@ .secondary { background: scale-color($secondary, $lightness: -5%); - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; + border-bottom: 1px solid $primary-low; font-size: 0.929em; .btn { padding: 3px 12px; } @@ -249,7 +252,7 @@ } dt { - color: dark-light-diff($secondary, $primary, 50%, -40%); + color: $secondary-medium; margin: 0; } } @@ -261,8 +264,8 @@ transition: margin .15s linear; blockquote { - background-color: dark-light-diff($secondary, $primary, 30%, -70%); - border-left-color: dark-light-diff($secondary, $primary, 50%, -50%); + background-color: $secondary-low; + border-left-color: $secondary-low; } h1 { @@ -389,7 +392,7 @@ padding: 0 0 2px 0; margin-top: 0; background: rgba($secondary, .85); - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; .bio { display: none; } .primary { @@ -433,7 +436,7 @@ color: $primary; } .item.moderator-action { - background-color: dark-light-diff($highlight, $secondary, 50%, -10%); + background-color: $highlight-medium; } .item.deleted { opacity: 0.8; @@ -446,7 +449,7 @@ .item { padding: 20px 8px 15px 8px; background-color: $secondary; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } .type { color: $primary; @@ -490,7 +493,7 @@ } .notification { &.unread { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: $tertiary-low; } li { display: inline-block; } diff --git a/app/assets/stylesheets/embed.scss b/app/assets/stylesheets/embed.scss index 66af74d3d0..65a25aa8f1 100644 --- a/app/assets/stylesheets/embed.scss +++ b/app/assets/stylesheets/embed.scss @@ -16,8 +16,8 @@ article.post { } .quote .title { - border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%); - background-color: dark-light-diff($primary, $secondary, 97%, -45%); + border-left: 5px solid darken($primary-low, 10%); + background-color: $primary-low; padding: 10px 10px 0 12px; .avatar { margin-right: 7px; } } @@ -29,8 +29,8 @@ article.post { blockquote { padding: 10px 8px 10px 13px; margin: 0 0 10px 0; - background-color: dark-light-diff($primary, $secondary, 97%, -45%); - border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%); + background-color: $primary-low; + border-left: 5px solid darken($primary-low, 10%); p { margin: 0 0 10px 0; } diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index 405349f46d..1256f823e2 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -19,7 +19,7 @@ input { padding: 4px; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; } input[type=radio], input[type=checkbox] { box-shadow: none; @@ -38,28 +38,19 @@ input[type=radio], input[type=checkbox] { width: 100%; z-index: 1039; height: 0; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; bottom: 0; font-size: 1em; position: fixed; .toggle-toolbar, .toggler { - width: 15px; right: 1px; position: absolute; font-size: 1.071em; color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - padding: 0 10px 5px 10px; - &:before { - font-family: "FontAwesome"; - content: "\f078"; - } } .toggle-toolbar { right: 30px; - &:before { - content: "\f0c9"; - } } a.cancel { @@ -97,7 +88,7 @@ input[type=radio], input[type=checkbox] { &.draft { height: 35px !important; cursor: pointer; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; .draft-text { display: block; position: absolute; @@ -111,25 +102,13 @@ input[type=radio], input[type=checkbox] { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } - .toggler { - &:before { - font-family: "FontAwesome"; - content: "\f00d"; - } - } } &.saving { height: 40px !important; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; .saving-text { display: block; } - .toggler { - &:before { - font-family: "FontAwesome"; - content: "\f00d"; - } - } } // if this is a new topic, make room for the category field in the editor on diff --git a/app/assets/stylesheets/mobile/directory.scss b/app/assets/stylesheets/mobile/directory.scss index bbdbf8cf02..4cd15c8900 100644 --- a/app/assets/stylesheets/mobile/directory.scss +++ b/app/assets/stylesheets/mobile/directory.scss @@ -18,12 +18,12 @@ } .directory .user { - border-top: 1px solid dark-light-diff($primary, $secondary, 80%, -20%); + border-top: 1px solid $primary-low; padding: 1em; &.me { - background-color: dark-light-diff($highlight, $secondary, 70%, -80%); + background-color: $highlight-low; .username a, .name, .title, .number, .time-read, .user-stat .label { color: scale-color($highlight, $lightness: -50%); @@ -38,9 +38,9 @@ } .label { margin-left: 0.2em; - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: blend-primary-secondary(50%); } - i.fa-heart { + .d-icon-heart { color: $love; } } diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index 97c23d2427..18726ee67e 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -73,7 +73,7 @@ blockquote { display: inline-block; .topic-status { i { - color: dark-light-diff($secondary, $primary, 40%, -20%); + color: $secondary-medium; } } } @@ -123,7 +123,7 @@ h2#site-text-logo } margin: 0; padding: 0; - background: dark-light-diff($primary, $secondary, 90%, -65%); + background: $primary-low; list-style: none; overflow: visible; position: relative; @@ -136,7 +136,7 @@ h2#site-text-logo box-sizing: border-box; display: block; } - .fa-caret-down { + .d-icon-caret-down { position: absolute; right: 0px; } diff --git a/app/assets/stylesheets/mobile/emoji.scss b/app/assets/stylesheets/mobile/emoji.scss index 6d94493756..e69de29bb2 100644 --- a/app/assets/stylesheets/mobile/emoji.scss +++ b/app/assets/stylesheets/mobile/emoji.scss @@ -1,10 +0,0 @@ -.emoji-table-wrapper { - min-width: 320px; -} - -.emoji-modal { - width: 340px; - margin-top: -132px; - margin-left: -170px; - background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); -} diff --git a/app/assets/stylesheets/mobile/history.scss b/app/assets/stylesheets/mobile/history.scss index 0f52dbb84a..395f8ca956 100644 --- a/app/assets/stylesheets/mobile/history.scss +++ b/app/assets/stylesheets/mobile/history.scss @@ -9,7 +9,7 @@ line-height: 2em; } #revision-details { - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + background-color: $primary-low; padding: 5px; margin-top: 10px; } diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index d66aa8115a..7e8c56da2d 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -26,7 +26,7 @@ position: relative; } .nav-pills .drop { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border: 1px solid $primary-low; position: absolute; z-index: 1000; background-color: $secondary; @@ -47,8 +47,8 @@ } .nav-pills > li { - background: dark-light-diff($primary, $secondary, 90%, -65%); - i.fa-caret-down { + background: $primary-low; + .d-icon-caret-down { margin-left: 8px; } } @@ -146,9 +146,9 @@ tbody { tr { - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-bottom: 1px solid $primary-low; &:first-of-type { - border-top: 3px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 3px solid $primary-low; } } .category { diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 453f863efd..4068eb00ae 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -3,7 +3,7 @@ } .small-action { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; color: lighten($primary, 50%); padding-bottom: 3px; text-transform: uppercase; @@ -28,7 +28,7 @@ } .topic-post article { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-top: 1px solid $primary-low; padding: 8px 0; } @@ -47,6 +47,9 @@ span.badge-posts { nav.post-controls { clear: both; + .d-icon { + opacity: 1.0; + } } .who-liked { @@ -109,7 +112,7 @@ nav.post-controls { background-color: $secondary; width: 205px; padding: 10px; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; position: absolute; text-align: left; bottom: 0; @@ -131,7 +134,7 @@ nav.post-controls { width: 176px; margin-bottom: 5px; - i.fa { + .d-icon { width: 14px; margin-right: 14px; } @@ -181,11 +184,11 @@ a.star { margin: 10px 0; background: blend-primary-secondary(5%); - border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border: 1px solid $primary-low; border-top: none; // would cause double top border section { - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-top: 1px solid $primary-low; } h3 { @@ -300,8 +303,8 @@ a.star { padding: 0 15px; color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); background: blend-primary-secondary(5%); - border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-left: 1px solid $primary-low; + border-top: 1px solid $primary-low; .fa { margin: 0; font-size: 1.286em; @@ -309,12 +312,18 @@ a.star { } } } + + .link-summary .btn { + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); + background: blend-primary-secondary(5%); + width: 100%; + } } #topic-footer-buttons { @include clearfix; padding: 20px 0 0 0; - .fa-bookmark.bookmarked { color: $tertiary; } + .d-icon-bookmark.bookmarked { color: $tertiary; } .combobox { float: left; @@ -362,7 +371,7 @@ span.post-count { .btn { margin-bottom: 5px; margin-right: 10px; - .fa-star {margin-right: 5px;} + .d-icon-star {margin-right: 5px;} } } @@ -386,7 +395,7 @@ span.post-count { } .moderator .topic-body { - background-color: dark-light-diff($highlight, $secondary, 70%, -80%); + background-color: $highlight-low; } .quote-button.visible { diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index e823050cb7..f52b3ce0fc 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -45,7 +45,7 @@ .topic-status-info { padding-left: 10px; - border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); + border-top: 1px solid $primary-low; padding-top: 10px; h3 { margin: 0; @@ -66,7 +66,7 @@ } #topic-progress-expanded { - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border: 1px solid $primary-low; padding: 5px; background: $secondary; @@ -113,7 +113,7 @@ } background-color: $secondary; color: $tertiary; - border: 1px solid dark-light-diff($tertiary, $secondary, 85%, -65%); + border: 1px solid $tertiary-low; border-bottom: none; width: 145px; height: 34px; @@ -133,7 +133,7 @@ font-size: 1.286em; line-height: 15px; } - i.fa { + .d-icon { position: absolute; right: 8px; bottom: 9px; @@ -144,8 +144,8 @@ top: 0; bottom: 0; width: 0; - border-right: 1px solid dark-light-diff($tertiary, $secondary, 85%, -65%); - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + border-right: 1px solid $tertiary-low; + background-color: $tertiary-low; transition: width .75s; } } diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 7136d89613..3bcb6fb745 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -159,11 +159,11 @@ th { padding: 0.5em; text-align: right; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } td { padding: 0.5em; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; img { margin-right: 10px; } @@ -196,13 +196,13 @@ th { text-align: left; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; padding: 5px; } td { padding: 5px; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } } } @@ -215,7 +215,7 @@ color: $secondary; .secondary { - background: dark-light-diff($primary, $secondary, 90%, -65%); + background: $primary-low; font-size: 0.929em; .btn { padding: 3px 12px; } @@ -241,7 +241,7 @@ } dt { - color: dark-light-diff($secondary, $primary, 50%, -40%); + color: $secondary-medium; margin: 0; } } @@ -251,8 +251,8 @@ background: rgba($secondary, .85); blockquote { - background-color: dark-light-diff($secondary, $primary, 30%, -70%); - border-left-color: dark-light-diff($secondary, $primary, 50%, -50%); + background-color: $secondary-low; + border-left-color: $secondary-low; } h1 { @@ -399,7 +399,7 @@ color: $primary; } .item.moderator-action { - background-color: dark-light-diff($highlight, $secondary, 50%, -10%); + background-color: $highlight-medium; } .item.deleted { opacity: 0.8; @@ -412,7 +412,7 @@ .item { padding: 20px 0 15px 0; background-color: $secondary; - border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-bottom: 1px solid $primary-low; } .type { color: $primary; diff --git a/app/assets/stylesheets/vendor/pikaday.scss b/app/assets/stylesheets/vendor/pikaday.scss index 8465631879..7880b8bc65 100644 --- a/app/assets/stylesheets/vendor/pikaday.scss +++ b/app/assets/stylesheets/vendor/pikaday.scss @@ -14,20 +14,20 @@ $pd-text-color: $primary !default; $pd-title-color: $primary !default; $pd-title-bg: $secondary !default; $pd-picker-bg: $secondary !default; -$pd-picker-border: dark-light-diff($secondary, $primary, -20%, 20%) !default; -$pd-picker-border-bottom: dark-light-diff($secondary, $primary, -30%, 30%) !default; +$pd-picker-border: $primary-low !default; +$pd-picker-border-bottom: $primary-low !default; $pd-picker-shadow: rgba(0,0,0,.5) !default; -$pd-th-color: dark-light-diff($primary, $secondary, -20%, 20%) !default; -$pd-day-color: dark-light-diff($primary, $secondary, -30%, 30%) !default; -$pd-day-bg: dark-light-diff($primary, $secondary, 90%, -90%) !default; -$pd-day-hover-color: $secondary !default; -$pd-day-hover-bg: dark-light-diff($tertiary, $secondary, 30%, -30%) !default; +$pd-th-color: $primary !default; +$pd-day-color: $primary !default; +$pd-day-bg: $secondary !default; +$pd-day-hover-color: $primary !default; +$pd-day-hover-bg: $tertiary-low !default; $pd-day-today-color: $tertiary !default; $pd-day-selected-color: $secondary !default; $pd-day-selected-bg: $tertiary !default; -$pd-day-selected-shadow: dark-light-diff($tertiary, $secondary, 50%, -50%) !default; -$pd-day-disabled-color: dark-light-diff($primary, $secondary, -20%, 20%) !default; -$pd-week-color: dark-light-diff($primary, $secondary, -20%, 20%) !default; +$pd-day-selected-shadow: $tertiary-low !default; +$pd-day-disabled-color: $primary !default; +$pd-week-color: $primary !default; // Font $pd-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !default; diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index 245478866a..922f5afe5d 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -224,11 +224,11 @@ body.wizard { background-color: #ccc; } - i.fa-chevron-right { + .d-icon-chevron-right { margin-left: 0.25em; font-size: 0.8em; } - i.fa-chevron-left { + .d-icon-chevron-left { margin-right: 0.25em; font-size: 0.8em; } @@ -287,7 +287,7 @@ body.wizard { .wizard-btn.next { min-width: 70px; - i.fa-chevron-right { + .d-icon-chevron-right { margin-left: 0.25em; font-size: 0.8em; } diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index e33a476586..6fcdc7e7df 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -5,9 +5,9 @@ class Admin::BadgesController < Admin::AdminController badge_types: BadgeType.all.order(:id).to_a, badge_groupings: BadgeGrouping.all.order(:position).to_a, badges: Badge.includes(:badge_grouping) - .includes(:badge_type) - .references(:badge_grouping) - .order('badge_groupings.position, badge_type_id, badges.name').to_a, + .includes(:badge_type) + .references(:badge_grouping) + .order('badge_groupings.position, badge_type_id, badges.name').to_a, protected_system_fields: Badge.protected_system_fields, triggers: Badge.trigger_hash } @@ -43,9 +43,9 @@ class Admin::BadgesController < Admin::AdminController badge_groupings = BadgeGrouping.all.order(:position).to_a ids = params[:ids].map(&:to_i) - params[:names].each_with_index do |name,index| + params[:names].each_with_index do |name, index| id = ids[index].to_i - group = badge_groupings.find{|b| b.id == id} || BadgeGrouping.new() + group = badge_groupings.find { |b| b.id == id } || BadgeGrouping.new() group.name = name group.position = index group.save @@ -95,7 +95,7 @@ class Admin::BadgesController < Admin::AdminController # Options: # :new - reset the badge id to nil before saving - def update_badge_from_params(badge, opts={}) + def update_badge_from_params(badge, opts = {}) errors = [] Badge.transaction do allowed = Badge.column_names.map(&:to_sym) @@ -112,7 +112,7 @@ class Admin::BadgesController < Admin::AdminController # Badge query contract checks begin if SiteSetting.enable_badge_sql - BadgeGranter.contract_checks!(badge.query, { target_posts: badge.target_posts, trigger: badge.trigger }) + BadgeGranter.contract_checks!(badge.query, target_posts: badge.target_posts, trigger: badge.trigger) end rescue => e errors << e.message diff --git a/app/controllers/admin/color_schemes_controller.rb b/app/controllers/admin/color_schemes_controller.rb index dda6c9f07c..a070f2324d 100644 --- a/app/controllers/admin/color_schemes_controller.rb +++ b/app/controllers/admin/color_schemes_controller.rb @@ -29,7 +29,6 @@ class Admin::ColorSchemesController < Admin::AdminController render json: success_json end - private def fetch_color_scheme diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index d7250ca826..ed021d9158 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -2,13 +2,13 @@ require 'disk_space' class Admin::DashboardController < Admin::AdminController def index dashboard_data = AdminDashboardData.fetch_cached_stats || Jobs::DashboardStats.new.execute({}) - dashboard_data.merge!({version_check: DiscourseUpdates.check_version.as_json}) if SiteSetting.version_checks? + dashboard_data.merge!(version_check: DiscourseUpdates.check_version.as_json) if SiteSetting.version_checks? dashboard_data[:disk_space] = DiskSpace.cached_stats render json: dashboard_data end def problems - render_json_dump({problems: AdminDashboardData.fetch_problems}) + render_json_dump(problems: AdminDashboardData.fetch_problems) end end diff --git a/app/controllers/admin/diagnostics_controller.rb b/app/controllers/admin/diagnostics_controller.rb index ba889837a3..8411e85484 100644 --- a/app/controllers/admin/diagnostics_controller.rb +++ b/app/controllers/admin/diagnostics_controller.rb @@ -42,8 +42,8 @@ class Admin::DiagnosticsController < Admin::AdminController GC.start(full_mark: true) require 'objspace' - io = File.open("discourse-heap-#{SecureRandom.hex(3)}.json",'w') - ObjectSpace.dump_all(:output => io) + io = File.open("discourse-heap-#{SecureRandom.hex(3)}.json", 'w') + ObjectSpace.dump_all(output: io) io.close render plain: "HEAP DUMP:\n#{io.path}" diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 455a22151e..a5030d06dd 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -13,7 +13,7 @@ class Admin::EmailController < Admin::AdminController Jobs::TestEmail.new.execute(to_address: params[:email_address]) render nothing: true rescue => e - render json: {errors: [e.message]}, status: 422 + render json: { errors: [e.message] }, status: 422 end end @@ -55,17 +55,17 @@ class Admin::EmailController < Admin::AdminController params.require(:username) params.require(:email) user = User.find_by_username(params[:username]) - message, skip_reason = UserNotifications.send(:digest, user, {since: params[:last_seen_at]}) + message, skip_reason = UserNotifications.send(:digest, user, since: params[:last_seen_at]) if message message.to = params[:email] begin Email::Sender.new(message, :digest).send render json: success_json rescue => e - render json: {errors: [e.message]}, status: 422 + render json: { errors: [e.message] }, status: 422 end else - render json: {errors: skip_reason} + render json: { errors: skip_reason } end end @@ -73,7 +73,7 @@ class Admin::EmailController < Admin::AdminController params.require(:from) params.require(:to) # These strings aren't localized; they are sent to an anonymous SMTP user. - if !User.exists?(email: Email.downcase(params[:from])) && !SiteSetting.enable_staged_users + if !User.with_email(Email.downcase(params[:from])).exists? && !SiteSetting.enable_staged_users render json: { reject: true, reason: "Mail from your address is not accepted. Do you have an account here?" } elsif Email::Receiver.check_address(Email.downcase(params[:to])).nil? render json: { reject: true, reason: "Mail to this address is not accepted. Check the address and try to send again?" } @@ -131,18 +131,18 @@ class Admin::EmailController < Admin::AdminController serializer = IncomingEmailDetailsSerializer.new(incoming_email, root: false) render_json_dump(serializer) rescue => e - render json: {errors: [e.message]}, status: 404 + render json: { errors: [e.message] }, status: 404 end end private def filter_email_logs(email_logs, params) - email_logs = email_logs.includes(:user, { post: :topic }) - .references(:user) - .order(created_at: :desc) - .offset(params[:offset] || 0) - .limit(50) + email_logs = email_logs.includes(:user, post: :topic) + .references(:user) + .order(created_at: :desc) + .offset(params[:offset] || 0) + .limit(50) email_logs = email_logs.where("users.username ILIKE ?", "%#{params[:user]}%") if params[:user].present? email_logs = email_logs.where("email_logs.to_address ILIKE ?", "%#{params[:address]}%") if params[:address].present? @@ -154,10 +154,10 @@ class Admin::EmailController < Admin::AdminController end def filter_incoming_emails(incoming_emails, params) - incoming_emails = incoming_emails.includes(:user, { post: :topic }) - .order(created_at: :desc) - .offset(params[:offset] || 0) - .limit(50) + incoming_emails = incoming_emails.includes(:user, post: :topic) + .order(created_at: :desc) + .offset(params[:offset] || 0) + .limit(50) incoming_emails = incoming_emails.where("from_address ILIKE ?", "%#{params[:from]}%") if params[:from].present? incoming_emails = incoming_emails.where("to_addresses ILIKE :to OR cc_addresses ILIKE :to", to: "%#{params[:to]}%") if params[:to].present? @@ -170,7 +170,7 @@ class Admin::EmailController < Admin::AdminController def delivery_settings action_mailer_settings .reject { |k, _| k == :password } - .map { |k, v| { name: k, value: v }} + .map { |k, v| { name: k, value: v } } end def delivery_method diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index d3d1e96328..b6fd3b631c 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -15,6 +15,7 @@ class Admin::EmailTemplatesController < Admin::AdminController "system_messages.email_reject_reply_key", "system_messages.email_reject_topic_closed", "system_messages.email_reject_topic_not_found", "system_messages.email_reject_screened_email", + "system_messages.email_reject_unrecognized_error", "system_messages.pending_users_reminder", "system_messages.post_hidden", "system_messages.restore_failed", "system_messages.restore_succeeded", "system_messages.spam_post_blocked", "system_messages.too_many_spam_flags", @@ -26,6 +27,7 @@ class Admin::EmailTemplatesController < Admin::AdminController "user_notifications.set_password", "user_notifications.signup", "user_notifications.signup_after_approval", "user_notifications.user_invited_to_private_message_pm", + "user_notifications.user_invited_to_private_message_pm_group", "user_notifications.user_invited_to_topic", "user_notifications.user_mentioned", "user_notifications.user_posted", "user_notifications.user_posted_pm", "user_notifications.user_quoted", "user_notifications.user_replied", diff --git a/app/controllers/admin/emojis_controller.rb b/app/controllers/admin/emojis_controller.rb index 8bd3f36ede..47b78e83b4 100644 --- a/app/controllers/admin/emojis_controller.rb +++ b/app/controllers/admin/emojis_controller.rb @@ -13,8 +13,8 @@ class Admin::EmojisController < Admin::AdminController Scheduler::Defer.later("Upload Emoji") do # fix the name name = name.gsub(/[^a-z0-9]+/i, '_') - .gsub(/_{2,}/, '_') - .downcase + .gsub(/_{2,}/, '_') + .downcase upload = UploadCreator.new( file.tempfile, @@ -61,4 +61,3 @@ class Admin::EmojisController < Admin::AdminController end end - diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index 1385f958d5..f986ecd43e 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -10,11 +10,11 @@ class Admin::FlagsController < Admin::AdminController if posts.blank? render json: { posts: [], topics: [], users: [] } else - render json: MultiJson.dump({ + render json: MultiJson.dump( posts: posts, topics: serialize_data(topics, FlaggedTopicSerializer), users: serialize_data(users, FlaggedUserSerializer) - }) + ) end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 90045704ff..1ad9367aa8 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,19 +1,4 @@ class Admin::GroupsController < Admin::AdminController - - def index - groups = Group.order(:name).where("groups.id <> ?", Group::AUTO_GROUPS[:everyone]) - - if search = params[:search].to_s - groups = groups.where("name ILIKE ?", "%#{search}%") - end - - if params[:ignore_automatic].to_s == "true" - groups = groups.where(automatic: false) - end - - render_serialized(groups, BasicGroupSerializer) - end - def show render nothing: true end @@ -26,10 +11,14 @@ class Admin::GroupsController < Admin::AdminController group = Group.find(params[:group_id].to_i) users_added = 0 if group.present? - users = (params[:users] || []).map {|u| u.downcase} + users = (params[:users] || []).map { |u| u.downcase } valid_emails = {} valid_usernames = {} - valid_users = User.where("username_lower IN (:users) OR email IN (:users)", users: users).pluck(:id, :username_lower, :email) + + valid_users = User.joins(:user_emails) + .where("username_lower IN (:users) OR user_emails.email IN (:users)", users: users) + .pluck(:id, :username_lower, :"user_emails.email") + valid_users.map! do |id, username_lower, email| valid_emails[email] = valid_usernames[username_lower] = id id @@ -43,10 +32,7 @@ class Admin::GroupsController < Admin::AdminController end def create - group = Group.new - - group.name = (group_params[:name] || '').strip - save_group(group) + save_group(Group.new) end def update @@ -58,6 +44,7 @@ class Admin::GroupsController < Admin::AdminController end def save_group(group) + group.name = group_params[:name] if group_params[:name].present? && !group.automatic group.alias_level = group_params[:alias_level].to_i if group_params[:alias_level].present? if group_params[:visibility_level] @@ -80,7 +67,13 @@ class Admin::GroupsController < Admin::AdminController group.flair_url = group_params[:flair_url].presence group.flair_bg_color = group_params[:flair_bg_color].presence group.flair_color = group_params[:flair_color].presence - group.public = group_params[:public] if group_params[:public] + + %i{public_admission public_exit}.each do |key| + if group_params[key] + group.public_send("#{key}=", group_params[key]) + end + end + group.bio_raw = group_params[:bio_raw] if group_params[:bio_raw] group.full_name = group_params[:full_name] if group_params[:full_name] @@ -92,8 +85,27 @@ class Admin::GroupsController < Admin::AdminController group.allow_membership_requests = group_params[:allow_membership_requests] end + if group_params[:owner_usernames].present? + owner_ids = User.where( + username: group_params[:owner_usernames].split(",") + ).pluck(:id) + + owner_ids.each do |user_id| + group.group_users.build(user_id: user_id, owner: true) + end + end + + if group_params[:usernames].present? + user_ids = User.where(username: group_params[:usernames].split(",")).pluck(:id) + user_ids -= owner_ids if owner_ids + + user_ids.each do |user_id| + group.group_users.build(user_id: user_id) + end + end + if group.save - Group.reset_counters(group.id, :group_users) + group.restore_user_count! yield(group) if block_given? @@ -122,8 +134,7 @@ class Admin::GroupsController < Admin::AdminController def add_owners group = Group.find(params.require(:id)) return can_not_modify_automatic if group.automatic - - users = User.where(username: params[:usernames].split(",")) + users = User.where(username: group_params[:usernames].split(",")) users.each do |user| group_action_logger = GroupActionLogger.new(current_user, group) @@ -136,7 +147,7 @@ class Admin::GroupsController < Admin::AdminController group_action_logger.log_make_user_group_owner(user) end - Group.reset_counters(group.id, :group_users) + group.restore_user_count! render json: success_json end @@ -157,18 +168,33 @@ class Admin::GroupsController < Admin::AdminController protected def can_not_modify_automatic - render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422 + render json: { errors: I18n.t('groups.errors.can_not_modify_automatic') }, status: 422 end private def group_params params.require(:group).permit( - :name, :alias_level, :visibility_level, :automatic_membership_email_domains, - :automatic_membership_retroactive, :title, :primary_group, - :grant_trust_level, :incoming_email, :flair_url, :flair_bg_color, - :flair_color, :bio_raw, :public, :allow_membership_requests, :full_name, - :default_notification_level + :name, + :alias_level, + :visibility_level, + :automatic_membership_email_domains, + :automatic_membership_retroactive, + :title, + :primary_group, + :grant_trust_level, + :incoming_email, + :flair_url, + :flair_bg_color, + :flair_color, + :bio_raw, + :public_admission, + :public_exit, + :allow_membership_requests, + :full_name, + :default_notification_level, + :usernames, + :owner_usernames ) end end diff --git a/app/controllers/admin/screened_ip_addresses_controller.rb b/app/controllers/admin/screened_ip_addresses_controller.rb index 31df2a6b8e..1f4bc31565 100644 --- a/app/controllers/admin/screened_ip_addresses_controller.rb +++ b/app/controllers/admin/screened_ip_addresses_controller.rb @@ -46,7 +46,7 @@ class Admin::ScreenedIpAddressesController < Admin::AdminController def roll_up subnets = ScreenedIpAddress.roll_up(current_user) - render json: success_json.merge!({ subnets: subnets }) + render json: success_json.merge!(subnets: subnets) end private diff --git a/app/controllers/admin/site_settings_controller.rb b/app/controllers/admin/site_settings_controller.rb index 7f20c5d88e..87478597a1 100644 --- a/app/controllers/admin/site_settings_controller.rb +++ b/app/controllers/admin/site_settings_controller.rb @@ -17,7 +17,7 @@ class Admin::SiteSettingsController < Admin::AdminController SiteSetting.set_and_log(id, value, current_user) render nothing: true rescue Discourse::InvalidParameters => e - render json: {errors: [e.message]}, status: 422 + render json: { errors: [e.message] }, status: 422 end end diff --git a/app/controllers/admin/site_texts_controller.rb b/app/controllers/admin/site_texts_controller.rb index 33c7b716fc..8cdb0d9b19 100644 --- a/app/controllers/admin/site_texts_controller.rb +++ b/app/controllers/admin/site_texts_controller.rb @@ -14,7 +14,7 @@ class Admin::SiteTextsController < Admin::AdminController query = params[:q] || "" if query.blank? && !overridden extras[:recommended] = true - results = self.class.preferred_keys.map {|k| record_for(k) } + results = self.class.preferred_keys.map { |k| record_for(k) } else results = [] translations = I18n.search(query, overridden: overridden) @@ -69,14 +69,14 @@ class Admin::SiteTextsController < Admin::AdminController protected - def record_for(k, value=nil) + def record_for(k, value = nil) if k.ends_with?("_MF") ovr = TranslationOverride.where(translation_key: k).pluck(:value) value = ovr[0] if ovr.present? end value ||= I18n.t(k) - {id: k, value: value} + { id: k, value: value } end def find_site_text diff --git a/app/controllers/admin/staff_action_logs_controller.rb b/app/controllers/admin/staff_action_logs_controller.rb index 242cc4f402..2e72e2b86f 100644 --- a/app/controllers/admin/staff_action_logs_controller.rb +++ b/app/controllers/admin/staff_action_logs_controller.rb @@ -6,7 +6,7 @@ class Admin::StaffActionLogsController < Admin::AdminController staff_action_logs = UserHistory.staff_action_records(current_user, filters).to_a render json: StaffActionLogsSerializer.new({ staff_action_logs: staff_action_logs, - user_history_actions: UserHistory.staff_actions.sort.map{|name| {id: UserHistory.actions[name], name: name}} + user_history_actions: UserHistory.staff_actions.sort.map { |name| { id: UserHistory.actions[name], name: name } } }, root: false) end @@ -46,20 +46,18 @@ class Admin::StaffActionLogsController < Admin::AdminController cur: child_themes(cur) } - load_diff(diff_fields, :cur, cur) load_diff(diff_fields, :prev, prev) - diff_fields.delete_if{|k,v| v[:cur] == v[:prev]} + diff_fields.delete_if { |k, v| v[:cur] == v[:prev] } - - diff_fields.each do |k,v| + diff_fields.each do |k, v| output << "

    #{k}

    " diff = DiscourseDiff.new(v[:prev] || "", v[:cur] || "") output << diff.side_by_side_markdown end - render json: {side_by_side: output} + render json: { side_by_side: output } end protected @@ -67,11 +65,11 @@ class Admin::StaffActionLogsController < Admin::AdminController def child_themes(theme) return "" unless children = theme["child_themes"] - children.map{|row| row["name"]}.join(" ").to_s + children.map { |row| row["name"] }.join(" ").to_s end def load_diff(hash, key, val) - if f=val["theme_fields"] + if f = val["theme_fields"] f.each do |row| entry = hash[row["target"] + " " + row["name"]] ||= {} entry[key] = row["value"] diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index d6adf20741..4b8a8c5835 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -62,7 +62,7 @@ class Admin::ThemesController < Admin::AdminController } respond_to do |format| - format.json { render json: payload} + format.json { render json: payload } end end @@ -77,7 +77,7 @@ class Admin::ThemesController < Admin::AdminController if @theme.save update_default_theme log_theme_change(nil, @theme) - format.json { render json: @theme, status: :created} + format.json { render json: @theme, status: :created } else format.json { render json: @theme.errors, status: :unprocessable_entity } end @@ -100,7 +100,7 @@ class Admin::ThemesController < Admin::AdminController @theme.child_theme_relation.to_a.each do |child| if expected.include?(child.child_theme_id) - expected.reject!{|id| id == child.child_theme_id} + expected.reject! { |id| id == child.child_theme_id } else child.destroy end @@ -133,12 +133,12 @@ class Admin::ThemesController < Admin::AdminController update_default_theme log_theme_change(original_json, @theme) - format.json { render json: @theme, status: :created} + format.json { render json: @theme, status: :created } else format.json { error = @theme.errors[:color_scheme] ? I18n.t("themes.bad_color_scheme") : I18n.t("themes.other_error") - render json: {errors: [ error ]}, status: :unprocessable_entity + render json: { errors: [ error ] }, status: :unprocessable_entity } end end @@ -192,13 +192,14 @@ class Admin::ThemesController < Admin::AdminController begin # deep munge is a train wreck, work around it for now params[:theme][:child_theme_ids] ||= [] if params[:theme].key?(:child_theme_ids) - params.require(:theme) - .permit(:name, - :color_scheme_id, - :default, - :user_selectable, - theme_fields: [:name, :target, :value, :upload_id, :type_id], - child_theme_ids: []) + params.require(:theme).permit( + :name, + :color_scheme_id, + :default, + :user_selectable, + theme_fields: [:name, :target, :value, :upload_id, :type_id], + child_theme_ids: [] + ) end end diff --git a/app/controllers/admin/user_fields_controller.rb b/app/controllers/admin/user_fields_controller.rb index 596b53e4ee..2af8832e33 100644 --- a/app/controllers/admin/user_fields_controller.rb +++ b/app/controllers/admin/user_fields_controller.rb @@ -51,8 +51,7 @@ class Admin::UserFieldsController < Admin::AdminController options = params[:user_field][:options] if options.present? UserFieldOption.where(user_field_id: field.id).delete_all - field.user_field_options_attributes = options.map {|o| {value: o} }.uniq + field.user_field_options_attributes = options.map { |o| { value: o } }.uniq end end end - diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 5db8236745..36dab7149f 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -77,7 +77,7 @@ class Admin::UsersController < Admin::AdminController @user.logged_out render json: success_json else - render json: {error: I18n.t('admin_js.admin.users.id_not_found')}, status: 404 + render json: { error: I18n.t('admin_js.admin.users.id_not_found') }, status: 404 end end @@ -154,8 +154,7 @@ class Admin::UsersController < Admin::AdminController guardian.ensure_can_change_trust_level!(@user) level = params[:level].to_i - - if !@user.trust_level_locked && [0,1,2].include?(level) && Promotion.send("tl#{level+1}_met?", @user) + if !@user.trust_level_locked && [0, 1, 2].include?(level) && Promotion.send("tl#{level + 1}_met?", @user) @user.trust_level_locked = true @user.save end @@ -187,7 +186,7 @@ class Admin::UsersController < Admin::AdminController unless @user.trust_level_locked p = Promotion.new(@user) - 2.times{ p.review } + 2.times { p.review } p.review_tl2 if @user.trust_level == 3 && Promotion.tl3_lost?(@user) @user.change_trust_level!(2, log_action_for: current_user) @@ -212,6 +211,8 @@ class Admin::UsersController < Admin::AdminController def activate guardian.ensure_can_activate!(@user) + # ensure there is an active email token + @user.email_tokens.create(email: @user.email) unless @user.email_tokens.active.exists? @user.activate StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff')) render json: success_json @@ -242,7 +243,7 @@ class Admin::UsersController < Admin::AdminController d = UserDestroyer.new(current_user) User.where(id: params[:users]).each do |u| - success_count += 1 if guardian.can_delete_user?(u) and d.destroy(u, params.slice(:context)) rescue UserDestroyer::PostsExistError + success_count += 1 if guardian.can_delete_user?(u) && d.destroy(u, params.slice(:context)) rescue UserDestroyer::PostsExistError end render json: { @@ -339,12 +340,12 @@ class Admin::UsersController < Admin::AdminController user.save! user.grant_admin! user.change_trust_level!(4) - user.email_tokens.update_all confirmed: true + user.email_tokens.update_all confirmed: true email_token = user.email_tokens.create(email: user.email) unless params[:send_email] == '0' || params[:send_email] == 'false' - Jobs.enqueue( :critical_user_email, + Jobs.enqueue(:critical_user_email, type: :account_created, user_id: user.id, email_token: email_token.token) diff --git a/app/controllers/admin/watched_words_controller.rb b/app/controllers/admin/watched_words_controller.rb new file mode 100644 index 0000000000..119562dc4e --- /dev/null +++ b/app/controllers/admin/watched_words_controller.rb @@ -0,0 +1,47 @@ +class Admin::WatchedWordsController < Admin::AdminController + + def index + render_json_dump WatchedWordListSerializer.new(WatchedWord.by_action, scope: guardian, root: false) + end + + def create + watched_word = WatchedWord.create_or_update_word(watched_words_params) + if watched_word.valid? + render json: watched_word, root: false + else + render_json_error(watched_word) + end + end + + def destroy + watched_word = WatchedWord.find(params[:id]) + watched_word.destroy + render json: success_json + end + + def upload + file = params[:file] || params[:files].first + action_key = params[:action_key].to_sym + + Scheduler::Defer.later("Upload watched words") do + begin + File.open(file.tempfile, encoding: "ISO-8859-1").each_line do |line| + WatchedWord.create_or_update_word(word: line, action_key: action_key) unless line.empty? + end + data = { url: '/ok' } + rescue => e + data = failed_json.merge(errors: [e.message]) + end + MessageBus.publish("/uploads/csv", data.as_json, client_ids: [params[:client_id]]) + end + + render json: success_json + end + + private + + def watched_words_params + params.permit(:id, :word, :action_key) + end + +end diff --git a/app/controllers/admin/web_hooks_controller.rb b/app/controllers/admin/web_hooks_controller.rb index 4deb554c8b..c3b2cfe9ef 100644 --- a/app/controllers/admin/web_hooks_controller.rb +++ b/app/controllers/admin/web_hooks_controller.rb @@ -6,10 +6,10 @@ class Admin::WebHooksController < Admin::AdminController offset = params[:offset].to_i web_hooks = WebHook.limit(limit) - .offset(offset) - .includes(:web_hook_event_types) - .includes(:categories) - .includes(:groups) + .offset(offset) + .includes(:web_hook_event_types) + .includes(:categories) + .includes(:groups) json = { web_hooks: serialize_data(web_hooks, AdminWebHookSerializer), diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7527f4a9ba..1484df2603 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -147,7 +147,7 @@ class ApplicationController < ActionController::Base render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503 end - def rescue_discourse_actions(type, status_code, include_ember=false) + def rescue_discourse_actions(type, status_code, include_ember = false) show_json_errors = (request.format && request.format.json?) || (request.xhr?) || @@ -175,7 +175,7 @@ class ApplicationController < ActionController::Base def set_current_user_for_logs if current_user - Logster.add_to_env(request.env,"username",current_user.username) + Logster.add_to_env(request.env, "username", current_user.username) response.headers["X-Discourse-Username"] = current_user.username end response.headers["X-Discourse-Route"] = "#{controller_name}/#{action_name}" @@ -298,9 +298,9 @@ class ApplicationController < ActionController::Base current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage end - def serialize_data(obj, serializer, opts=nil) + def serialize_data(obj, serializer, opts = nil) # If it's an array, apply the serializer as an each_serializer to the elements - serializer_opts = {scope: guardian}.merge!(opts || {}) + serializer_opts = { scope: guardian }.merge!(opts || {}) if obj.respond_to?(:to_ary) serializer_opts[:each_serializer] = serializer ActiveModel::ArraySerializer.new(obj.to_ary, serializer_opts).as_json @@ -313,11 +313,11 @@ class ApplicationController < ActionController::Base # 20% slower than calling MultiJSON.dump ourselves. I'm not sure why # Rails doesn't call MultiJson.dump when you pass it json: obj but # it seems we don't need whatever Rails is doing. - def render_serialized(obj, serializer, opts=nil) + def render_serialized(obj, serializer, opts = nil) render_json_dump(serialize_data(obj, serializer, opts), opts) end - def render_json_dump(obj, opts=nil) + def render_json_dump(obj, opts = nil) opts ||= {} if opts[:rest_serializer] obj['__rest_serializer'] = "1" @@ -341,7 +341,7 @@ class ApplicationController < ActionController::Base Middleware::AnonymousCache.anon_cache(request.env, time_length) end - def fetch_user_from_params(opts=nil, eager_load = []) + def fetch_user_from_params(opts = nil, eager_load = []) opts ||= {} user = if params[:username] username_lower = params[:username].downcase.chomp('.json') @@ -361,9 +361,9 @@ class ApplicationController < ActionController::Base end def post_ids_including_replies - post_ids = params[:post_ids].map {|p| p.to_i} + post_ids = params[:post_ids].map { |p| p.to_i } if params[:reply_post_ids] - post_ids |= PostReply.where(post_id: params[:reply_post_ids].map {|p| p.to_i}).pluck(:reply_id) + post_ids |= PostReply.where(post_id: params[:reply_post_ids].map { |p| p.to_i }).pluck(:reply_id) end post_ids end @@ -394,7 +394,6 @@ class ApplicationController < ActionController::Base end end - def secure_session SecureSession.new(session["secure_session_id"] ||= SecureRandom.hex) end @@ -433,14 +432,16 @@ class ApplicationController < ActionController::Base def custom_html_json target = view_context.mobile_view? ? :mobile : :desktop - data = if @theme_key - { - top: Theme.lookup_field(@theme_key, target, "after_header"), - footer: Theme.lookup_field(@theme_key, target, "footer") - } - else - {} - end + + data = + if @theme_key + { + top: Theme.lookup_field(@theme_key, target, "after_header"), + footer: Theme.lookup_field(@theme_key, target, "footer") + } + else + {} + end if DiscoursePluginRegistry.custom_html data.merge! DiscoursePluginRegistry.custom_html @@ -480,7 +481,7 @@ class ApplicationController < ActionController::Base # opts: # type - a machine-readable description of the error # status - HTTP status code to return - def render_json_error(obj, opts={}) + def render_json_error(obj, opts = {}) opts = { status: opts } if opts.is_a?(Integer) render json: MultiJson.dump(create_errors_json(obj, opts[:type])), status: opts[:status] || 422 end @@ -493,7 +494,7 @@ class ApplicationController < ActionController::Base { failed: 'FAILED' } end - def json_result(obj, opts={}) + def json_result(obj, opts = {}) if yield(obj) json = success_json @@ -574,21 +575,20 @@ class ApplicationController < ActionController::Base raise Discourse::ReadOnly.new if !(request.get? || request.head?) && Discourse.readonly_mode? end - def build_not_found_page(status=404, layout=false) + def build_not_found_page(status = 404, layout = false) category_topic_ids = Category.pluck(:topic_id).compact @container_class = "wrap not-found-container" - @top_viewed = TopicQuery.new(nil, {except_topic_ids: category_topic_ids}).list_top_for("monthly").topics.first(10) + @top_viewed = TopicQuery.new(nil, except_topic_ids: category_topic_ids).list_top_for("monthly").topics.first(10) @recent = Topic.where.not(id: category_topic_ids).recent(10) @slug = params[:slug].class == String ? params[:slug] : '' @slug = (params[:id].class == String ? params[:id] : '') if @slug.blank? - @slug.tr!('-',' ') + @slug.tr!('-', ' ') render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found' end - protected - def render_post_json(post, add_raw=true) + def render_post_json(post, add_raw = true) post_serializer = PostSerializer.new(post, scope: guardian, root: false) post_serializer.add_raw = add_raw diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb index 432489a3d4..6447558bef 100644 --- a/app/controllers/badges_controller.rb +++ b/app/controllers/badges_controller.rb @@ -14,7 +14,7 @@ class BadgesController < ApplicationController if (params[:only_listable] == "true") || !request.xhr? # NOTE: this is sorted client side if needed badges = badges.includes(:badge_grouping) - .where(enabled: true, listable: true) + .where(enabled: true, listable: true) end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index c2da87bac4..51e8e08852 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -218,7 +218,7 @@ class CategoriesController < ApplicationController end if p = params[:permissions] - p.each do |k,v| + p.each do |k, v| p[k] = v.to_i end end @@ -250,10 +250,10 @@ class CategoriesController < ApplicationController :default_view, :subcategory_list_style, :default_top_period, - :custom_fields => [params[:custom_fields].try(:keys)], - :permissions => [*p.try(:keys)], - :allowed_tags => [], - :allowed_tag_groups => []) + custom_fields: [params[:custom_fields].try(:keys)], + permissions: [*p.try(:keys)], + allowed_tags: [], + allowed_tag_groups: []) end end @@ -265,7 +265,7 @@ class CategoriesController < ApplicationController @staff_action_logger = StaffActionLogger.new(current_user) end - def include_topics(parent_category=nil) + def include_topics(parent_category = nil) view_context.mobile_view? || params[:include_topics] || (parent_category && parent_category.subcategory_list_includes_topics?) || diff --git a/app/controllers/clicks_controller.rb b/app/controllers/clicks_controller.rb index 4d7e186b0e..23b1684f2b 100644 --- a/app/controllers/clicks_controller.rb +++ b/app/controllers/clicks_controller.rb @@ -8,7 +8,7 @@ class ClicksController < ApplicationController params = track_params.merge(ip: request.remote_ip) if params[:topic_id].present? || params[:post_id].present? - params.merge!({ user_id: current_user.id }) if current_user.present? + params.merge!(user_id: current_user.id) if current_user.present? @redirect_url = TopicLinkClick.create_from(params) end diff --git a/app/controllers/composer_messages_controller.rb b/app/controllers/composer_messages_controller.rb index ff1a51d2bd..62f86b0583 100644 --- a/app/controllers/composer_messages_controller.rb +++ b/app/controllers/composer_messages_controller.rb @@ -11,7 +11,7 @@ class ComposerMessagesController < ApplicationController if params[:topic_id].present? topic = Topic.where(id: params[:topic_id]).first if guardian.can_see?(topic) - json[:extras] = {duplicate_lookup: TopicLink.duplicate_lookup(topic)} + json[:extras] = { duplicate_lookup: TopicLink.duplicate_lookup(topic) } end end diff --git a/app/controllers/directory_items_controller.rb b/app/controllers/directory_items_controller.rb index 6158534b40..22c723c1e7 100644 --- a/app/controllers/directory_items_controller.rb +++ b/app/controllers/directory_items_controller.rb @@ -53,7 +53,7 @@ class DirectoryItemsController < ApplicationController # Put yourself at the top of the first page if result.present? && current_user.present? && page == 0 - position = result.index {|r| r.user_id == current_user.id } + position = result.index { |r| r.user_id == current_user.id } # Don't show the record unless you're not in the top positions already if (position || 10) >= 10 diff --git a/app/controllers/draft_controller.rb b/app/controllers/draft_controller.rb index 7af211db56..4e7a043747 100644 --- a/app/controllers/draft_controller.rb +++ b/app/controllers/draft_controller.rb @@ -5,7 +5,7 @@ class DraftController < ApplicationController def show seq = params[:sequence] || DraftSequence.current(current_user, params[:draft_key]) - render json: {draft: Draft.get(current_user, params[:draft_key], seq), draft_sequence: seq} + render json: { draft: Draft.get(current_user, params[:draft_key], seq), draft_sequence: seq } end def update diff --git a/app/controllers/email_controller.rb b/app/controllers/email_controller.rb index 76861025c3..771e6e1b37 100644 --- a/app/controllers/email_controller.rb +++ b/app/controllers/email_controller.rb @@ -9,51 +9,42 @@ class EmailController < ApplicationController end def unsubscribe - key = UnsubscribeKey.find_by(key: params[:key]) + @not_found = true + @watched_count = nil - if key - @user = key.user - post = key.post - @topic = (post && post.topic) || key.topic - @type = key.unsubscribe_key_type + if key = UnsubscribeKey.find_by(key: params[:key]) + if @user = key.user + post = key.post + @topic = post&.topic || key.topic + @type = key.unsubscribe_key_type + @not_found = false - if current_user.present? && (@user != current_user) - @different_user = @user.name - @return_url = request.original_url - end + if current_user.present? && (@user != current_user) + @different_user = @user.name + @return_url = request.original_url + end - @watching_topic = @topic && TopicUser.exists?(user_id: @user.id, - notification_level: TopicUser.notification_levels[:watching], - topic_id: @topic.id) + watching = TopicUser.notification_levels[:watching] - @watched_count = nil - if @topic && @topic.category_id - if CategoryUser.exists?(user_id: @user.id, - notification_level: CategoryUser.watching_levels, - category_id: @topic.category_id) - @watched_count = TopicUser.joins(:topic) - .where(:user => @user, - :notification_level => TopicUser.notification_levels[:watching], - "topics.category_id" => @topic.category_id - ).count + if @topic + @watching_topic = TopicUser.exists?(user_id: @user.id, notification_level: watching, topic_id: @topic.id) + if @topic.category_id + if CategoryUser.exists?(user_id: @user.id, notification_level: CategoryUser.watching_levels, category_id: @topic.category_id) + @watched_count = TopicUser.joins(:topic) + .where(user: @user, notification_level: watching, "topics.category_id" => @topic.category_id) + .count + end + end end end end - - if @user.blank? - @not_found = true - end - end def perform_unsubscribe - key = UnsubscribeKey.find_by(key: params[:key]) - unless key && key.user - raise Discourse::NotFound - end + raise Discourse::NotFound unless key && key.user - topic = (key.post && key.post.topic) || key.topic + topic = key&.post&.topic || key.topic user = key.user updated = false @@ -61,28 +52,28 @@ class EmailController < ApplicationController if topic if params["unwatch_topic"] TopicUser.where(topic_id: topic.id, user_id: user.id) - .update_all(notification_level: TopicUser.notification_levels[:tracking]) + .update_all(notification_level: TopicUser.notification_levels[:tracking]) updated = true end if params["unwatch_category"] && topic.category_id TopicUser.joins(:topic) - .where(:user => user, - :notification_level => TopicUser.notification_levels[:watching], - "topics.category_id" => topic.category_id) - .update_all(notification_level: TopicUser.notification_levels[:tracking]) + .where(:user => user, + :notification_level => TopicUser.notification_levels[:watching], + "topics.category_id" => topic.category_id) + .update_all(notification_level: TopicUser.notification_levels[:tracking]) CategoryUser.where(user_id: user.id, - category_id: topic.category_id, - notification_level: CategoryUser.watching_levels + category_id: topic.category_id, + notification_level: CategoryUser.watching_levels ) - .destroy_all + .destroy_all updated = true end if params["mute_topic"] TopicUser.where(topic_id: topic.id, user_id: user.id) - .update_all(notification_level: TopicUser.notification_levels[:muted]) + .update_all(notification_level: TopicUser.notification_levels[:muted]) updated = true end end @@ -99,9 +90,9 @@ class EmailController < ApplicationController if params["unsubscribe_all"] user.user_option.update_columns(email_always: false, - email_digests: false, - email_direct: false, - email_private_messages: false) + email_digests: false, + email_direct: false, + email_private_messages: false) updated = true end diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index f64cc8035c..d7da707d98 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -74,7 +74,7 @@ class EmbedController < ApplicationController by_url = {} if embed_urls.present? - urls = embed_urls.map {|u| u.sub(/#discourse-comments$/, '').sub(/\/$/, '') } + urls = embed_urls.map { |u| u.sub(/#discourse-comments$/, '').sub(/\/$/, '') } topic_embeds = TopicEmbed.where(embed_url: urls).includes(:topic).references(:topic) topic_embeds.each do |te| @@ -88,7 +88,7 @@ class EmbedController < ApplicationController end end - render json: {counts: by_url}, callback: params[:callback] + render json: { counts: by_url }, callback: params[:callback] end private @@ -96,7 +96,7 @@ class EmbedController < ApplicationController def get_embeddable_css_class @embeddable_css_class = "" embeddable_host = EmbeddableHost.record_for_url(request.referer) - @embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? and embeddable_host.class_name.present? + @embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && embeddable_host.class_name.present? end def ensure_api_request @@ -114,5 +114,4 @@ class EmbedController < ApplicationController raise Discourse::InvalidAccess.new('invalid referer host') end - end diff --git a/app/controllers/finish_installation_controller.rb b/app/controllers/finish_installation_controller.rb index c9a30724d4..4e73594d8a 100644 --- a/app/controllers/finish_installation_controller.rb +++ b/app/controllers/finish_installation_controller.rb @@ -15,7 +15,7 @@ class FinishInstallationController < ApplicationController email = params[:email].strip raise Discourse::InvalidParameters.new unless @allowed_emails.include?(email) - return redirect_confirm(email) if User.where(email: email).exists? + return redirect_confirm(email) if UserEmail.exists?(email: email) @user.email = email @user.username = params[:username] @@ -37,7 +37,7 @@ class FinishInstallationController < ApplicationController def resend_email @email = session[:registered_email] - @user = User.where(email: @email).first + @user = User.find_by_email(@email) if @user.present? @email_token = @user.email_tokens.unconfirmed.active.first if @email_token.present? diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 74b7ad0f66..d73b87e4d6 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -6,7 +6,8 @@ class GroupsController < ApplicationController :update, :messages, :histories, - :request_membership + :request_membership, + :search ] skip_before_filter :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed] @@ -123,13 +124,13 @@ class GroupsController < ApplicationController members = group.users .order('NOT group_users.owner') .order(order) - .order(:username_lower => dir) + .order(username_lower: dir) .limit(limit) .offset(offset) owners = group.users .order(order) - .order(:username_lower => dir) + .order(username_lower: dir) .where('group_users.owner') render json: { @@ -145,7 +146,7 @@ class GroupsController < ApplicationController def add_members group = Group.find(params[:id]) - group.public ? ensure_logged_in : guardian.ensure_can_edit!(group) + group.public_admission ? ensure_logged_in : guardian.ensure_can_edit!(group) users = if params[:usernames].present? @@ -153,7 +154,7 @@ class GroupsController < ApplicationController elsif params[:user_ids].present? User.find(params[:user_ids].split(",")) elsif params[:user_emails].present? - User.where(email: params[:user_emails].split(",")) + User.with_email(params[:user_emails].split(",")) else raise Discourse::InvalidParameters.new( 'user_ids or usernames or user_emails must be present' @@ -162,7 +163,7 @@ class GroupsController < ApplicationController raise Discourse::NotFound if users.blank? - if group.public + if group.public_admission if !guardian.can_log_group_changes?(group) && current_user != users.first raise Discourse::InvalidAccess end @@ -200,7 +201,7 @@ class GroupsController < ApplicationController def remove_member group = Group.find(params[:id]) - group.public ? ensure_logged_in : guardian.ensure_can_edit!(group) + group.public_exit ? ensure_logged_in : guardian.ensure_can_edit!(group) user = if params[:user_id].present? @@ -215,7 +216,7 @@ class GroupsController < ApplicationController raise Discourse::NotFound unless user - if group.public + if group.public_exit if !guardian.can_log_group_changes?(group) && current_user != user raise Discourse::InvalidAccess end @@ -273,8 +274,8 @@ class GroupsController < ApplicationController end GroupUser.where(group_id: group.id) - .where(user_id: user_id) - .update_all(notification_level: notification_level) + .where(user_id: user_id) + .update_all(notification_level: notification_level) render json: success_json end @@ -296,6 +297,22 @@ class GroupsController < ApplicationController ) end + def search + groups = Group.visible_groups(current_user) + .where("groups.id <> ?", Group::AUTO_GROUPS[:everyone]) + .order(:name) + + if term = params[:term].to_s + groups = groups.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{term}%") + end + + if params[:ignore_automatic].to_s == "true" + groups = groups.where(automatic: false) + end + + render_serialized(groups, BasicGroupSerializer) + end + private def group_params @@ -305,7 +322,8 @@ class GroupsController < ApplicationController :flair_color, :bio_raw, :full_name, - :public, + :public_admission, + :public_exit, :allow_membership_requests ) end diff --git a/app/controllers/highlight_js_controller.rb b/app/controllers/highlight_js_controller.rb index 86137be56e..19a83c4a76 100644 --- a/app/controllers/highlight_js_controller.rb +++ b/app/controllers/highlight_js_controller.rb @@ -26,4 +26,3 @@ class HighlightJsController < ApplicationController end end end - diff --git a/app/controllers/inline_onebox_controller.rb b/app/controllers/inline_onebox_controller.rb new file mode 100644 index 0000000000..67afcf01b2 --- /dev/null +++ b/app/controllers/inline_onebox_controller.rb @@ -0,0 +1,10 @@ +require_dependency 'inline_oneboxer' + +class InlineOneboxController < ApplicationController + before_filter :ensure_logged_in + + def show + oneboxes = InlineOneboxer.new(params[:urls]).process + render json: { "inline-oneboxes" => oneboxes } + end +end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 4da902a8e4..a892c50cfc 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -16,11 +16,12 @@ class InvitesController < ApplicationController invite = Invite.find_by(invite_key: params[:id]) if invite.present? - store_preloaded("invite_info", MultiJson.dump({ + store_preloaded("invite_info", MultiJson.dump( invited_by: UserNameSerializer.new(invite.invited_by, scope: guardian, root: false), email: invite.email, - username: UserNameSuggester.suggest(invite.email) - })) + username: UserNameSuggester.suggest(invite.email)) + ) + render layout: 'application' else flash.now[:error] = I18n.t('invite.not_found') @@ -61,9 +62,13 @@ class InvitesController < ApplicationController def create params.require(:email) - group_ids = Group.lookup_group_ids(params) + groups = Group.lookup_groups( + group_ids: params[:group_ids], + group_names: params[:group_names] + ) - guardian.ensure_can_invite_to_forum!(group_ids) + guardian.ensure_can_invite_to_forum!(groups) + group_ids = groups.map(&:id) invite_exists = Invite.where(email: params[:email], invited_by_id: current_user.id).first if invite_exists && !guardian.can_send_multiple_invites?(current_user) @@ -71,21 +76,27 @@ class InvitesController < ApplicationController end begin - if Invite.invite_by_email(params[:email], current_user, _topic=nil, group_ids, params[:custom_message]) + if Invite.invite_by_email(params[:email], current_user, nil, group_ids, params[:custom_message]) render json: success_json else render json: failed_json, status: 422 end rescue Invite::UserExists, ActiveRecord::RecordInvalid => e - render json: {errors: [e.message]}, status: 422 + render json: { errors: [e.message] }, status: 422 end end def create_invite_link params.require(:email) - group_ids = Group.lookup_group_ids(params) + + groups = Group.lookup_groups( + group_ids: params[:group_ids], + group_names: params[:group_names] + ) + + guardian.ensure_can_invite_to_forum!(groups) topic = Topic.find_by(id: params[:topic_id]) - guardian.ensure_can_invite_to_forum!(group_ids) + group_ids = groups.map(&:id) invite_exists = Invite.where(email: params[:email], invited_by_id: current_user.id).first if invite_exists && !guardian.can_send_multiple_invites?(current_user) @@ -100,7 +111,7 @@ class InvitesController < ApplicationController render json: failed_json, status: 422 end rescue => e - render json: {errors: [e.message]}, status: 422 + render json: { errors: [e.message] }, status: 422 end end @@ -153,7 +164,7 @@ class InvitesController < ApplicationController data = if extension.downcase == ".csv" path = Invite.create_csv(file, name) Jobs.enqueue(:bulk_invite, filename: "#{name}#{extension}", current_user_id: current_user.id) - {url: path} + { url: path } else failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")]) end diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 66e59247f9..65635e5429 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -205,7 +205,7 @@ class ListController < ApplicationController render 'list', formats: [:rss] end - def top(options=nil) + def top(options = nil) options ||= {} period = ListController.best_period_for(current_user.try(:previous_visit_at), options[:category]) send("top_#{period}", options) @@ -308,7 +308,7 @@ class ListController < ApplicationController parent_category_id = nil if parent_slug_or_id.present? parent_category_id = Category.query_parent_category(parent_slug_or_id) - permalink_redirect_or_not_found and return if parent_category_id.blank? && !id + permalink_redirect_or_not_found && (return) if parent_category_id.blank? && !id end @category = Category.query_category(slug_or_id, parent_category_id) @@ -319,7 +319,7 @@ class ListController < ApplicationController (redirect_to category.url, status: 301) && return if category end - permalink_redirect_or_not_found and return if !@category + permalink_redirect_or_not_found && (return) if !@category @description_meta = @category.description_text raise Discourse::NotFound unless guardian.can_see?(@category) @@ -362,23 +362,23 @@ class ListController < ApplicationController else # :next public_send(method, opts.merge(next_page_params(opts))) end - url.sub('.json?','?') + url.sub('.json?', '?') end - def get_excluded_category_ids(current_category=nil) + def get_excluded_category_ids(current_category = nil) exclude_category_ids = Category.where(suppress_from_homepage: true) exclude_category_ids = exclude_category_ids.where.not(id: current_category) if current_category exclude_category_ids.pluck(:id) end - def self.best_period_for(previous_visit_at, category_id=nil) + def self.best_period_for(previous_visit_at, category_id = nil) default_period = ((category_id && Category.where(id: category_id).pluck(:default_top_period).first) || SiteSetting.top_page_default_timeframe).to_sym best_period_with_topics_for(previous_visit_at, category_id, default_period) || default_period end - def self.best_period_with_topics_for(previous_visit_at, category_id=nil, default_period=SiteSetting.top_page_default_timeframe) + def self.best_period_with_topics_for(previous_visit_at, category_id = nil, default_period = SiteSetting.top_page_default_timeframe) best_periods_for(previous_visit_at, default_period.to_sym).each do |period| top_topics = TopTopic.where("#{period}_score > 0") top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id @@ -389,12 +389,12 @@ class ListController < ApplicationController false end - def self.best_periods_for(date, default_period=:all) + def self.best_periods_for(date, default_period = :all) date ||= 1.year.ago periods = [] periods << default_period if :all != default_period - periods << :daily if :daily != default_period && date > 8.days.ago - periods << :weekly if :weekly != default_period && date > 35.days.ago + periods << :daily if :daily != default_period && date > 8.days.ago + periods << :weekly if :weekly != default_period && date > 35.days.ago periods << :monthly if :monthly != default_period && date > 180.days.ago periods << :yearly if :yearly != default_period periods diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index 04a5eaafd4..c297ed18bd 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -31,7 +31,7 @@ class MetadataController < ApplicationController } if SiteSetting.native_app_install_banner - manifest = manifest.merge({ + manifest = manifest.merge( prefer_related_applications: true, related_applications: [ { @@ -39,7 +39,7 @@ class MetadataController < ApplicationController id: "com.discourse" } ] - }) + ) end manifest diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 1415f78f57..7f6b95ca53 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -37,9 +37,9 @@ class NotificationsController < ApplicationController offset = params[:offset].to_i notifications = Notification.where(user_id: user.id) - .visible - .includes(:topic) - .order(created_at: :desc) + .visible + .includes(:topic) + .order(created_at: :desc) total_rows = notifications.dup.count notifications = notifications.offset(offset).limit(60) diff --git a/app/controllers/post_action_users_controller.rb b/app/controllers/post_action_users_controller.rb index e9e6c05480..46b6328d61 100644 --- a/app/controllers/post_action_users_controller.rb +++ b/app/controllers/post_action_users_controller.rb @@ -12,10 +12,9 @@ class PostActionUsersController < ApplicationController post = finder.first guardian.ensure_can_see!(post) - post_actions = post.post_actions.where(post_action_type_id: post_action_type_id) - .includes(:user) - .order('post_actions.created_at asc') + .includes(:user) + .order('post_actions.created_at asc') if !guardian.can_see_post_actors?(post.topic, post_action_type_id) if !current_user diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 5e18b57520..1794ddded0 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -41,23 +41,23 @@ class PostsController < ApplicationController if params[:id] == "private_posts" raise Discourse::NotFound if current_user.nil? posts = Post.private_posts - .order(created_at: :desc) - .where('posts.id <= ?', last_post_id) - .where('posts.id > ?', last_post_id - 50) - .includes(topic: :category) - .includes(user: :primary_group) - .includes(:reply_to_user) - .limit(50) + .order(created_at: :desc) + .where('posts.id <= ?', last_post_id) + .where('posts.id > ?', last_post_id - 50) + .includes(topic: :category) + .includes(user: :primary_group) + .includes(:reply_to_user) + .limit(50) rss_description = I18n.t("rss_description.private_posts") else posts = Post.public_posts - .order(created_at: :desc) - .where('posts.id <= ?', last_post_id) - .where('posts.id > ?', last_post_id - 50) - .includes(topic: :category) - .includes(user: :primary_group) - .includes(:reply_to_user) - .limit(50) + .order(created_at: :desc) + .where('posts.id <= ?', last_post_id) + .where('posts.id > ?', last_post_id - 50) + .includes(topic: :category) + .includes(user: :primary_group) + .includes(:reply_to_user) + .limit(50) rss_description = I18n.t("rss_description.posts") end @@ -92,12 +92,12 @@ class PostsController < ApplicationController user = fetch_user_from_params posts = Post.public_posts - .where(user_id: user.id) - .where(post_type: Post.types[:regular]) - .order(created_at: :desc) - .includes(:user) - .includes(topic: :category) - .limit(50) + .where(user_id: user.id) + .where(post_type: Post.types[:regular]) + .order(created_at: :desc) + .includes(:user) + .includes(topic: :category) + .limit(50) posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? } @@ -230,20 +230,20 @@ class PostsController < ApplicationController RateLimiter.new(current_user, "delete_post", 3, 1.minute).performed! unless current_user.staff? if too_late_to(:delete_post, post) - render json: {errors: [I18n.t('too_late_to_edit')]}, status: 422 + render json: { errors: [I18n.t('too_late_to_edit')] }, status: 422 return end guardian.ensure_can_delete!(post) - destroyer = PostDestroyer.new(current_user, post, { context: params[:context] }) + destroyer = PostDestroyer.new(current_user, post, context: params[:context]) destroyer.destroy render nothing: true end def expand_embed - render json: {cooked: TopicEmbed.expanded_for(find_post_from_params) } + render json: { cooked: TopicEmbed.expanded_for(find_post_from_params) } rescue render_json_error I18n.t('errors.embed.load_from_remote') end @@ -266,10 +266,10 @@ class PostsController < ApplicationController raise Discourse::InvalidParameters.new(:post_ids) if posts.blank? # Make sure we can delete the posts - posts.each {|p| guardian.ensure_can_delete!(p) } + posts.each { |p| guardian.ensure_can_delete!(p) } Post.transaction do - posts.each {|p| PostDestroyer.new(current_user, p).destroy } + posts.each { |p| PostDestroyer.new(current_user, p).destroy } end render nothing: true @@ -399,7 +399,7 @@ class PostsController < ApplicationController post = find_post_from_params guardian.ensure_can_wiki!(post) - post.revise(current_user, { wiki: params[:wiki] }) + post.revise(current_user, wiki: params[:wiki]) render nothing: true end @@ -408,7 +408,7 @@ class PostsController < ApplicationController guardian.ensure_can_change_post_type! post = find_post_from_params - post.revise(current_user, { post_type: params[:post_type].to_i }) + post.revise(current_user, post_type: params[:post_type].to_i) render nothing: true end @@ -441,7 +441,7 @@ class PostsController < ApplicationController limit = [(params[:limit] || 60).to_i, 100].min posts = user_posts(guardian, user.id, offset: offset, limit: limit) - .where(id: PostAction.where(post_action_type_id: PostActionType.notify_flag_type_ids) + .where(id: PostAction.where(post_action_type_id: PostActionType.notify_flag_type_ids) .where(disagreed_at: nil) .select(:post_id)) @@ -479,7 +479,6 @@ class PostsController < ApplicationController render json: json_obj, status: (!!success) ? 200 : 422 end - def find_post_revision_from_params post_id = params[:id] || params[:post_id] revision = params[:revision].to_i @@ -529,9 +528,9 @@ class PostsController < ApplicationController def user_posts(guardian, user_id, opts) posts = Post.includes(:user, :topic, :deleted_by, :user_actions) - .where(user_id: user_id) - .with_deleted - .order(created_at: :desc) + .where(user_id: user_id) + .with_deleted + .order(created_at: :desc) if guardian.user.moderator? @@ -545,7 +544,7 @@ class PostsController < ApplicationController end posts.offset(opts[:offset]) - .limit(opts[:limit]) + .limit(opts[:limit]) end def create_params @@ -622,7 +621,7 @@ class PostsController < ApplicationController "post##" << Digest::SHA1.hexdigest(args .to_a .concat([["user", current_user.id]]) - .sort{|x,y| x[0] <=> y[0]}.join do |x,y| + .sort { |x, y| x[0] <=> y[0] }.join do |x, y| "#{x}:#{y}" end) end diff --git a/app/controllers/queued_posts_controller.rb b/app/controllers/queued_posts_controller.rb index 6b225b5e65..888ef0ddc2 100644 --- a/app/controllers/queued_posts_controller.rb +++ b/app/controllers/queued_posts_controller.rb @@ -41,18 +41,17 @@ class QueuedPostsController < ApplicationController render_serialized(qp, QueuedPostSerializer, root: :queued_posts) end - private def user_deletion_opts base = { - context: I18n.t('queue.delete_reason', {performed_by: current_user.username}), + context: I18n.t('queue.delete_reason', performed_by: current_user.username), delete_posts: true, delete_as_spammer: true } if Rails.env.production? && ENV["Staging"].nil? - base.merge!({block_email: true, block_ip: true}) + base.merge!(block_email: true, block_ip: true) end base diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a88f578093..a07df59f57 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -13,7 +13,10 @@ class SearchController < ApplicationController type_filter: 'topic', guardian: guardian, include_blurbs: true, - blurb_length: 300 + blurb_length: 300, + page: if params[:page].to_i <= 10 + [params[:page].to_i, 1].max + end } context, type = lookup_search_context @@ -22,6 +25,10 @@ class SearchController < ApplicationController search_args[:type_filter] = type if type end + search_args[:search_type] = :full_page + search_args[:ip_address] = request.remote_ip + search_args[:user_id] = current_user.id if current_user.present? + search = Search.new(params[:q], search_args) result = search.execute @@ -37,7 +44,6 @@ class SearchController < ApplicationController render_json_dump(serializer) end end - end def query @@ -49,17 +55,43 @@ class SearchController < ApplicationController search_args[:include_blurbs] = params[:include_blurbs] == "true" if params[:include_blurbs].present? search_args[:search_for_id] = true if params[:search_for_id].present? - context,type = lookup_search_context + context, type = lookup_search_context + if context search_args[:search_context] = context search_args[:type_filter] = type if type end - search = Search.new(params[:term], search_args.symbolize_keys) + search_args[:search_type] = :header + search_args[:ip_address] = request.remote_ip + search_args[:user_id] = current_user.id if current_user.present? + + search = Search.new(params[:term], search_args) result = search.execute render_serialized(result, GroupedSearchResultSerializer, result: result) end + def click + params.require(:search_log_id) + params.require(:search_result_type) + params.require(:search_result_id) + + if params[:search_result_type] == 'topic' + where = { id: params[:search_log_id] } + if current_user.present? + where[:user_id] = current_user.id + else + where[:ip_address] = request.remote_ip + end + + SearchLog.where(where).update_all( + clicked_topic_id: params[:search_result_id] + ) + end + + render json: success_json + end + protected def lookup_search_context @@ -69,7 +101,7 @@ class SearchController < ApplicationController search_context = params[:search_context] unless search_context if (context = params[:context]) && (id = params[:context_id]) - search_context = {type: context, id: id} + search_context = { type: context, id: id } end end @@ -79,7 +111,7 @@ class SearchController < ApplicationController # A user is found by username context_obj = nil - if ['user','private_messages'].include? search_context[:type] + if ['user', 'private_messages'].include? search_context[:type] context_obj = User.find_by(username_lower: search_context[:id].downcase) elsif 'category' == search_context[:type] context_obj = Category.find_by(id: search_context[:id].to_i) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 96259d430d..60d3069151 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -9,7 +9,7 @@ class SessionController < ApplicationController ACTIVATE_USER_KEY = "activate_user" def csrf - render json: {csrf: form_authenticity_token } + render json: { csrf: form_authenticity_token } end def sso @@ -35,7 +35,7 @@ class SessionController < ApplicationController end end - def sso_provider(payload=nil) + def sso_provider(payload = nil) payload ||= request.query_string if SiteSetting.enable_sso_provider sso = SingleSignOn.parse(payload, SiteSetting.sso_secret) @@ -112,7 +112,7 @@ class SessionController < ApplicationController activation = UserActivator.new(user, request, session, cookies) activation.finish session["user_created_message"] = activation.message - redirect_to users_account_created_path and return + redirect_to(users_account_created_path) && (return) else if SiteSetting.verbose_sso_logging Rails.logger.warn("Verbose SSO log: User was logged on #{user.username}\n\n#{sso.diagnostics}") @@ -292,11 +292,11 @@ class SessionController < ApplicationController end def invalid_credentials - render json: {error: I18n.t("login.incorrect_username_email_or_password")} + render json: { error: I18n.t("login.incorrect_username_email_or_password") } end def login_not_approved - render json: {error: I18n.t("login.not_approved")} + render json: { error: I18n.t("login.not_approved") } end def not_activated(user) @@ -310,19 +310,19 @@ class SessionController < ApplicationController end def not_allowed_from_ip_address(user) - render json: {error: I18n.t("login.not_allowed_from_ip_address", username: user.username)} + render json: { error: I18n.t("login.not_allowed_from_ip_address", username: user.username) } end def admin_not_allowed_from_ip_address(user) - render json: {error: I18n.t("login.admin_not_allowed_from_ip_address", username: user.username)} + render json: { error: I18n.t("login.admin_not_allowed_from_ip_address", username: user.username) } end def failed_to_login(user) message = user.suspend_reason ? "login.suspended_with_reason" : "login.suspended" render json: { - error: I18n.t(message, { date: I18n.l(user.suspended_till, format: :date_only), - reason: Rack::Utils.escape_html(user.suspend_reason) }), + error: I18n.t(message, date: I18n.l(user.suspended_till, format: :date_only), + reason: Rack::Utils.escape_html(user.suspend_reason)), reason: 'suspended' } end @@ -337,7 +337,6 @@ class SessionController < ApplicationController render_serialized(user, UserSerializer) end - def render_sso_error(status:, text:) @sso_error = text render status: status, layout: 'no_ember' diff --git a/app/controllers/similar_topics_controller.rb b/app/controllers/similar_topics_controller.rb index 1f0a84d84f..8070697e57 100644 --- a/app/controllers/similar_topics_controller.rb +++ b/app/controllers/similar_topics_controller.rb @@ -26,7 +26,7 @@ class SimilarTopicsController < ApplicationController return render json: [] if invalid_length || !Topic.count_exceeds_minimum? topics = Topic.similar_to(title, raw, current_user).to_a - topics.map! {|t| SimilarTopic.new(t) } + topics.map! { |t| SimilarTopic.new(t) } render_serialized(topics, SimilarTopicSerializer, root: :similar_topics, rest_serializer: true) end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 30c140304e..b524b0dd8e 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -13,9 +13,9 @@ class StaticController < ApplicationController return redirect_to path('/login') if SiteSetting.login_required? && current_user.nil? && (params[:id] == 'faq' || params[:id] == 'guidelines') map = { - "faq" => {redirect: "faq_url", topic_id: "guidelines_topic_id"}, - "tos" => {redirect: "tos_url", topic_id: "tos_topic_id"}, - "privacy" => {redirect: "privacy_policy_url", topic_id: "privacy_topic_id"} + "faq" => { redirect: "faq_url", topic_id: "guidelines_topic_id" }, + "tos" => { redirect: "tos_url", topic_id: "tos_topic_id" }, + "privacy" => { redirect: "privacy_policy_url", topic_id: "privacy_topic_id" } } @page = params[:id] @@ -100,7 +100,7 @@ class StaticController < ApplicationController # a huge expiry, we also cache these assets in nginx so it bypassed if needed def favicon - data = DistributedMemoizer.memoize('favicon' + SiteSetting.favicon_url, 60*30) do + data = DistributedMemoizer.memoize('favicon' + SiteSetting.favicon_url, 60 * 30) do begin file = FileHelper.download( SiteSetting.favicon_url, @@ -137,21 +137,19 @@ class StaticController < ApplicationController end end - def cdn_asset serve_asset end protected - def serve_asset(suffix=nil) + def serve_asset(suffix = nil) path = File.expand_path(Rails.root + "public/assets/#{params[:path]}#{suffix}") # SECURITY what if path has /../ raise Discourse::NotFound unless path.start_with?(Rails.root.to_s + "/public/assets") - response.headers["Expires"] = 1.year.from_now.httpdate response.headers["Access-Control-Allow-Origin"] = params[:origin] if params[:origin] diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 005d2d7d78..c80bdffd2f 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -20,7 +20,7 @@ class StepsController < ApplicationController else errors = [] updater.errors.messages.each do |field, msg| - errors << {field: field, description: msg.join } + errors << { field: field, description: msg.join } end render json: { errors: errors }, status: 422 end diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb index dea3d5d34c..d174f28faa 100644 --- a/app/controllers/stylesheets_controller.rb +++ b/app/controllers/stylesheets_controller.rb @@ -19,17 +19,17 @@ class StylesheetsController < ApplicationController no_cookies - target,digest = params[:name].split(/_([a-f0-9]{40})/) + target, digest = params[:name].split(/_([a-f0-9]{40})/) - if Rails.env == "development" + if !Rails.env.production? # TODO add theme # calling this method ensures we have a cache for said target # we hold of re-compilation till someone asks for asset if target.include?("theme") - split_target,theme_id = target.split(/_(-?[0-9]+)/) + split_target, theme_id = target.split(/_(-?[0-9]+)/) theme = Theme.find(theme_id) if theme_id else - split_target,color_scheme_id = target.split(/_(-?[0-9]+)/) + split_target, color_scheme_id = target.split(/_(-?[0-9]+)/) theme = Theme.find_by(color_scheme_id: color_scheme_id) end Stylesheet::Manager.stylesheet_link_tag(split_target, nil, theme&.key) @@ -59,7 +59,6 @@ class StylesheetsController < ApplicationController return render nothing: true, status: 304 end - unless File.exist?(location) if current = query.limit(1).pluck(source_map ? :source_map : :content).first File.write(location, current) @@ -90,4 +89,3 @@ class StylesheetsController < ApplicationController end end - diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb index 783839fd4d..d29d3ba8d7 100644 --- a/app/controllers/tag_groups_controller.rb +++ b/app/controllers/tag_groups_controller.rb @@ -69,7 +69,7 @@ class TagGroupsController < ApplicationController end def tag_groups_params - result = params.permit(:id, :name, :one_per_topic, :tag_names => [], :parent_tag_name => []) + result = params.permit(:id, :name, :one_per_topic, tag_names: [], parent_tag_name: []) result[:tag_names] ||= [] result[:parent_tag_name] ||= [] result[:one_per_topic] = (params[:one_per_topic] == "true") diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 0af8be70a6..e65e849425 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -14,18 +14,18 @@ class TagsController < ::ApplicationController :tag_feed, :search, :check_hashtag, - Discourse.anonymous_filters.map { |f| :"show_#{f}"} + Discourse.anonymous_filters.map { |f| :"show_#{f}" } ].flatten before_filter :set_category_from_params, except: [:index, :update, :destroy, :tag_feed, :search, :notifications, :update_notifications] def index categories = Category.where("id in (select category_id from category_tags)") - .where("id in (?)", guardian.allowed_category_ids) - .preload(:tags) + .where("id in (?)", guardian.allowed_category_ids) + .preload(:tags) category_tag_counts = categories.map do |c| h = Tag.category_tags_by_count_query(c, limit: 300).count(Tag::COUNT_ARG) - h.merge!(c.tags.where.not(name: h.keys).inject({}) { |sum,t| sum[t.name] = 0; sum }) # unused tags - {id: c.id, tags: self.class.tag_counts_json(h)} + h.merge!(c.tags.where.not(name: h.keys).inject({}) { |sum, t| sum[t.name] = 0; sum }) # unused tags + { id: c.id, tags: self.class.tag_counts_json(h) } end tag_counts = self.class.tags_by_count(guardian, limit: 300).count(Tag::COUNT_ARG) @@ -49,8 +49,8 @@ class TagsController < ::ApplicationController Discourse.filters.each do |filter| define_method("show_#{filter}") do - @tag_id = params[:tag_id] - @additional_tags = params[:additional_tag_ids].to_s.split('/') + @tag_id = params[:tag_id].force_encoding("UTF-8") + @additional_tags = params[:additional_tag_ids].to_s.split('/').map { |t| t.force_encoding("UTF-8") } list_opts = build_topic_list_options @@ -67,7 +67,7 @@ class TagsController < ::ApplicationController @title = @description_meta path_name = url_method(params.slice(:category, :parent_category)) - canonical_url "#{Discourse.base_url_no_prefix}#{public_send(path_name, *(params.slice(:parent_category, :category, :tag_id).values))}" + canonical_url "#{Discourse.base_url_no_prefix}#{public_send(path_name, *(params.slice(:parent_category, :category, :tag_id).values.map { |t| t.force_encoding("UTF-8") }))}" if @list.topics.size == 0 && params[:tag_id] != 'none' && !Tag.where(name: @tag_id).exists? permalink_redirect_or_not_found @@ -91,7 +91,7 @@ class TagsController < ::ApplicationController tag.name = new_tag_name if tag.save StaffActionLogger.new(current_user).log_custom('renamed_tag', previous_value: params[:tag_id], new_value: new_tag_name) - render json: { tag: { id: new_tag_name }} + render json: { tag: { id: new_tag_name } } else render_json_error tag.errors.full_messages end @@ -116,7 +116,7 @@ class TagsController < ::ApplicationController @title = "#{SiteSetting.title} - #{@description}" @atom_link = "#{Discourse.base_url}/tags/#{tag_id}.rss" - query = TopicQuery.new(current_user, {tags: [tag_id]}) + query = TopicQuery.new(current_user, tags: [tag_id]) latest_results = query.latest_results @topic_list = query.create_list(:by_tag, {}, latest_results) @@ -129,15 +129,13 @@ class TagsController < ::ApplicationController tags_with_counts = DiscourseTagging.filter_allowed_tags( Tag.tags_by_count_query(params.slice(:limit)), guardian, - { - for_input: params[:filterForInput], - term: params[:q], - category: category, - selected_tags: params[:selected_tags] - } + for_input: params[:filterForInput], + term: params[:q], + category: category, + selected_tags: params[:selected_tags] ) - tags = tags_with_counts.count(Tag::COUNT_ARG).map {|t, c| { id: t, text: t, count: c } } + tags = tags_with_counts.count(Tag::COUNT_ARG).map { |t, c| { id: t, text: t, count: c } } json_response = { results: tags } @@ -153,7 +151,7 @@ class TagsController < ::ApplicationController tag = Tag.find_by_name(params[:tag_id]) raise Discourse::NotFound unless tag level = tag.tag_users.where(user: current_user).first.try(:notification_level) || TagUser.notification_levels[:regular] - render json: { tag_notification: { id: params[:tag_id], notification_level: level.to_i } } + render json: { tag_notification: { id: tag.name, notification_level: level.to_i } } end def update_notifications @@ -161,7 +159,7 @@ class TagsController < ::ApplicationController raise Discourse::NotFound unless tag level = params[:tag_notification][:notification_level].to_i TagUser.change(current_user.id, tag.id, level) - render json: {notification_level: level} + render json: { notification_level: level } end def check_hashtag @@ -180,12 +178,12 @@ class TagsController < ::ApplicationController raise Discourse::NotFound unless SiteSetting.tagging_enabled? end - def self.tags_by_count(guardian, opts={}) + def self.tags_by_count(guardian, opts = {}) guardian.filter_allowed_categories(Tag.tags_by_count_query(opts)) end def self.tag_counts_json(tag_counts) - tag_counts.map {|t, c| { id: t, text: t, count: c } } + tag_counts.map { |t, c| { id: t, text: t, count: c } } end def set_category_from_params @@ -201,13 +199,13 @@ class TagsController < ::ApplicationController parent_category_id = nil if parent_slug_or_id.present? parent_category_id = Category.query_parent_category(parent_slug_or_id) - category_redirect_or_not_found and return if parent_category_id.blank? + category_redirect_or_not_found && (return) if parent_category_id.blank? end @filter_on_category = Category.query_category(slug_or_id, parent_category_id) end - category_redirect_or_not_found and return if !@filter_on_category + category_redirect_or_not_found && (return) if !@filter_on_category guardian.ensure_can_see!(@filter_on_category) end @@ -236,7 +234,7 @@ class TagsController < ::ApplicationController end end - def url_method(opts={}) + def url_method(opts = {}) if opts[:parent_category] && opts[:category] "tag_parent_category_category_#{action_name}_path" elsif opts[:category] @@ -254,7 +252,7 @@ class TagsController < ::ApplicationController else # :next public_send(method, opts.merge(next_page_params(opts))) end - url.sub('.json?','?') + url.sub('.json?', '?') end def build_topic_list_options diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index c593060603..27279d2dc3 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -40,7 +40,7 @@ class TopicsController < ApplicationController topic = Topic.find_by(slug: params[:slug].downcase) guardian.ensure_can_see!(topic) raise Discourse::NotFound unless topic - render json: {slug: topic.slug, topic_id: topic.id, url: topic.url} + render json: { slug: topic.slug, topic_id: topic.id, url: topic.url } end def show @@ -155,7 +155,6 @@ class TopicsController < ApplicationController TopicUser.change(current_user.id, params[:topic_id].to_i, notification_level: TopicUser.notification_levels[:muted]) end - perform_show_response end @@ -164,7 +163,8 @@ class TopicsController < ApplicationController params.require(:topic_id) params.permit(:min_trust_level, :min_score, :min_replies, :bypass_trust_level_score, :only_moderator_liked) - opts = { best: params[:best].to_i, + opts = { + best: params[:best].to_i, min_trust_level: params[:min_trust_level] ? params[:min_trust_level].to_i : 1, min_score: params[:min_score].to_i, min_replies: params[:min_replies].to_i, @@ -206,17 +206,16 @@ class TopicsController < ApplicationController guardian.ensure_can_see!(@topic) @posts = Post.where(hidden: false, deleted_at: nil, topic_id: @topic.id) - .where('posts.id in (?)', post_ids) - .joins("LEFT JOIN users u on u.id = posts.user_id") - .pluck(:id, :cooked, :username) - .map do |post_id, cooked, username| - { - post_id: post_id, - username: username, - excerpt: PrettyText.excerpt(cooked, 800, keep_emoji_images: true) - } - end - + .where('posts.id in (?)', post_ids) + .joins("LEFT JOIN users u on u.id = posts.user_id") + .pluck(:id, :cooked, :username) + .map do |post_id, cooked, username| + { + post_id: post_id, + username: username, + excerpt: PrettyText.excerpt(cooked, 800, keep_emoji_images: true) + } + end render json: @posts.to_json end @@ -266,7 +265,7 @@ class TopicsController < ApplicationController params.require(:enabled) params.permit(:until) - status = params[:status] + status = params[:status] topic_id = params[:topic_id].to_i enabled = params[:enabled] == 'true' @@ -319,13 +318,13 @@ class TopicsController < ApplicationController ) if topic.save - render json: success_json.merge!({ + render json: success_json.merge!( execute_at: topic_status_update&.execute_at, duration: topic_status_update&.duration, based_on_last_post: topic_status_update&.based_on_last_post, closed: topic.closed, category_id: topic_status_update&.category_id - }) + ) else render_json_error(topic) end @@ -353,8 +352,8 @@ class TopicsController < ApplicationController topic = Topic.find(params[:topic_id].to_i) PostAction.joins(:post) - .where(user_id: current_user.id) - .where('topic_id = ?', topic.id).each do |pa| + .where(user_id: current_user.id) + .where('topic_id = ?', topic.id).each do |pa| PostAction.remove_act(current_user, pa.post, PostActionType.types[:bookmark]) end @@ -378,7 +377,7 @@ class TopicsController < ApplicationController group_ids = current_user.groups.pluck(:id) if group_ids.present? allowed_groups = topic.allowed_groups - .where('topic_allowed_groups.group_id IN (?)', group_ids).pluck(:id) + .where('topic_allowed_groups.group_id IN (?)', group_ids).pluck(:id) allowed_groups.each do |id| if archive GroupArchivedMessage.archive!(id, topic.id) @@ -421,7 +420,7 @@ class TopicsController < ApplicationController guardian.ensure_can_delete!(topic) first_post = topic.ordered_posts.first - PostDestroyer.new(current_user, first_post, { context: params[:context] }).destroy + PostDestroyer.new(current_user, first_post, context: params[:context]).destroy render nothing: true end @@ -484,8 +483,13 @@ class TopicsController < ApplicationController topic = Topic.find_by(id: params[:topic_id]) - group_ids = Group.lookup_group_ids(params) - guardian.ensure_can_invite_to!(topic,group_ids) + groups = Group.lookup_groups( + group_ids: params[:group_ids], + group_names: params[:group_names] + ) + + guardian.ensure_can_invite_to!(topic, groups) + group_ids = groups.map(&:id) begin if topic.invite(current_user, username_or_email, group_ids, params[:custom_message]) @@ -499,7 +503,7 @@ class TopicsController < ApplicationController render json: failed_json, status: 422 end rescue Topic::UserExists => e - render json: {errors: [e.message]}, status: 422 + render json: { errors: [e.message] }, status: 422 end end @@ -541,10 +545,10 @@ class TopicsController < ApplicationController guardian.ensure_can_change_post_owner! begin - PostOwnerChanger.new( post_ids: params[:post_ids].to_a, - topic_id: params[:topic_id].to_i, - new_owner: User.find_by(username: params[:username]), - acting_user: current_user ).change_owner! + PostOwnerChanger.new(post_ids: params[:post_ids].to_a, + topic_id: params[:topic_id].to_i, + new_owner: User.find_by(username: params[:username]), + acting_user: current_user).change_owner! render json: success_json rescue ArgumentError render json: failed_json, status: 422 @@ -588,8 +592,8 @@ class TopicsController < ApplicationController current_user, params[:topic_id].to_i, params[:topic_time].to_i, - (params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]}, - {mobile: view_context.mobile_view?} + (params[:timings] || []).map { |post_number, t| [post_number.to_i, t.to_i] }, + mobile: view_context.mobile_view? ) render nothing: true end @@ -602,7 +606,7 @@ class TopicsController < ApplicationController def bulk if params[:topic_ids].present? - topic_ids = params[:topic_ids].map {|t| t.to_i} + topic_ids = params[:topic_ids].map { |t| t.to_i } elsif params[:filter] == 'unread' tq = TopicQuery.new(current_user) topics = TopicQuery.unread_filter(tq.joined_topic_user, current_user.id, staff: guardian.is_staff?).listable_topics @@ -658,7 +662,7 @@ class TopicsController < ApplicationController params[:slug] && @topic_view.topic.slug != params[:slug] end - def redirect_to_correct_topic(topic, post_number=nil) + def redirect_to_correct_topic(topic, post_number = nil) url = topic.relative_url url << "/#{post_number}" if post_number.to_i > 0 url << ".json" if request.format.json? @@ -722,9 +726,9 @@ class TopicsController < ApplicationController def render_topic_changes(dest_topic) if dest_topic.present? - render json: {success: true, url: dest_topic.relative_url} + render json: { success: true, url: dest_topic.relative_url } else - render json: {success: false} + render json: { success: false } end end diff --git a/app/controllers/user_api_keys_controller.rb b/app/controllers/user_api_keys_controller.rb index 8d11d46c1e..6fff6655c8 100644 --- a/app/controllers/user_api_keys_controller.rb +++ b/app/controllers/user_api_keys_controller.rb @@ -40,7 +40,7 @@ class UserApiKeysController < ApplicationController @client_id = params[:client_id] @auth_redirect = params[:auth_redirect] @push_url = params[:push_url] - @localized_scopes = params[:scopes].split(",").map{|s| I18n.t("user_api_key.scopes.#{s}")} + @localized_scopes = params[:scopes].split(",").map { |s| I18n.t("user_api_key.scopes.#{s}") } @scopes = params[:scopes] rescue Discourse::InvalidAccess @@ -52,10 +52,10 @@ class UserApiKeysController < ApplicationController require_params unless SiteSetting.allowed_user_api_auth_redirects - .split('|') - .any?{|u| params[:auth_redirect] == u} + .split('|') + .any? { |u| params[:auth_redirect] == u } - raise Discourse::InvalidAccess + raise Discourse::InvalidAccess end raise Discourse::InvalidAccess unless meets_tl? @@ -126,7 +126,7 @@ class UserApiKeysController < ApplicationController :client_id, :auth_redirect, :application_name - ].each{|p| params.require(p)} + ].each { |p| params.require(p) } end def validate_params diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index 30a3709c60..119266fd35 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -89,7 +89,7 @@ class UserAvatarsController < ApplicationController return render_blank if size < 8 || size > 1000 if !Discourse.avatar_sizes.include?(size) && Discourse.store.external? - closest = Discourse.avatar_sizes.to_a.min { |a,b| (size-a).abs <=> (size-b).abs } + closest = Discourse.avatar_sizes.to_a.min { |a, b| (size - a).abs <=> (size - b).abs } avatar_url = UserAvatar.local_avatar_url(hostname, user.username_lower, upload_id, closest) return redirect_to cdn_path(avatar_url) end diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index 7c9cfb0bee..d8f44a41ea 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -33,12 +33,12 @@ class UserBadgesController < ApplicationController if params[:grouped] user_badges = user_badges.group(:badge_id) - .select(UserBadge.attribute_names.map {|x| "MAX(#{x}) AS #{x}" }, 'COUNT(*) AS "count"') + .select(UserBadge.attribute_names.map { |x| "MAX(#{x}) AS #{x}" }, 'COUNT(*) AS "count"') end user_badges = user_badges.includes(badge: [:badge_grouping, :badge_type]) - .includes(post: :topic) - .includes(:granted_by) + .includes(post: :topic) + .includes(:granted_by) render_serialized(user_badges, DetailedUserBadgeSerializer, root: :user_badges) end @@ -104,6 +104,6 @@ class UserBadgesController < ApplicationController def can_assign_badge_to_user?(user) master_api_call = current_user.nil? && is_api? - master_api_call or guardian.can_grant_badges?(user) + master_api_call || guardian.can_grant_badges?(user) end end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 6b0409e2a4..4908b527bb 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -36,7 +36,7 @@ class Users::OmniauthCallbacksController < ApplicationController auth[:session] = session authenticator = self.class.find_authenticator(params[:provider]) - provider = Discourse.auth_providers && Discourse.auth_providers.find{|p| p.name == params[:provider]} + provider = Discourse.auth_providers && Discourse.auth_providers.find { |p| p.name == params[:provider] } @auth_result = authenticator.after_authenticate(auth) @@ -83,7 +83,6 @@ class Users::OmniauthCallbacksController < ApplicationController render layout: 'no_ember' end - def self.find_authenticator(name) BUILTIN_AUTH.each do |authenticator| if authenticator.name == name diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index fb510fa4eb..6c416f4d93 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -50,7 +50,7 @@ class UsersController < ApplicationController topic_id = params[:include_post_count_for].to_i if topic_id != 0 - user_serializer.topic_post_count = {topic_id => Post.where(topic_id: topic_id, user_id: @user.id).count } + user_serializer.topic_post_count = { topic_id => Post.where(topic_id: topic_id, user_id: @user.id).count } end if !params[:skip_track_visit] && (@user != current_user) @@ -59,7 +59,7 @@ class UsersController < ApplicationController # This is a hack to get around a Rails issue where values with periods aren't handled correctly # when used as part of a route. - if params[:external_id] and params[:external_id].ends_with? '.json' + if params[:external_id] && params[:external_id].ends_with?('.json') return render_json_dump(user_serializer) end @@ -224,8 +224,8 @@ class UsersController < ApplicationController pending_count = Invite.find_pending_invites_count(inviter) redeemed_count = Invite.find_redeemed_invites_count(inviter) - render json: {counts: { pending: pending_count, redeemed: redeemed_count, - total: (pending_count.to_i + redeemed_count.to_i) } } + render json: { counts: { pending: pending_count, redeemed: redeemed_count, + total: (pending_count.to_i + redeemed_count.to_i) } } end def is_local_username @@ -238,7 +238,7 @@ class UsersController < ApplicationController Group.mentionable(current_user) .where(name: usernames) .pluck(:name, :user_count) - .map{ |name,user_count| {name: name, user_count: user_count} } + .map { |name, user_count| { name: name, user_count: user_count } } end usernames -= groups @@ -250,14 +250,14 @@ class UsersController < ApplicationController topic_id = params[:topic_id] unless topic_id.blank? topic = Topic.find_by(id: topic_id) - usernames.each{ |username| cannot_see.push(username) unless Guardian.new(User.find_by_username(username)).can_see?(topic) } + usernames.each { |username| cannot_see.push(username) unless Guardian.new(User.find_by_username(username)).can_see?(topic) } end result = User.where(staged: false) - .where(username_lower: usernames) - .pluck(:username_lower) + .where(username_lower: usernames) + .pluck(:username_lower) - render json: {valid: result, valid_groups: groups, mentionable_groups: mentionable_groups, cannot_see: cannot_see} + render json: { valid: result, valid_groups: groups, mentionable_groups: mentionable_groups, cannot_see: cannot_see } end def render_available_true @@ -265,7 +265,7 @@ class UsersController < ApplicationController end def changing_case_of_own_username(target_user, username) - target_user and username.downcase == target_user.username.downcase + target_user && username.downcase == (target_user.username.downcase) end # Used for checking availability of a username and will return suggestions @@ -311,7 +311,7 @@ class UsersController < ApplicationController return fail_with("login.reserved_username") end - if user = User.find_by(staged: true, email: params[:email].strip.downcase) + if user = User.where(staged: true).with_email(params[:email].strip.downcase).first user_params.each { |k, v| user.send("#{k}=", v) } user.staged = false else @@ -385,7 +385,7 @@ class UsersController < ApplicationController end def get_honeypot_value - render json: {value: honeypot_value, challenge: challenge_value} + render json: { value: honeypot_value, challenge: challenge_value } end def password_reset @@ -433,7 +433,7 @@ class UsersController < ApplicationController if @error render layout: 'no_ember' else - store_preloaded("password_reset", MultiJson.dump({ is_developer: UsernameCheckerService.is_developer?(@user.email) })) + store_preloaded("password_reset", MultiJson.dump(is_developer: UsernameCheckerService.is_developer?(@user.email))) end return redirect_to(wizard_path) if request.put? && Wizard.user_requires_completion?(@user) end @@ -456,7 +456,7 @@ class UsersController < ApplicationController } end else - render json: {is_developer: UsernameCheckerService.is_developer?(@user.email)} + render json: { is_developer: UsernameCheckerService.is_developer?(@user.email) } end end end @@ -469,14 +469,15 @@ class UsersController < ApplicationController end def logon_after_password_reset - message = if Guardian.new(@user).can_access_forum? - # Log in the user - log_on_user(@user) - 'password_reset.success' - else - @requires_approval = true - 'password_reset.success_unapproved' - end + message = + if Guardian.new(@user).can_access_forum? + # Log in the user + log_on_user(@user) + 'password_reset.success' + else + @requires_approval = true + 'password_reset.success_unapproved' + end @success = I18n.t(message) end @@ -490,7 +491,7 @@ class UsersController < ApplicationController RateLimiter.new(nil, "admin-login-hr-#{request.remote_ip}", 6, 1.hour).performed! RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed! - user = User.where(email: params[:email], admin: true).human_users.first + user = User.with_email(params[:email]).where(admin: true).human_users.first if user email_token = user.email_tokens.create(email: user.email) Jobs.enqueue(:critical_user_email, type: :admin_login, user_id: user.id, email_token: email_token.token) @@ -606,6 +607,7 @@ class UsersController < ApplicationController User.transaction do @user.email = params[:email] + if @user.save @user.email_tokens.create(email: @user.email) enqueue_activation_email @@ -648,7 +650,7 @@ class UsersController < ApplicationController def enqueue_activation_email @email_token ||= @user.email_tokens.create(email: @user.email) - Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token) + Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token, to_address: @user.email) end def search_users @@ -661,7 +663,6 @@ class UsersController < ApplicationController @group = Group.find_by(name: params[:group]) end - results = UserSearch.new(term, topic_id: topic_id, topic_allowed_users: topic_allowed_users, @@ -682,8 +683,8 @@ class UsersController < ApplicationController if params[:include_mentionable_groups] == "true" && current_user to_render[:groups] = Group.mentionable(current_user) - .where("name ILIKE :term_like", term_like: "#{term}%") - .map do |m| + .where("name ILIKE :term_like", term_like: "#{term}%") + .map do |m| { name: m.name, full_name: m.full_name } end end @@ -751,7 +752,7 @@ class UsersController < ApplicationController @user = fetch_user_from_params guardian.ensure_can_delete_user!(@user) - UserDestroyer.new(current_user).destroy(@user, { delete_posts: true, context: params[:context] }) + UserDestroyer.new(current_user).destroy(@user, delete_posts: true, context: params[:context]) render json: success_json end @@ -796,14 +797,14 @@ class UsersController < ApplicationController private def honeypot_value - Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{GlobalSetting.safe_secret_key_base}")[0,15] + Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{GlobalSetting.safe_secret_key_base}")[0, 15] end def challenge_value challenge = $redis.get('SECRET_CHALLENGE') - unless challenge && challenge.length == 16*2 + unless challenge && challenge.length == 16 * 2 challenge = SecureRandom.hex(16) - $redis.set('SECRET_CHALLENGE',challenge) + $redis.set('SECRET_CHALLENGE', challenge) end challenge @@ -832,9 +833,9 @@ class UsersController < ApplicationController def user_params result = params.permit(:name, :email, :password, :username, :date_of_birth) - .merge(ip_address: request.remote_ip, - registration_ip_address: request.remote_ip, - locale: user_locale) + .merge(ip_address: request.remote_ip, + registration_ip_address: request.remote_ip, + locale: user_locale) if !UsernameCheckerService.is_developer?(result['email']) && is_api? && @@ -844,7 +845,6 @@ class UsersController < ApplicationController result.merge!(params.permit(:active, :staged)) end - result end diff --git a/app/controllers/wizard_controller.rb b/app/controllers/wizard_controller.rb index 4b7bc82faa..88edb7af8d 100644 --- a/app/controllers/wizard_controller.rb +++ b/app/controllers/wizard_controller.rb @@ -3,8 +3,8 @@ require_dependency 'wizard/builder' class WizardController < ApplicationController before_filter :ensure_wizard_enabled, only: [:index] - before_filter :ensure_logged_in - before_filter :ensure_admin + before_filter :ensure_logged_in, except: [:qunit] + before_filter :ensure_admin, except: [:qunit] skip_before_filter :check_xhr, :preload_json @@ -21,6 +21,7 @@ class WizardController < ApplicationController end def qunit + raise Discourse::InvalidAccess.new if Rails.env.production? end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4a4ab33218..1d837bbf0b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,7 +15,7 @@ module ApplicationHelper include ConfigurableUrls include GlobalPath - def google_universal_analytics_json(ua_domain_name=nil) + def google_universal_analytics_json(ua_domain_name = nil) result = {} if ua_domain_name result[:cookieDomain] = ua_domain_name.gsub(/^http(s)?:\/\//, '') @@ -23,6 +23,9 @@ module ApplicationHelper if current_user.present? result[:userId] = current_user.id end + if SiteSetting.ga_universal_auto_link_domains.present? + result[:allowLinker] = true + end result.to_json.html_safe end @@ -52,7 +55,7 @@ module ApplicationHelper GlobalSetting.cdn_url.start_with?("https") && ENV["COMPRESS_BROTLI"] == "1" && request.env["HTTP_ACCEPT_ENCODING"] =~ /br/ - path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") + path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") end " ".html_safe @@ -155,7 +158,7 @@ module ApplicationHelper end # Creates open graph and twitter card meta data - def crawlable_meta_data(opts=nil) + def crawlable_meta_data(opts = nil) opts ||= {} opts[:url] ||= "#{Discourse.base_url_no_prefix}#{request.fullpath}" @@ -227,9 +230,7 @@ module ApplicationHelper end def gsub_emoji_to_unicode(str) - if str - str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } - end + Emoji.gsub_emoji_to_unicode(str) end def application_logo_url @@ -241,7 +242,7 @@ module ApplicationHelper end def mobile_view? - MobileDetection.resolve_mobile_view!(request.user_agent,params,session) + MobileDetection.resolve_mobile_view!(request.user_agent, params, session) end def crawler_layout? @@ -282,7 +283,7 @@ module ApplicationHelper controller.class.name.split("::").first == "Admin" end - def category_badge(category, opts=nil) + def category_badge(category, opts = nil) CategoryBadge.html_for(category, opts).html_safe end @@ -296,11 +297,11 @@ module ApplicationHelper return "" if Rails.env.test? matcher = Regexp.new("/connectors/#{name}/.*\.html\.erb$") - erbs = ApplicationHelper.all_connectors.select {|c| c =~ matcher } + erbs = ApplicationHelper.all_connectors.select { |c| c =~ matcher } return "" if erbs.blank? result = "" - erbs.each {|erb| result << render(file: erb) } + erbs.each { |erb| result << render(file: erb) } result.html_safe end @@ -334,7 +335,7 @@ module ApplicationHelper lookup.html_safe if lookup end - def discourse_stylesheet_link_tag(name, opts={}) + def discourse_stylesheet_link_tag(name, opts = {}) if opts.key?(:theme_key) key = opts[:theme_key] unless customization_disabled? else diff --git a/app/helpers/list_helper.rb b/app/helpers/list_helper.rb index a7cf7766ac..9839401f9e 100644 --- a/app/helpers/list_helper.rb +++ b/app/helpers/list_helper.rb @@ -6,7 +6,7 @@ module ListHelper return if total_pages < 2 - page = [total_pages - (max_pages+1), 2].max + page = [total_pages - (max_pages + 1), 2].max result = "(" while page <= total_pages diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index ad96e94177..b50b923bb8 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -2,7 +2,7 @@ module TopicsHelper include ApplicationHelper def render_topic_title(topic) - link_to(gsub_emoji_to_unicode(topic.title),topic.relative_url) + link_to(Emoji.gsub_emoji_to_unicode(topic.title), topic.relative_url) end def categories_breadcrumb(topic) diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb index 044b4eb312..2e18c702ee 100644 --- a/app/helpers/user_notifications_helper.rb +++ b/app/helpers/user_notifications_helper.rb @@ -1,6 +1,6 @@ module UserNotificationsHelper - def indent(text, by=2) + def indent(text, by = 2) spacer = " " * by result = "" text.each_line do |line| @@ -48,9 +48,9 @@ module UserNotificationsHelper doc.css('div').first end - def email_excerpt(html_arg, posts_count=nil) + def email_excerpt(html_arg, posts_count = nil) # only include 1st paragraph when more than 1 posts - html = (posts_count.nil? || posts_count > 1) ? (first_paragraph_from(html_arg)||html_arg).to_s : html_arg + html = (posts_count.nil? || posts_count > 1) ? (first_paragraph_from(html_arg) || html_arg).to_s : html_arg PrettyText.format_for_email(html).html_safe end diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 4445c78aa9..9ce3b6e076 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -71,11 +71,11 @@ module Jobs ctx end - def self.delayed_perform(opts={}) + def self.delayed_perform(opts = {}) self.new.perform(opts) end - def execute(opts={}) + def execute(opts = {}) raise "Overwrite me!" end @@ -114,7 +114,6 @@ module Jobs end end - dbs = if opts[:current_site_id] [opts[:current_site_id]] @@ -203,7 +202,7 @@ module Jobs end end - def self.enqueue(job_name, opts={}) + def self.enqueue(job_name, opts = {}) klass = "Jobs::#{job_name.to_s.camelcase}".constantize # Unless we want to work on all sites @@ -227,20 +226,20 @@ module Jobs end - def self.enqueue_in(secs, job_name, opts={}) + def self.enqueue_in(secs, job_name, opts = {}) enqueue(job_name, opts.merge!(delay_for: secs)) end - def self.enqueue_at(datetime, job_name, opts={}) + def self.enqueue_at(datetime, job_name, opts = {}) secs = [(datetime - Time.zone.now).to_i, 0].max enqueue_in(secs, job_name, opts) end - def self.cancel_scheduled_job(job_name, opts={}) + def self.cancel_scheduled_job(job_name, opts = {}) scheduled_for(job_name, opts).each(&:delete) end - def self.scheduled_for(job_name, opts={}) + def self.scheduled_for(job_name, opts = {}) opts = opts.with_indifferent_access unless opts.delete(:all_sites) opts[:current_site_id] ||= RailsMultisite::ConnectionManagement.current_db @@ -265,6 +264,6 @@ module Jobs end end -Dir["#{Rails.root}/app/jobs/onceoff/*.rb"].each {|file| require_dependency file } -Dir["#{Rails.root}/app/jobs/regular/*.rb"].each {|file| require_dependency file } -Dir["#{Rails.root}/app/jobs/scheduled/*.rb"].each {|file| require_dependency file } +Dir["#{Rails.root}/app/jobs/onceoff/*.rb"].each { |file| require_dependency file } +Dir["#{Rails.root}/app/jobs/regular/*.rb"].each { |file| require_dependency file } +Dir["#{Rails.root}/app/jobs/scheduled/*.rb"].each { |file| require_dependency file } diff --git a/app/jobs/onceoff/grant_emoji.rb b/app/jobs/onceoff/grant_emoji.rb index 696fd1f952..39d9ececf8 100644 --- a/app/jobs/onceoff/grant_emoji.rb +++ b/app/jobs/onceoff/grant_emoji.rb @@ -6,11 +6,11 @@ module Jobs to_award = {} Post.secured(Guardian.new) - .select(:id, :created_at, :cooked, :user_id) - .visible - .public_posts - .where("cooked LIKE '%emoji%'") - .find_in_batches do |group| + .select(:id, :created_at, :cooked, :user_id) + .visible + .public_posts + .where("cooked LIKE '%emoji%'") + .find_in_batches do |group| group.each do |p| doc = Nokogiri::HTML::fragment(p.cooked) if (doc.css("img.emoji") - doc.css(".quote img")).size > 0 diff --git a/app/jobs/onceoff/grant_first_reply_by_email.rb b/app/jobs/onceoff/grant_first_reply_by_email.rb index ae769f681c..ef8bcc6469 100644 --- a/app/jobs/onceoff/grant_first_reply_by_email.rb +++ b/app/jobs/onceoff/grant_first_reply_by_email.rb @@ -7,12 +7,12 @@ module Jobs to_award = {} Post.select(:id, :created_at, :user_id) - .secured(Guardian.new) - .visible - .public_posts - .where(via_email: true) - .where("post_number > 1") - .find_in_batches do |group| + .secured(Guardian.new) + .visible + .public_posts + .where(via_email: true) + .where("post_number > 1") + .find_in_batches do |group| group.each do |p| to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } end diff --git a/app/jobs/onceoff/grant_onebox.rb b/app/jobs/onceoff/grant_onebox.rb index 0c63e6b7b8..259dfe9702 100644 --- a/app/jobs/onceoff/grant_onebox.rb +++ b/app/jobs/onceoff/grant_onebox.rb @@ -8,11 +8,11 @@ module Jobs to_award = {} Post.secured(Guardian.new) - .select(:id, :created_at, :raw, :user_id) - .visible - .public_posts - .where("raw LIKE '%http%'") - .find_in_batches do |group| + .select(:id, :created_at, :raw, :user_id) + .visible + .public_posts + .where("raw LIKE '%http%'") + .find_in_batches do |group| group.each do |p| begin # Note we can't use `p.cooked` here because oneboxes have been cooked out diff --git a/app/jobs/onceoff/migrate_censored_words.rb b/app/jobs/onceoff/migrate_censored_words.rb new file mode 100644 index 0000000000..03ce4b358c --- /dev/null +++ b/app/jobs/onceoff/migrate_censored_words.rb @@ -0,0 +1,12 @@ +module Jobs + class MigrateCensoredWords < Jobs::Onceoff + def execute_onceoff(args) + row = WatchedWord.exec_sql("SELECT value FROM site_settings WHERE name = 'censored_words'") + if row.count > 0 + row.first["value"].split('|').each do |word| + WatchedWord.create(word: word, action: WatchedWord.actions[:censor]) + end + end + end + end +end diff --git a/app/jobs/onceoff/migrate_tagging_plugin.rb b/app/jobs/onceoff/migrate_tagging_plugin.rb index f0bb91e7e4..9d94b8e88f 100644 --- a/app/jobs/onceoff/migrate_tagging_plugin.rb +++ b/app/jobs/onceoff/migrate_tagging_plugin.rb @@ -4,7 +4,7 @@ module Jobs def execute_onceoff(args) all_tags = TopicCustomField.where(name: "tags").select('DISTINCT value').all.map(&:value) - tag_id_lookup = Tag.create(all_tags.map { |tag_name| {name: tag_name} }).inject({}) { |h,v| h[v.name] = v.id; h } + tag_id_lookup = Tag.create(all_tags.map { |tag_name| { name: tag_name } }).inject({}) { |h, v| h[v.name] = v.id; h } TopicCustomField.where(name: "tags").find_each do |tcf| TopicTag.create(topic_id: tcf.topic_id, tag_id: tag_id_lookup[tcf.value] || Tag.find_by_name(tcf.value).try(:id)) diff --git a/app/jobs/onceoff/migrate_upload_extensions.rb b/app/jobs/onceoff/migrate_upload_extensions.rb new file mode 100644 index 0000000000..38bcd3a1ea --- /dev/null +++ b/app/jobs/onceoff/migrate_upload_extensions.rb @@ -0,0 +1,11 @@ +module Jobs + + class MigrateUploadExtensions < Jobs::Onceoff + def execute_onceoff(args) + Upload.find_each do |upload| + upload.extension = File.extname(upload.original_filename)[1..10] + upload.save + end + end + end +end diff --git a/app/jobs/onceoff/retro_grant_anniversary.rb b/app/jobs/onceoff/retro_grant_anniversary.rb index 2b338e6dcd..9cbdddd810 100644 --- a/app/jobs/onceoff/retro_grant_anniversary.rb +++ b/app/jobs/onceoff/retro_grant_anniversary.rb @@ -14,4 +14,3 @@ module Jobs end end - diff --git a/app/jobs/regular/automatic_group_membership.rb b/app/jobs/regular/automatic_group_membership.rb index 91a417927a..4e688d8400 100644 --- a/app/jobs/regular/automatic_group_membership.rb +++ b/app/jobs/regular/automatic_group_membership.rb @@ -13,11 +13,12 @@ module Jobs domains = group.automatic_membership_email_domains.gsub('.', '\.') - User.where("email ~* '@(#{domains})$'") - .where("users.id NOT IN (SELECT user_id FROM group_users WHERE group_users.group_id = ?)", group_id) - .activated - .where(staged: false) - .find_each do |user| + User.joins(:user_emails) + .where("user_emails.email ~* '@(#{domains})$'") + .where("users.id NOT IN (SELECT user_id FROM group_users WHERE group_users.group_id = ?)", group_id) + .activated + .where(staged: false) + .find_each do |user| next unless user.email_confirmed? group.add(user) GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user) diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index c13a6e7b03..b5077aff85 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -1,50 +1,11 @@ require 'open-uri' require 'nokogiri' require 'excon' -require 'final_destination' +require_dependency 'retrieve_title' module Jobs class CrawlTopicLink < Jobs::Base - class ReadEnough < StandardError; end - - def self.max_chunk_size(uri) - # Amazon leaves the title until very late. Normally it's a bad idea to make an exception for - # one host but amazon is a big one. - return 80 if uri.host =~ /amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/ - - # Default is 10k - 10 - end - - # Fetch the beginning of a HTML document at a url - def self.fetch_beginning(url) - # Never crawl in test mode - return if Rails.env.test? - - fd = FinalDestination.new(url) - uri = fd.resolve - return "" unless uri - - result = "" - streamer = lambda do |chunk, _, _| - result << chunk - - # Using exceptions for flow control is really bad, but there really seems to - # be no sane way to get a stream to stop reading in Excon (or Net::HTTP for - # that matter!) - raise ReadEnough.new if result.size > (CrawlTopicLink.max_chunk_size(uri) * 1024) - end - Excon.get(uri.to_s, response_block: streamer, read_timeout: 20, headers: fd.request_headers) - result - - rescue Excon::Errors::SocketError => ex - return result if ex.socket_error.is_a?(ReadEnough) - raise - rescue ReadEnough - result - end - def execute(args) raise Discourse::InvalidParameters.new(:topic_link_id) unless args[:topic_link_id].present? @@ -72,18 +33,9 @@ module Jobs unless crawled # Fetch the beginning of the document to find the title - result = CrawlTopicLink.fetch_beginning(topic_link.url) - doc = Nokogiri::HTML(result) - if doc - title = doc.at('title').try(:inner_text) - if title.present? - title.gsub!(/\n/, ' ') - title.gsub!(/ +/, ' ') - title.strip! - if title.present? - crawled = (TopicLink.where(id: topic_link.id).update_all(['title = ?, crawled_at = CURRENT_TIMESTAMP', title[0..254]]) == 1) - end - end + title = RetrieveTitle.crawl(topic_link.url) + if title.present? + crawled = (TopicLink.where(id: topic_link.id).update_all(['title = ?, crawled_at = CURRENT_TIMESTAMP', title[0..254]]) == 1) end end rescue Exception diff --git a/app/jobs/regular/delete_topic.rb b/app/jobs/regular/delete_topic.rb index e50cd9c2da..bfb23f568c 100644 --- a/app/jobs/regular/delete_topic.rb +++ b/app/jobs/regular/delete_topic.rb @@ -12,7 +12,7 @@ module Jobs if Guardian.new(topic_timer.user).can_delete?(topic) first_post = topic.ordered_posts.first - PostDestroyer.new(topic_timer.user, first_post, { context: I18n.t("topic_statuses.auto_deleted_by_timer") }).destroy + PostDestroyer.new(topic_timer.user, first_post, context: I18n.t("topic_statuses.auto_deleted_by_timer")).destroy topic_timer.trash!(Discourse.system_user) end end diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 46ce3f43bd..341702904a 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -83,12 +83,13 @@ module Jobs web_hook_event = WebHookEvent.create!(web_hook_id: web_hook.id) begin - content_type = case web_hook.content_type - when WebHook.content_types['application/x-www-form-urlencoded'] - 'application/x-www-form-urlencoded' - else - 'application/json' - end + content_type = + case web_hook.content_type + when WebHook.content_types['application/x-www-form-urlencoded'] + 'application/x-www-form-urlencoded' + else + 'application/json' + end headers = { 'Accept' => '*/*', diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index 336c0bfdcc..69eeec72d4 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -8,18 +8,18 @@ module Jobs sidekiq_options retry: false - HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new({ - user_archive: ['topic_title','category','sub_category','is_pm','post','like_count','reply_count','url','created_at'], - user_list: ['id','name','username','email','title','created_at','last_seen_at','last_posted_at','last_emailed_at','trust_level','approved','suspended_at','suspended_till','blocked','active','admin','moderator','ip_address'], - user_stats: ['topics_entered','posts_read_count','time_read','topic_count','post_count','likes_given','likes_received'], - user_profile: ['location','website','views'], - user_sso: ['external_id','external_email', 'external_username', 'external_name', 'external_avatar_url'], - staff_action: ['staff_user','action','subject','created_at','details', 'context'], - screened_email: ['email','action','match_count','last_match_at','created_at','ip_address'], - screened_ip: ['ip_address','action','match_count','last_match_at','created_at'], - screened_url: ['domain','action','match_count','last_match_at','created_at'], - report: ['date', 'value'], - }) + HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( + user_archive: ['topic_title', 'category', 'sub_category', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'], + user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'blocked', 'active', 'admin', 'moderator', 'ip_address'], + user_stats: ['topics_entered', 'posts_read_count', 'time_read', 'topic_count', 'post_count', 'likes_given', 'likes_received'], + user_profile: ['location', 'website', 'views'], + user_sso: ['external_id', 'external_email', 'external_username', 'external_name', 'external_avatar_url'], + staff_action: ['staff_user', 'action', 'subject', 'created_at', 'details', 'context'], + screened_email: ['email', 'action', 'match_count', 'last_match_at', 'created_at', 'ip_address'], + screened_ip: ['ip_address', 'action', 'match_count', 'last_match_at', 'created_at'], + screened_url: ['domain', 'action', 'match_count', 'last_match_at', 'created_at'], + report: ['date', 'value'] + ) def execute(args) @entity = args[:entity] @@ -58,11 +58,11 @@ module Jobs return enum_for(:user_archive_export) unless block_given? Post.includes(topic: :category) - .where(user_id: @current_user.id) - .select(:topic_id, :post_number, :raw, :like_count, :reply_count, :created_at) - .order(:created_at) - .with_deleted - .each do |user_archive| + .where(user_id: @current_user.id) + .select(:topic_id, :post_number, :raw, :like_count, :reply_count, :created_at) + .order(:created_at) + .with_deleted + .each do |user_archive| yield get_user_archive_fields(user_archive) end end @@ -132,9 +132,9 @@ module Jobs return enum_for(:screened_url_export) unless block_given? ScreenedUrl.select("domain, sum(match_count) as match_count, max(last_match_at) as last_match_at, min(created_at) as created_at") - .group(:domain) - .order('last_match_at DESC') - .each do |screened_url| + .group(:domain) + .order('last_match_at DESC') + .each do |screened_url| yield get_screened_url_fields(screened_url) end end @@ -181,14 +181,14 @@ module Jobs def get_base_user_array(user) user_array = [] - user_array.push(user.id,escape_comma(user.name),user.username,user.email,escape_comma(user.title),user.created_at,user.last_seen_at,user.last_posted_at,user.last_emailed_at,user.trust_level,user.approved,user.suspended_at,user.suspended_till,user.blocked,user.active,user.admin,user.moderator,user.ip_address,user.user_stat.topics_entered,user.user_stat.posts_read_count,user.user_stat.time_read,user.user_stat.topic_count,user.user_stat.post_count,user.user_stat.likes_given,user.user_stat.likes_received,escape_comma(user.user_profile.location),user.user_profile.website,user.user_profile.views) + user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.blocked, user.active, user.admin, user.moderator, user.ip_address, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views) end def add_single_sign_on(user, user_info_array) if user.single_sign_on_record - user_info_array.push(user.single_sign_on_record.external_id,user.single_sign_on_record.external_email,user.single_sign_on_record.external_username,escape_comma(user.single_sign_on_record.external_name),user.single_sign_on_record.external_avatar_url) + user_info_array.push(user.single_sign_on_record.external_id, user.single_sign_on_record.external_email, user.single_sign_on_record.external_username, escape_comma(user.single_sign_on_record.external_name), user.single_sign_on_record.external_avatar_url) else - user_info_array.push(nil,nil,nil,nil,nil) + user_info_array.push(nil, nil, nil, nil, nil) end user_info_array end @@ -233,7 +233,7 @@ module Jobs is_pm = topic_data.archetype == "private_message" ? I18n.t("csv_export.boolean_yes") : I18n.t("csv_export.boolean_no") url = "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive['post_number']}" - topic_hash = {"post" => user_archive['raw'], "topic_title" => topic_data.title, "category" => category_name, "sub_category" => sub_category, "is_pm" => is_pm, "url" => url} + topic_hash = { "post" => user_archive['raw'], "topic_title" => topic_data.title, "category" => category_name, "sub_category" => sub_category, "is_pm" => is_pm, "url" => url } user_archive.merge!(topic_hash) HEADER_ATTRS_FOR['user_archive'].each do |attr| diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index 393b324dc1..6b8647a36b 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -14,24 +14,25 @@ module Jobs users = User.activated.not_blocked.not_suspended.real - .joins(:user_option) - .where('user_options.mailing_list_mode AND user_options.mailing_list_mode_frequency > 0') - .where('NOT EXISTS ( + .joins(:user_option) + .where('user_options.mailing_list_mode AND user_options.mailing_list_mode_frequency > 0') + .where('NOT EXISTS ( SELECT 1 FROM muted_users mu WHERE mu.muted_user_id = ? AND mu.user_id = users.id )', post.user_id) - .where('NOT EXISTS ( + .where('NOT EXISTS ( SELECT 1 FROM topic_users tu WHERE tu.topic_id = ? AND tu.user_id = users.id AND tu.notification_level = ? )', post.topic_id, TopicUser.notification_levels[:muted]) - .where('NOT EXISTS ( + .where('NOT EXISTS ( SELECT 1 FROM category_users cu WHERE cu.category_id = ? AND cu.user_id = users.id AND cu.notification_level = ? )', post.topic.category_id, CategoryUser.notification_levels[:muted]) + DiscourseEvent.trigger(:notify_mailing_list_subscribers, users, post) users.find_each do |user| if Guardian.new(user).can_see?(post) if EmailLog.reached_max_emails?(user) @@ -56,7 +57,7 @@ module Jobs end end rescue => e - Discourse.handle_job_exception(e, error_context(args, "Sending post to mailing list subscribers", { user_id: user.id, user_email: user.email })) + Discourse.handle_job_exception(e, error_context(args, "Sending post to mailing list subscribers", user_id: user.id, user_email: user.email)) end end end diff --git a/app/jobs/regular/notify_moved_posts.rb b/app/jobs/regular/notify_moved_posts.rb index da1300e86c..9916bab8e7 100644 --- a/app/jobs/regular/notify_moved_posts.rb +++ b/app/jobs/regular/notify_moved_posts.rb @@ -17,8 +17,8 @@ module Jobs p.user.notifications.create(notification_type: Notification.types[:moved_post], topic_id: p.topic_id, post_number: p.post_number, - data: {topic_title: p.topic.title, - display_username: moved_by.username}.to_json) + data: { topic_title: p.topic.title, + display_username: moved_by.username }.to_json) users_notified << p.user_id end end diff --git a/app/jobs/regular/process_post.rb b/app/jobs/regular/process_post.rb index ddfb0ff3d5..1230dbc752 100644 --- a/app/jobs/regular/process_post.rb +++ b/app/jobs/regular/process_post.rb @@ -37,6 +37,14 @@ module Jobs post.publish_change_to_clients! :revised end end + + if !post.user.staff? && !post.user.staged + s = post.cooked + s << " #{post.topic.title}" if post.post_number == 1 + if WordWatcher.new(s).should_flag? + PostAction.act(Discourse.system_user, post, PostActionType.types[:inappropriate]) rescue PostAction::AlreadyActed + end + end end # onebox may have added some links, so extract them now diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index ed158bef13..c38d9a55e2 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -58,7 +58,7 @@ module Jobs log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}") end else - log(:error, "There was an error while downloading '#{src}' locally for post: #{post_id}") + log(:info, "There was an error while downloading '#{src}' locally for post: #{post_id}") end end # have we successfully downloaded that file? diff --git a/app/jobs/regular/push_notification.rb b/app/jobs/regular/push_notification.rb index e07e5d5920..965acb963b 100644 --- a/app/jobs/regular/push_notification.rb +++ b/app/jobs/regular/push_notification.rb @@ -13,11 +13,9 @@ module Jobs } clients = args["clients"] - clients.group_by{|r| r[1]}.each do |push_url, group| + clients.group_by { |r| r[1] }.each do |push_url, group| notifications = group.map do |client_id, _| - notification.merge({ - client_id: client_id - }) + notification.merge(client_id: client_id) end result = Excon.post(push_url, diff --git a/app/jobs/regular/retrieve_topic.rb b/app/jobs/regular/retrieve_topic.rb index 2820f05e31..9d5d4e248c 100644 --- a/app/jobs/regular/retrieve_topic.rb +++ b/app/jobs/regular/retrieve_topic.rb @@ -19,5 +19,3 @@ module Jobs end end - - diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index f9def1f451..f28019b74c 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -56,7 +56,7 @@ module Jobs quoted } - def message_for_email(user, post, type, notification, notification_type=nil, notification_data_hash=nil, email_token=nil, to_address=nil) + def message_for_email(user, post, type, notification, notification_type = nil, notification_data_hash = nil, email_token = nil, to_address = nil) set_skip_context(type, user.id, to_address || user.email, post.try(:id)) return skip_message(I18n.t("email_log.anonymous_user")) if user.anonymous? @@ -90,8 +90,8 @@ module Jobs user.user_option.mailing_list_mode_frequency > 0 && # don't catch notifications for users on daily mailing list mode (!post.try(:topic).try(:private_message?)) && NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type]) - # no need to log a reason when the mail was already sent via the mailing list job - return [nil, nil] + # no need to log a reason when the mail was already sent via the mailing list job + return [nil, nil] end unless user.user_option.email_always? @@ -141,7 +141,7 @@ module Jobs # extracted from sidekiq def self.seconds_to_delay(count) - (count ** 4) + 15 + (rand(30) * (count + 1)) + (count**4) + 15 + (rand(30) * (count + 1)) end private diff --git a/app/jobs/scheduled/badge_grant.rb b/app/jobs/scheduled/badge_grant.rb index dc5def0684..a9d9b915a7 100644 --- a/app/jobs/scheduled/badge_grant.rb +++ b/app/jobs/scheduled/badge_grant.rb @@ -15,7 +15,7 @@ module Jobs BadgeGranter.backfill(b) rescue => ex # TODO - expose errors in UI - Discourse.handle_job_exception(ex, error_context({}, code_desc: 'Exception granting badges', extra: {badge_id: b.id})) + Discourse.handle_job_exception(ex, error_context({}, code_desc: 'Exception granting badges', extra: { badge_id: b.id })) end end diff --git a/app/jobs/scheduled/clean_up_email_logs.rb b/app/jobs/scheduled/clean_up_email_logs.rb index 30305c4d9a..48421c1cfa 100644 --- a/app/jobs/scheduled/clean_up_email_logs.rb +++ b/app/jobs/scheduled/clean_up_email_logs.rb @@ -9,8 +9,8 @@ module Jobs threshold = SiteSetting.delete_email_logs_after_days.days.ago EmailLog.where(reply_key: nil) - .where("created_at < ?", threshold) - .delete_all + .where("created_at < ?", threshold) + .delete_all end end diff --git a/app/jobs/scheduled/clean_up_search_logs.rb b/app/jobs/scheduled/clean_up_search_logs.rb new file mode 100644 index 0000000000..bd3bc2e04a --- /dev/null +++ b/app/jobs/scheduled/clean_up_search_logs.rb @@ -0,0 +1,9 @@ +module Jobs + class CleanUpSearchLogs < Jobs::Scheduled + every 1.week + + def execute(args) + SearchLog.clean_up + end + end +end diff --git a/app/jobs/scheduled/clean_up_unmatched_emails.rb b/app/jobs/scheduled/clean_up_unmatched_emails.rb index 6ce62f3f23..4fff997ef8 100644 --- a/app/jobs/scheduled/clean_up_unmatched_emails.rb +++ b/app/jobs/scheduled/clean_up_unmatched_emails.rb @@ -7,8 +7,8 @@ module Jobs last_match_threshold = SiteSetting.max_age_unmatched_emails.days.ago ScreenedEmail.where(action_type: ScreenedEmail.actions[:block]) - .where("last_match_at < ?", last_match_threshold) - .destroy_all + .where("last_match_at < ?", last_match_threshold) + .destroy_all end end diff --git a/app/jobs/scheduled/clean_up_unmatched_ips.rb b/app/jobs/scheduled/clean_up_unmatched_ips.rb index abd3cdc442..a9b4e4d5d3 100644 --- a/app/jobs/scheduled/clean_up_unmatched_ips.rb +++ b/app/jobs/scheduled/clean_up_unmatched_ips.rb @@ -11,8 +11,8 @@ module Jobs # remove old unmatched IP addresses ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]) - .where("last_match_at < ? OR (last_match_at IS NULL AND created_at < ?)", last_match_threshold, last_match_threshold) - .destroy_all + .where("last_match_at < ? OR (last_match_at IS NULL AND created_at < ?)", last_match_threshold, last_match_threshold) + .destroy_all end end diff --git a/app/jobs/scheduled/clean_up_unsubscribe_keys.rb b/app/jobs/scheduled/clean_up_unsubscribe_keys.rb index 0535866671..606a13390b 100644 --- a/app/jobs/scheduled/clean_up_unsubscribe_keys.rb +++ b/app/jobs/scheduled/clean_up_unsubscribe_keys.rb @@ -10,4 +10,3 @@ module Jobs end end - diff --git a/app/jobs/scheduled/create_missing_avatars.rb b/app/jobs/scheduled/create_missing_avatars.rb index 2d4befe245..d973e9a0be 100644 --- a/app/jobs/scheduled/create_missing_avatars.rb +++ b/app/jobs/scheduled/create_missing_avatars.rb @@ -5,11 +5,11 @@ module Jobs def execute(args) # backfill in batches of 5000 an hour UserAvatar.includes(:user) - .joins(:user) - .where(last_gravatar_download_attempt: nil) - .order("users.last_posted_at DESC") - .limit(5000) - .each do |u| + .joins(:user) + .where(last_gravatar_download_attempt: nil) + .order("users.last_posted_at DESC") + .limit(5000) + .each do |u| u.user.refresh_avatar end end diff --git a/app/jobs/scheduled/dashboard_stats.rb b/app/jobs/scheduled/dashboard_stats.rb index 2b35fc4380..75977e7acb 100644 --- a/app/jobs/scheduled/dashboard_stats.rb +++ b/app/jobs/scheduled/dashboard_stats.rb @@ -7,7 +7,7 @@ module Jobs if problems_started_at && problems_started_at < 2.days.ago # If there have been problems reported on the dashboard for a while, # send a message to admins no more often than once per week. - GroupMessage.create(Group[:admins].name, :dashboard_problems, {limit_once_per: 7.days.to_i}) + GroupMessage.create(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i) end AdminDashboardData.refresh_stats diff --git a/app/jobs/scheduled/directory_refresh_older.rb b/app/jobs/scheduled/directory_refresh_older.rb index 3c25c8151f..93147d6c88 100644 --- a/app/jobs/scheduled/directory_refresh_older.rb +++ b/app/jobs/scheduled/directory_refresh_older.rb @@ -4,7 +4,7 @@ module Jobs def execute(args) periods = DirectoryItem.period_types.keys - [:daily] - periods.each {|p| DirectoryItem.refresh_period!(p)} + periods.each { |p| DirectoryItem.refresh_period!(p) } end end end diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index 1bd4ed4fdf..23db70d4e5 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -14,15 +14,15 @@ module Jobs def target_user_ids # Users who want to receive digest email within their chosen digest email frequency query = User.real - .not_suspended - .activated - .where(staged: false) - .joins(:user_option, :user_stat) - .where("user_options.email_digests") - .where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}") - .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") - .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") - .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.suppress_digest_email_after_days})") + .not_suspended + .activated + .where(staged: false) + .joins(:user_option, :user_stat) + .where("user_options.email_digests") + .where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}") + .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") + .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") + .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.suppress_digest_email_after_days})") # If the site requires approval, make sure the user is approved query = query.where("approved OR moderator OR admin") if SiteSetting.must_approve_users? diff --git a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb index c2190d1605..83e5b5cc72 100644 --- a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb +++ b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb @@ -8,7 +8,7 @@ module Jobs def execute(args) badge = Badge.find(Badge::NewUserOfTheMonth) - return unless SiteSetting.enable_badges? and badge.enabled? + return unless SiteSetting.enable_badges? && badge.enabled? # Don't award it if a month hasn't gone by return if UserBadge.where("badge_id = ? AND granted_at >= ?", @@ -22,9 +22,7 @@ module Jobs user = User.find(user_id) if user.badges.where(id: Badge::NewUserOfTheMonth).blank? BadgeGranter.grant(badge, user) - SystemMessage.new(user).create('new_user_of_the_month', { - month_year: Time.now.strftime("%B %Y") - }) + SystemMessage.new(user).create('new_user_of_the_month', month_year: Time.now.strftime("%B %Y")) end end end @@ -75,10 +73,8 @@ module Jobs LIMIT :max_awarded SQL - User.exec_sql(sql, { - like: PostActionType.types[:like], - max_awarded: MAX_AWARDED - }).each do |row| + User.exec_sql(sql, like: PostActionType.types[:like], + max_awarded: MAX_AWARDED).each do |row| scores[row['id'].to_i] = row['score'].to_f end scores diff --git a/app/jobs/scheduled/migrate_upload_scheme.rb b/app/jobs/scheduled/migrate_upload_scheme.rb index a5e21f3e0d..6be51da699 100644 --- a/app/jobs/scheduled/migrate_upload_scheme.rb +++ b/app/jobs/scheduled/migrate_upload_scheme.rb @@ -9,8 +9,8 @@ module Jobs # clean up failed uploads Upload.where("created_at < ?", 1.hour.ago) - .where("LENGTH(COALESCE(url, '')) = 0") - .destroy_all + .where("LENGTH(COALESCE(url, '')) = 0") + .destroy_all # migrate uploads to new scheme problems = Upload.migrate_to_new_scheme(50) diff --git a/app/jobs/scheduled/pending_flags_reminder.rb b/app/jobs/scheduled/pending_flags_reminder.rb index 2a33c51785..128a52187c 100644 --- a/app/jobs/scheduled/pending_flags_reminder.rb +++ b/app/jobs/scheduled/pending_flags_reminder.rb @@ -25,8 +25,8 @@ module Jobs target_group_names: Group[:moderators].name, archetype: Archetype.private_message, subtype: TopicSubtype.system_message, - title: I18n.t('flags_reminder.subject_template', { count: flagged_posts_count }), - raw: mentions + I18n.t('flags_reminder.flags_were_submitted', { count: SiteSetting.notify_about_flags_after }) + title: I18n.t('flags_reminder.subject_template', count: flagged_posts_count), + raw: mentions + I18n.t('flags_reminder.flags_were_submitted', count: SiteSetting.notify_about_flags_after) ) self.last_notified_id = flag_ids.max @@ -54,10 +54,10 @@ module Jobs def active_moderator_usernames User.where(moderator: true) - .human_users - .order('last_seen_at DESC') - .limit(3) - .pluck(:username) + .human_users + .order('last_seen_at DESC') + .limit(3) + .pluck(:username) end end diff --git a/app/jobs/scheduled/pending_users_reminder.rb b/app/jobs/scheduled/pending_users_reminder.rb index c2e392d65a..5c682bb4e3 100644 --- a/app/jobs/scheduled/pending_users_reminder.rb +++ b/app/jobs/scheduled/pending_users_reminder.rb @@ -7,7 +7,7 @@ module Jobs def execute(args) if SiteSetting.must_approve_users && SiteSetting.pending_users_reminder_delay >= 0 - query = AdminUserIndexQuery.new({query: 'pending', stats: false}).find_users_query # default order is: users.created_at DESC + query = AdminUserIndexQuery.new(query: 'pending', stats: false).find_users_query # default order is: users.created_at DESC if SiteSetting.pending_users_reminder_delay > 0 query = query.where('users.created_at < ?', SiteSetting.pending_users_reminder_delay.hours.ago) end @@ -21,10 +21,10 @@ module Jobs if count > 0 target_usernames = Group[:moderators].users.map do |u| u.id > 0 && u.notifications.joins(:topic) - .where("notifications.id > ?", u.seen_notification_id) - .where("notifications.read = false") - .where("topics.subtype = '#{TopicSubtype.pending_users_reminder}'") - .count == 0 ? u.username : nil + .where("notifications.id > ?", u.seen_notification_id) + .where("notifications.read = false") + .where("topics.subtype = '#{TopicSubtype.pending_users_reminder}'") + .count == 0 ? u.username : nil end.compact unless target_usernames.empty? @@ -33,8 +33,8 @@ module Jobs target_usernames: target_usernames, archetype: Archetype.private_message, subtype: TopicSubtype.pending_users_reminder, - title: I18n.t("system_messages.pending_users_reminder.subject_template", {count: count}), - raw: I18n.t("system_messages.pending_users_reminder.text_body_template", {count: count, base_url: Discourse.base_url}) + title: I18n.t("system_messages.pending_users_reminder.subject_template", count: count), + raw: I18n.t("system_messages.pending_users_reminder.text_body_template", count: count, base_url: Discourse.base_url) ) self.previous_newest_username = newest_username diff --git a/app/jobs/scheduled/periodical_updates.rb b/app/jobs/scheduled/periodical_updates.rb index 98751dffc3..a122343482 100644 --- a/app/jobs/scheduled/periodical_updates.rb +++ b/app/jobs/scheduled/periodical_updates.rb @@ -20,7 +20,7 @@ module Jobs CategoryFeaturedTopic.feature_topics # Update the scores of posts - args = {min_topic_age: 1.day.ago} + args = { min_topic_age: 1.day.ago } args[:max_topic_length] = 500 unless self.class.should_update_long_topics? ScoreCalculator.new.calculate(args) diff --git a/app/jobs/scheduled/purge_inactive.rb b/app/jobs/scheduled/purge_inactive.rb index a6615f5fe2..21417bef21 100644 --- a/app/jobs/scheduled/purge_inactive.rb +++ b/app/jobs/scheduled/purge_inactive.rb @@ -7,4 +7,3 @@ module Jobs end end end - diff --git a/app/jobs/scheduled/tl3_promotions.rb b/app/jobs/scheduled/tl3_promotions.rb index 37acfcec15..a816d471c5 100644 --- a/app/jobs/scheduled/tl3_promotions.rb +++ b/app/jobs/scheduled/tl3_promotions.rb @@ -19,7 +19,7 @@ module Jobs # Promotions User.real.where(trust_level: TrustLevel[2], trust_level_locked: false) - .where.not(id: demoted_user_ids).find_each do |u| + .where.not(id: demoted_user_ids).find_each do |u| Promotion.new(u).review_tl2 end end diff --git a/app/jobs/scheduled/version_check.rb b/app/jobs/scheduled/version_check.rb index 9fb39252ee..51b08d379d 100644 --- a/app/jobs/scheduled/version_check.rb +++ b/app/jobs/scheduled/version_check.rb @@ -6,7 +6,7 @@ module Jobs every 1.day def execute(args) - if SiteSetting.version_checks? and (DiscourseUpdates.updated_at.nil? or DiscourseUpdates.updated_at < 1.minute.ago) + if SiteSetting.version_checks? && (DiscourseUpdates.updated_at.nil? || DiscourseUpdates.updated_at < (1.minute.ago)) begin prev_missing_versions_count = DiscourseUpdates.missing_versions_count || 0 @@ -18,10 +18,10 @@ module Jobs DiscourseUpdates.updated_at = Time.zone.now DiscourseUpdates.missing_versions = json['versions'] - if GlobalSetting.new_version_emails and - SiteSetting.new_version_emails and - json['missingVersionsCount'] > 0 and - prev_missing_versions_count < json['missingVersionsCount'].to_i + if GlobalSetting.new_version_emails && + SiteSetting.new_version_emails && + json['missingVersionsCount'] > (0) && + prev_missing_versions_count < (json['missingVersionsCount'].to_i) message = VersionMailer.send_notice Email::Sender.new(message, :new_version).send diff --git a/app/mailers/admin_confirmation_mailer.rb b/app/mailers/admin_confirmation_mailer.rb index ea4d444ad8..dc2d7df41d 100644 --- a/app/mailers/admin_confirmation_mailer.rb +++ b/app/mailers/admin_confirmation_mailer.rb @@ -12,4 +12,3 @@ class AdminConfirmationMailer < ActionMailer::Base ) end end - diff --git a/app/mailers/pending_flags_mailer.rb b/app/mailers/pending_flags_mailer.rb index c0f67d2bf2..39fbbb0fd1 100644 --- a/app/mailers/pending_flags_mailer.rb +++ b/app/mailers/pending_flags_mailer.rb @@ -23,14 +23,14 @@ class PendingFlagsMailer < ActionMailer::Base @hours = SiteSetting.notify_about_flags_after - subject = "[#{SiteSetting.title}] " + I18n.t('flags_reminder.subject_template', { count: PostAction.flagged_posts_count }) + subject = "[#{SiteSetting.title}] " + I18n.t('flags_reminder.subject_template', count: PostAction.flagged_posts_count) build_email(SiteSetting.contact_email, subject: subject) end private def flag_reason_counts(post) - post[:post_actions].inject({}) do |h,v| + post[:post_actions].inject({}) do |h, v| h[v[:name_key]] ||= 0 h[v[:name_key]] += 1 h diff --git a/app/mailers/pending_queued_posts_mailer.rb b/app/mailers/pending_queued_posts_mailer.rb index fec7c3ba72..525454b7d2 100644 --- a/app/mailers/pending_queued_posts_mailer.rb +++ b/app/mailers/pending_queued_posts_mailer.rb @@ -3,7 +3,7 @@ require_dependency 'email/message_builder' class PendingQueuedPostsMailer < ActionMailer::Base include Email::BuildEmailHelper - def notify(opts={}) + def notify(opts = {}) return unless SiteSetting.contact_email build_email(SiteSetting.contact_email, template: 'queued_posts_reminder', count: opts[:count]) end diff --git a/app/mailers/subscription_mailer.rb b/app/mailers/subscription_mailer.rb index 51a66797a6..d8ea40d2fb 100644 --- a/app/mailers/subscription_mailer.rb +++ b/app/mailers/subscription_mailer.rb @@ -3,7 +3,7 @@ require_dependency 'email/message_builder' class SubscriptionMailer < ActionMailer::Base include Email::BuildEmailHelper - def confirm_unsubscribe(user, opts={}) + def confirm_unsubscribe(user, opts = {}) unsubscribe_key = UnsubscribeKey.create_key_for(user, "all") build_email user.email, template: "unsubscribe_mailer", diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 3fa3f96a70..049e665827 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -11,14 +11,14 @@ class UserNotifications < ActionMailer::Base include Email::BuildEmailHelper - def signup(user, opts={}) + def signup(user, opts = {}) build_email(user.email, template: "user_notifications.signup", locale: user_locale(user), email_token: opts[:email_token]) end - def signup_after_approval(user, opts={}) + def signup_after_approval(user, opts = {}) build_email(user.email, template: 'user_notifications.signup_after_approval', locale: user_locale(user), @@ -26,42 +26,42 @@ class UserNotifications < ActionMailer::Base new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url, locale: locale)) end - def notify_old_email(user, opts={}) + def notify_old_email(user, opts = {}) build_email(user.email, template: "user_notifications.notify_old_email", locale: user_locale(user), new_email: opts[:new_email]) end - def confirm_old_email(user, opts={}) + def confirm_old_email(user, opts = {}) build_email(user.email, template: "user_notifications.confirm_old_email", locale: user_locale(user), email_token: opts[:email_token]) end - def confirm_new_email(user, opts={}) + def confirm_new_email(user, opts = {}) build_email(user.email, template: "user_notifications.confirm_new_email", locale: user_locale(user), email_token: opts[:email_token]) end - def forgot_password(user, opts={}) + def forgot_password(user, opts = {}) build_email(user.email, template: user.has_password? ? "user_notifications.forgot_password" : "user_notifications.set_password", locale: user_locale(user), email_token: opts[:email_token]) end - def admin_login(user, opts={}) + def admin_login(user, opts = {}) build_email(user.email, template: "user_notifications.admin_login", locale: user_locale(user), email_token: opts[:email_token]) end - def account_created(user, opts={}) + def account_created(user, opts = {}) build_email(user.email, template: "user_notifications.account_created", locale: user_locale(user), @@ -76,30 +76,30 @@ class UserNotifications < ActionMailer::Base end end - def digest(user, opts={}) + def digest(user, opts = {}) build_summary_for(user) min_date = opts[:since] || user.last_emailed_at || user.last_seen_at || 1.month.ago # Fetch some topics and posts to show - digest_opts = {limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, top_order: true} + digest_opts = { limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, top_order: true } topics_for_digest = Topic.for_digest(user, min_date, digest_opts).to_a if topics_for_digest.empty? && !user.user_option.try(:include_tl0_in_digests) # Find some topics from new users that are at least 24 hours old topics_for_digest = Topic.for_digest(user, min_date, digest_opts.merge(include_tl0: true)).where('topics.created_at < ?', 24.hours.ago).to_a end - @popular_topics = topics_for_digest[0,SiteSetting.digest_topics] + @popular_topics = topics_for_digest[0, SiteSetting.digest_topics] if @popular_topics.present? @other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : [] @popular_posts = if SiteSetting.digest_posts > 0 Post.order("posts.score DESC") - .for_mailing_list(user, min_date) - .where('posts.post_type = ?', Post.types[:regular]) - .where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false') - .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0) - .limit(SiteSetting.digest_posts) + .for_mailing_list(user, min_date) + .where('posts.post_type = ?', Post.types[:regular]) + .where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false') + .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0) + .limit(SiteSetting.digest_posts) else [] end @@ -117,19 +117,19 @@ class UserNotifications < ActionMailer::Base # We used topics from new users instead, so count should match new_topics_count = topics_for_digest.size end - @counts = [{label_key: 'user_notifications.digest.new_topics', - value: new_topics_count, - href: "#{Discourse.base_url}/new"}] + @counts = [{ label_key: 'user_notifications.digest.new_topics', + value: new_topics_count, + href: "#{Discourse.base_url}/new" }] value = user.unread_notifications - @counts << {label_key: 'user_notifications.digest.unread_notifications', value: value, href: "#{Discourse.base_url}/my/notifications"} if value > 0 + @counts << { label_key: 'user_notifications.digest.unread_notifications', value: value, href: "#{Discourse.base_url}/my/notifications" } if value > 0 value = user.unread_private_messages - @counts << {label_key: 'user_notifications.digest.unread_messages', value: value, href: "#{Discourse.base_url}/my/messages"} if value > 0 + @counts << { label_key: 'user_notifications.digest.unread_messages', value: value, href: "#{Discourse.base_url}/my/messages" } if value > 0 if @counts.size < 3 value = user.unread_notifications_of_type(Notification.types[:liked]) - @counts << {label_key: 'user_notifications.digest.liked_received', value: value, href: "#{Discourse.base_url}/my/notifications"} if value > 0 + @counts << { label_key: 'user_notifications.digest.liked_received', value: value, href: "#{Discourse.base_url}/my/notifications" } if value > 0 end if @counts.size < 3 @@ -157,7 +157,6 @@ class UserNotifications < ActionMailer::Base end end - def user_replied(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true @@ -252,7 +251,7 @@ class UserNotifications < ActionMailer::Base (user.locale.present? && I18n.available_locales.include?(user.locale.to_sym)) ? user.locale : nil end - def email_post_markdown(post, add_posted_by=false) + def email_post_markdown(post, add_posted_by = false) result = "#{post.raw}\n\n" if add_posted_by result << "#{I18n.t('user_notifications.posted_by', username: post.username, post_date: post.created_at.strftime("%m/%d/%Y"))}\n\n" @@ -275,12 +274,12 @@ class UserNotifications < ActionMailer::Base allowed_post_types << Post.types[:whisper] if topic_user.try(:user).try(:staff?) context_posts = Post.where(topic_id: post.topic_id) - .where("post_number < ?", post.post_number) - .where(user_deleted: false) - .where(hidden: false) - .where(post_type: allowed_post_types) - .order('created_at desc') - .limit(SiteSetting.email_posts_context) + .where("post_number < ?", post.post_number) + .where(user_deleted: false) + .where(hidden: false) + .where(post_type: allowed_post_types) + .order('created_at desc') + .limit(SiteSetting.email_posts_context) if topic_user && topic_user.last_emailed_post_number && user.user_option.email_previous_replies == UserOption.previous_replies_type[:unless_emailed] context_posts = context_posts.where("post_number > ?", topic_user.last_emailed_post_number) @@ -311,7 +310,7 @@ class UserNotifications < ActionMailer::Base allow_reply_by_email = opts[:allow_reply_by_email] unless user.suspended? original_username = notification_data[:original_username] || notification_data[:display_username] - send_notification_email( + email_options = { title: notification_data[:topic_title], post: post, username: original_username, @@ -323,7 +322,13 @@ class UserNotifications < ActionMailer::Base notification_type: notification_type, use_invite_template: opts[:use_invite_template], user: user - ) + } + + if group_id = notification_data[:group_id] + email_options[:group_name] = Group.find_by(id: group_id)&.name + end + + send_notification_email(email_options) end def send_notification_email(opts) @@ -337,12 +342,18 @@ class UserNotifications < ActionMailer::Base from_alias = opts[:from_alias] notification_type = opts[:notification_type] user = opts[:user] + group_name = opts[:group_name] locale = user_locale(user) template = "user_notifications.user_#{notification_type}" if post.topic.private_message? template << "_pm" - template << "_staged" if user.staged? + + if group_name + template << "_group" + elsif user.staged + template << "_staged" + end end # category name @@ -376,17 +387,33 @@ class UserNotifications < ActionMailer::Base end end - translation_override_exists = TranslationOverride.where(locale: SiteSetting.default_locale, translation_key: "#{template}.text_body_template").exists? + translation_override_exists = TranslationOverride.where( + locale: SiteSetting.default_locale, + translation_key: "#{template}.text_body_template" + ).exists? if opts[:use_invite_template] - if post.topic.private_message? - invite_template = "user_notifications.invited_to_private_message_body" - else - invite_template = "user_notifications.invited_to_topic_body" - end + invite_template = "user_notifications.invited" + invite_template << "_group" if group_name + + invite_template << + if post.topic.private_message? + "_to_private_message_body" + else + "_to_topic_body" + end + topic_excerpt = post.excerpt.tr("\n", " ") if post.is_first_post? && post.excerpt topic_excerpt = "" if SiteSetting.private_email? - message = I18n.t(invite_template, username: username, topic_title: gsub_emoji_to_unicode(title), topic_excerpt: topic_excerpt, site_title: SiteSetting.title, site_description: SiteSetting.site_description) + + message = I18n.t(invite_template, + username: username, + group_name: group_name, + topic_title: gsub_emoji_to_unicode(title), + topic_excerpt: topic_excerpt, + site_title: SiteSetting.title, + site_description: SiteSetting.site_description + ) unless translation_override_exists html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render( @@ -401,7 +428,7 @@ class UserNotifications < ActionMailer::Base reached_limit = SiteSetting.max_emails_per_day_per_user > 0 reached_limit &&= (EmailLog.where(user_id: user.id, skipped: false) .where('created_at > ?', 1.day.ago) - .count) >= (SiteSetting.max_emails_per_day_per_user-1) + .count) >= (SiteSetting.max_emails_per_day_per_user - 1) in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to if SiteSetting.private_email? @@ -410,7 +437,6 @@ class UserNotifications < ActionMailer::Base message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : ""); end - unless translation_override_exists html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render( template: 'email/notification', @@ -426,7 +452,7 @@ class UserNotifications < ActionMailer::Base end email_opts = { - topic_title: gsub_emoji_to_unicode(title), + topic_title: Emoji.gsub_emoji_to_unicode(title), topic_title_url_encoded: title ? URI.encode(title) : title, message: message, url: post.url(without_slug: SiteSetting.private_email?), @@ -434,6 +460,7 @@ class UserNotifications < ActionMailer::Base topic_id: post.topic_id, context: context, username: username, + group_name: group_name, add_unsubscribe_link: !user.staged, mailing_list_mode: user.user_option.mailing_list_mode, unsubscribe_url: post.unsubscribe_url(user), diff --git a/app/mailers/version_mailer.rb b/app/mailers/version_mailer.rb index b39136933c..dad4dbd448 100644 --- a/app/mailers/version_mailer.rb +++ b/app/mailers/version_mailer.rb @@ -6,17 +6,17 @@ class VersionMailer < ActionMailer::Base def send_notice if SiteSetting.contact_email.present? missing_versions = DiscourseUpdates.missing_versions - if missing_versions.present? and missing_versions.first['notes'].present? - build_email( SiteSetting.contact_email, + if missing_versions.present? && missing_versions.first['notes'].present? + build_email(SiteSetting.contact_email, template: 'new_version_mailer_with_notes', notes: missing_versions.first['notes'], new_version: DiscourseUpdates.latest_version, - installed_version: Discourse::VERSION::STRING ) + installed_version: Discourse::VERSION::STRING) else - build_email( SiteSetting.contact_email, + build_email(SiteSetting.contact_email, template: 'new_version_mailer', new_version: DiscourseUpdates.latest_version, - installed_version: Discourse::VERSION::STRING ) + installed_version: Discourse::VERSION::STRING) end end end diff --git a/app/models/about.rb b/app/models/about.rb index 0d670183ba..8392df3c48 100644 --- a/app/models/about.rb +++ b/app/models/about.rb @@ -35,8 +35,8 @@ class About def moderators @moderators ||= User.where(moderator: true, admin: false) - .human_users - .order(:username_lower) + .human_users + .order(:username_lower) end def admins diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index b4113d609e..20550a9fd0 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -31,7 +31,7 @@ class AdminDashboardData USER_REPORTS ||= ['users_by_trust_level'] - MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select {|r| r =~ /mobile/}.map { |r| r + "_reqs" } + MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select { |r| r =~ /mobile/ }.map { |r| r + "_reqs" } def self.add_problem_check(*syms, &blk) @problem_syms.push(*syms) if syms @@ -120,7 +120,7 @@ class AdminDashboardData $redis.get(problem_message_key(i18n_key)) ? I18n.t(i18n_key) : nil end - def self.add_problem_message(i18n_key, expire_seconds=nil) + def self.add_problem_message(i18n_key, expire_seconds = nil) if expire_seconds.to_i > 0 $redis.setex problem_message_key(i18n_key), expire_seconds.to_i, 1 else diff --git a/app/models/api_key.rb b/app/models/api_key.rb index 9145ce4372..a617902d41 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -2,6 +2,7 @@ class ApiKey < ActiveRecord::Base belongs_to :user belongs_to :created_by, class_name: User + validates :user_id, uniqueness: true validates_presence_of :key def regenerate!(updated_by) diff --git a/app/models/application_request.rb b/app/models/application_request.rb index 8f146f10fd..1f1f758d95 100644 --- a/app/models/application_request.rb +++ b/app/models/application_request.rb @@ -19,7 +19,7 @@ class ApplicationRequest < ActiveRecord::Base self.autoflush_seconds = 5.minutes self.last_flush = Time.now.utc - def self.increment!(type, opts=nil) + def self.increment!(type, opts = nil) key = redis_key(type) val = $redis.incr(key).to_i # 3.days, see: https://github.com/rails/rails/issues/21296 @@ -36,7 +36,7 @@ class ApplicationRequest < ActiveRecord::Base end end - def self.write_cache!(date=nil) + def self.write_cache!(date = nil) if date.nil? write_cache!(Time.now.utc) write_cache!(Time.now.utc.yesterday) @@ -49,8 +49,8 @@ class ApplicationRequest < ActiveRecord::Base # this may seem a bit fancy but in so it allows # for concurrent calls without double counting - req_types.each do |req_type,_| - key = redis_key(req_type,date) + req_types.each do |req_type, _| + key = redis_key(req_type, date) val = $redis.get(key).to_i next if val == 0 @@ -63,28 +63,28 @@ class ApplicationRequest < ActiveRecord::Base next end - id = req_id(date,req_type) + id = req_id(date, req_type) where(id: id).update_all(["count = count + ?", val]) end end - def self.clear_cache!(date=nil) + def self.clear_cache!(date = nil) if date.nil? clear_cache!(Time.now.utc) clear_cache!(Time.now.utc.yesterday) return end - req_types.each do |req_type,_| - key = redis_key(req_type,date) + req_types.each do |req_type, _| + key = redis_key(req_type, date) $redis.del key end end protected - def self.req_id(date,req_type,retries=0) + def self.req_id(date, req_type, retries = 0) req_type_id = req_types[req_type] @@ -94,13 +94,13 @@ class ApplicationRequest < ActiveRecord::Base rescue # primary key violation if retries == 0 - req_id(date,req_type,1) + req_id(date, req_type, 1) else raise end end - def self.redis_key(req_type, time=Time.now.utc) + def self.redis_key(req_type, time = Time.now.utc) "app_req_#{req_type}#{time.strftime('%Y%m%d')}" end diff --git a/app/models/backup.rb b/app/models/backup.rb index d23d459d00..72a619a06c 100644 --- a/app/models/backup.rb +++ b/app/models/backup.rb @@ -10,9 +10,9 @@ class Backup def self.all Dir.glob(File.join(Backup.base_directory, "*.{gz,tgz}")) - .sort_by { |file| File.mtime(file) } - .reverse - .map { |backup| Backup.create_from_filename(File.basename(backup)) } + .sort_by { |file| File.mtime(file) } + .reverse + .map { |backup| Backup.create_from_filename(File.basename(backup)) } end def self.[](filename) diff --git a/app/models/badge.rb b/app/models/badge.rb index ac1b3a4d49..46502613f5 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -63,7 +63,7 @@ class Badge < ActiveRecord::Base def self.trigger_hash Hash[*( - Badge::Trigger.constants.map{|k| + Badge::Trigger.constants.map { |k| [k.to_s.underscore, Badge::Trigger.const_get(k)] }.flatten )] @@ -100,7 +100,7 @@ class Badge < ActiveRecord::Base validates :allow_title, inclusion: [true, false] validates :multiple_grant, inclusion: [true, false] - scope :enabled, ->{ where(enabled: true) } + scope :enabled, -> { where(enabled: true) } before_create :ensure_not_system @@ -208,7 +208,6 @@ class Badge < ActiveRecord::Base val end - def slug Slug.for(self.display_name, '-') end diff --git a/app/models/badge_type.rb b/app/models/badge_type.rb index 11d4a4b655..b1d6e91edd 100644 --- a/app/models/badge_type.rb +++ b/app/models/badge_type.rb @@ -3,7 +3,6 @@ class BadgeType < ActiveRecord::Base Silver = 2 Bronze = 3 - has_many :badges validates :name, presence: true, uniqueness: true end diff --git a/app/models/category.rb b/app/models/category.rb index 02956b4eef..7939103cd0 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -72,7 +72,6 @@ class Category < ActiveRecord::Base has_many :category_tag_groups, dependent: :destroy has_many :tag_groups, through: :category_tag_groups - scope :latest, -> { order('topic_count DESC') } scope :secured, -> (guardian = nil) { @@ -141,10 +140,10 @@ class Category < ActiveRecord::Base def self.update_stats topics_with_post_count = Topic - .select("topics.category_id, COUNT(*) topic_count, SUM(topics.posts_count) post_count") - .where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)") - .group("topics.category_id") - .visible.to_sql + .select("topics.category_id, COUNT(*) topic_count, SUM(topics.posts_count) post_count") + .where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)") + .group("topics.category_id") + .visible.to_sql Category.exec_sql <<-SQL UPDATE categories c @@ -183,13 +182,12 @@ SQL end end - def visible_posts query = Post.joins(:topic) - .where(['topics.category_id = ?', self.id]) - .where('topics.visible = true') - .where('posts.deleted_at IS NULL') - .where('posts.user_deleted = false') + .where(['topics.category_id = ?', self.id]) + .where('topics.visible = true') + .where('posts.deleted_at IS NULL') + .where('posts.user_deleted = false') self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query end @@ -256,11 +254,11 @@ SQL def publish_category group_ids = self.groups.pluck(:id) if self.read_restricted - MessageBus.publish('/categories', {categories: ActiveModel::ArraySerializer.new([self]).as_json}, group_ids: group_ids) + MessageBus.publish('/categories', { categories: ActiveModel::ArraySerializer.new([self]).as_json }, group_ids: group_ids) end def publish_category_deletion - MessageBus.publish('/categories', {deleted_categories: [self.id]}) + MessageBus.publish('/categories', deleted_categories: [self.id]) end def parent_category_validator @@ -321,7 +319,7 @@ SQL end def allowed_tags=(tag_names_arg) - DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, {unlimited: true}) + DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end def allowed_tag_groups=(group_names) @@ -359,21 +357,21 @@ SQL def update_latest latest_post_id = Post - .order("posts.created_at desc") - .where("NOT hidden") - .joins("join topics on topics.id = topic_id") - .where("topics.category_id = :id", id: self.id) - .limit(1) - .pluck("posts.id") - .first + .order("posts.created_at desc") + .where("NOT hidden") + .joins("join topics on topics.id = topic_id") + .where("topics.category_id = :id", id: self.id) + .limit(1) + .pluck("posts.id") + .first latest_topic_id = Topic - .order("topics.created_at desc") - .where("visible") - .where("topics.category_id = :id", id: self.id) - .limit(1) - .pluck("topics.id") - .first + .order("topics.created_at desc") + .where("visible") + .where("topics.category_id = :id", id: self.id) + .limit(1) + .pluck("topics.id") + .first self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id) end @@ -384,7 +382,7 @@ SQL everyone = Group::AUTO_GROUPS[:everyone] full = CategoryGroup.permission_types[:full] - mapped = permissions.map do |group,permission| + mapped = permissions.map do |group, permission| group = group.id if group.is_a?(Group) # subtle, using Group[] ensures the group exists in the DB @@ -498,7 +496,7 @@ SQL SearchIndexer.index(self) end - def self.find_by_slug(category_slug, parent_category_slug=nil) + def self.find_by_slug(category_slug, parent_category_slug = nil) if parent_category_slug parent_category_id = self.where(slug: parent_category_slug, parent_category_id: nil).pluck(:id).first self.where(slug: category_slug, parent_category_id: parent_category_id).first diff --git a/app/models/category_featured_topic.rb b/app/models/category_featured_topic.rb index 78331574db..22d948dd59 100644 --- a/app/models/category_featured_topic.rb +++ b/app/models/category_featured_topic.rb @@ -13,7 +13,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base end end - def self.feature_topics_for(c, existing=nil) + def self.feature_topics_for(c, existing = nil) return if c.blank? query_opts = { @@ -28,7 +28,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base results = query.list_category_topic_ids(c).uniq # Add some topics that are visible to everyone: - anon_query = TopicQuery.new(nil, query_opts.merge({except_topic_ids: [c.topic_id] + results})) + anon_query = TopicQuery.new(nil, query_opts.merge(except_topic_ids: [c.topic_id] + results)) results += anon_query.list_category_topic_ids(c).uniq return if results == existing diff --git a/app/models/category_featured_user.rb b/app/models/category_featured_user.rb index fb75cca87b..73ac6b1516 100644 --- a/app/models/category_featured_user.rb +++ b/app/models/category_featured_user.rb @@ -30,7 +30,7 @@ class CategoryFeaturedUser < ActiveRecord::Base LIMIT :max_featured_users; ", category_id: category_id, max_featured_users: max_featured_users - user_ids = most_recent_user_ids.map{|uc| uc['user_id'].to_i} + user_ids = most_recent_user_ids.map { |uc| uc['user_id'].to_i } current = CategoryFeaturedUser.where(category_id: category_id).order(:id).pluck(:user_id) return if current == user_ids diff --git a/app/models/category_list.rb b/app/models/category_list.rb index 39507a2a8a..f3ae8a1d00 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -3,13 +3,16 @@ require_dependency 'pinned_check' class CategoryList include ActiveModel::Serialization + cattr_accessor :preloaded_topic_custom_fields + self.preloaded_topic_custom_fields = Set.new + attr_accessor :categories, :uncategorized, :draft, :draft_key, :draft_sequence - def initialize(guardian=nil, options={}) + def initialize(guardian = nil, options = {}) @guardian = guardian || Guardian.new @options = options @@ -20,6 +23,19 @@ class CategoryList find_user_data sort_unpinned trim_results + + if preloaded_topic_custom_fields.present? + displayable_topics = @categories.map(&:displayable_topics) + displayable_topics.flatten! + displayable_topics.compact! + + if displayable_topics.present? + Topic.preload_custom_fields( + displayable_topics, + preloaded_topic_custom_fields + ) + end + end end def preload_key @@ -63,9 +79,9 @@ class CategoryList @categories = @categories.order(:position, :id) else @categories = @categories.order('COALESCE(categories.posts_week, 0) DESC') - .order('COALESCE(categories.posts_month, 0) DESC') - .order('COALESCE(categories.posts_year, 0) DESC') - .order('id ASC') + .order('COALESCE(categories.posts_month, 0) DESC') + .order('COALESCE(categories.posts_year, 0) DESC') + .order('id ASC') end @categories = @categories.to_a diff --git a/app/models/category_user.rb b/app/models/category_user.rb index 1cb6f502b3..8d0cf15912 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -62,7 +62,7 @@ class CategoryUser < ActiveRecord::Base auto_track(user_id: user.id) end - def self.auto_track(opts={}) + def self.auto_track(opts = {}) builder = SqlBuilder.new < "#{name}CustomField" + has_many :_custom_fields, dependent: :destroy, class_name: "#{name}CustomField" after_save :save_custom_fields attr_accessor :preloaded_custom_fields @@ -58,7 +58,7 @@ module HasCustomFields return result if whitelisted_fields.blank? klass.where(foreign_key => ids, :name => whitelisted_fields) - .pluck(foreign_key, :name, :value).each do |cf| + .pluck(foreign_key, :name, :value).each do |cf| result[cf[0]] ||= {} append_custom_field(result[cf[0]], cf[1], cf[2]) end @@ -96,7 +96,7 @@ module HasCustomFields .where("name in (?)", fields) .pluck(fk, :name, :value).each do |id, name, value| - preloaded = map[id].preloaded_custom_fields + preloaded = map[id].preloaded_custom_fields if preloaded[name].nil? preloaded.delete(name) @@ -156,7 +156,7 @@ module HasCustomFields !@custom_fields || @custom_fields_orig == @custom_fields end - def save_custom_fields(force=false) + def save_custom_fields(force = false) if force || !custom_fields_clean? dup = @custom_fields.dup @@ -194,7 +194,7 @@ module HasCustomFields end end - dup.each do |k,v| + dup.each do |k, v| if v.is_a? Array v.each { |subv| _custom_fields.create(name: k, value: subv) } elsif v.is_a? Hash @@ -212,7 +212,7 @@ module HasCustomFields def refresh_custom_fields_from_db target = Hash.new - _custom_fields.pluck(:name,:value).each do |key, value| + _custom_fields.pluck(:name, :value).each do |key, value| self.class.append_custom_field(target, key, value) end @custom_fields_orig = target diff --git a/app/models/concerns/positionable.rb b/app/models/concerns/positionable.rb index 335b8ce0ac..68e71f1b14 100644 --- a/app/models/concerns/positionable.rb +++ b/app/models/concerns/positionable.rb @@ -11,18 +11,18 @@ module Positionable position = [[position_arg, 0].max, self.class.count - 1].min - if self.position.nil? or position > self.position + if self.position.nil? || position > (self.position) self.exec_sql " UPDATE #{self.class.table_name} SET position = position - 1 WHERE position > :current_position and position <= :new_position", - {current_position: self.position, new_position: position} + current_position: self.position, new_position: position elsif position < self.position self.exec_sql " UPDATE #{self.class.table_name} SET position = position + 1 WHERE position >= :new_position and position < :current_position", - {current_position: self.position, new_position: position} + current_position: self.position, new_position: position else # Not moving to a new position return @@ -31,6 +31,6 @@ module Positionable self.exec_sql " UPDATE #{self.class.table_name} SET position = :position - WHERE id = :id", {id: id, position: position} + WHERE id = :id", id: id, position: position end end diff --git a/app/models/concerns/trashable.rb b/app/models/concerns/trashable.rb index 9325f6849e..b758df0027 100644 --- a/app/models/concerns/trashable.rb +++ b/app/models/concerns/trashable.rb @@ -8,7 +8,6 @@ module Trashable belongs_to :deleted_by, class_name: 'User' end - module ClassMethods def with_deleted # lifted from acts_as_paranoid, works around https://github.com/rails/rails/issues/4306 @@ -30,7 +29,7 @@ module Trashable deleted_at.present? end - def trash!(trashed_by=nil) + def trash!(trashed_by = nil) # note, an argument could be made that the column should probably called trashed_at # however, deleted_at is the terminology used in the UI # @@ -44,7 +43,6 @@ module Trashable trash_update(nil, nil) end - private def trash_update(deleted_at, deleted_by_id) diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index 9bdcdf50ac..b877705d70 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -22,7 +22,7 @@ class DirectoryItem < ActiveRecord::Base end def self.refresh! - period_types.each_key {|p| refresh_period!(p)} + period_types.each_key { |p| refresh_period!(p) } end def self.refresh_period!(period_type) @@ -30,14 +30,15 @@ class DirectoryItem < ActiveRecord::Base # Don't calculate it if the user directory is disabled return unless SiteSetting.enable_user_directory? - since = case period_type - when :daily then 1.day.ago - when :weekly then 1.week.ago - when :monthly then 1.month.ago - when :quarterly then 3.months.ago - when :yearly then 1.year.ago - else 1000.years.ago - end + since = + case period_type + when :daily then 1.day.ago + when :weekly then 1.week.ago + when :monthly then 1.month.ago + when :quarterly then 3.months.ago + when :yearly then 1.year.ago + else 1000.years.ago + end ActiveRecord::Base.transaction do exec_sql "DELETE FROM directory_items @@ -47,7 +48,6 @@ class DirectoryItem < ActiveRecord::Base u.id IS NULL AND di.period_type = :period_type", period_type: period_types[period_type] - exec_sql "INSERT INTO directory_items(period_type, user_id, likes_received, likes_given, topics_entered, days_visited, posts_read, topic_count, post_count) SELECT :period_type, @@ -117,7 +117,6 @@ class DirectoryItem < ActiveRecord::Base reply_type: UserAction::REPLY, regular_post_type: Post.types[:regular] - if period_type == :all exec_sql < 0 GroupUser - .where(user_id: user.id) - .where('group_id IN (SELECT id FROM groups WHERE name in (?))',split) - .destroy_all + .where(user_id: user.id) + .where('group_id IN (SELECT id FROM groups WHERE name in (?))', split) + .destroy_all end end end diff --git a/app/models/draft.rb b/app/models/draft.rb index 437ad298fe..be412cc72a 100644 --- a/app/models/draft.rb +++ b/app/models/draft.rb @@ -4,7 +4,7 @@ class Draft < ActiveRecord::Base EXISTING_TOPIC = 'topic_' def self.set(user, key, sequence, data) - d = find_draft(user,key) + d = find_draft(user, key) if d return if d.sequence > sequence exec_sql("UPDATE drafts @@ -18,14 +18,14 @@ class Draft < ActiveRecord::Base end def self.get(user, key, sequence) - d = find_draft(user,key) + d = find_draft(user, key) if d && d.sequence == sequence d.data end end def self.clear(user, key, sequence) - d = find_draft(user,key) + d = find_draft(user, key) if d && d.sequence <= sequence d.destroy end diff --git a/app/models/draft_sequence.rb b/app/models/draft_sequence.rb index af4eb647c9..2e8eeb98c8 100644 --- a/app/models/draft_sequence.rb +++ b/app/models/draft_sequence.rb @@ -1,5 +1,5 @@ class DraftSequence < ActiveRecord::Base - def self.next!(user,key) + def self.next!(user, key) user_id = user user_id = user.id unless user.is_a?(Integer) diff --git a/app/models/email_log.rb b/app/models/email_log.rb index d17ec8c3a6..3add7e1c35 100644 --- a/app/models/email_log.rb +++ b/app/models/email_log.rb @@ -39,21 +39,21 @@ class EmailLog < ActiveRecord::Base end end - def self.reached_max_emails?(user, email_type=nil) + def self.reached_max_emails?(user, email_type = nil) return false if SiteSetting.max_emails_per_day_per_user == 0 || CRITICAL_EMAIL_TYPES.include?(email_type) count = sent.where('created_at > ?', 1.day.ago) - .where(user_id: user.id) - .count + .where(user_id: user.id) + .count count >= SiteSetting.max_emails_per_day_per_user end def self.count_per_day(start_date, end_date) sent.where("created_at BETWEEN ? AND ?", start_date, end_date) - .group("DATE(created_at)") - .order("DATE(created_at)") - .count + .group("DATE(created_at)") + .order("DATE(created_at)") + .count end def self.for(reply_key) @@ -62,9 +62,9 @@ class EmailLog < ActiveRecord::Base def self.last_sent_email_address self.where(email_type: "signup") - .order(created_at: :desc) - .first - .try(:to_address) + .order(created_at: :desc) + .first + .try(:to_address) end end diff --git a/app/models/email_token.rb b/app/models/email_token.rb index 225e083b54..2a10dd4e23 100644 --- a/app/models/email_token.rb +++ b/app/models/email_token.rb @@ -11,8 +11,8 @@ class EmailToken < ActiveRecord::Base after_create do # Expire the previous tokens EmailToken.where(user_id: self.user_id) - .where("id != ?", self.id) - .update_all(expired: true) + .where("id != ?", self.id) + .update_all(expired: true) end def self.token_length @@ -36,7 +36,7 @@ class EmailToken < ActiveRecord::Base end def self.valid_token_format?(token) - token.present? && token =~ /\h{#{token.length/2}}/i + token.present? && token =~ /\h{#{token.length / 2}}/i end def self.atomic_confirm(token) @@ -71,7 +71,7 @@ class EmailToken < ActiveRecord::Base end if user - return User.find_by(email: Email.downcase(user.email)) if Invite.redeem_from_email(user.email).present? + return User.find_by_email(user.email) if Invite.redeem_from_email(user.email).present? user end end @@ -81,10 +81,10 @@ class EmailToken < ActiveRecord::Base def self.confirmable(token) EmailToken.where(token: token) - .where(expired: false, confirmed: false) - .where("created_at >= ?", EmailToken.valid_after) - .includes(:user) - .first + .where(expired: false, confirmed: false) + .where("created_at >= ?", EmailToken.valid_after) + .includes(:user) + .first end end diff --git a/app/models/embedding.rb b/app/models/embedding.rb index f11de96f4f..a0be5ceea8 100644 --- a/app/models/embedding.rb +++ b/app/models/embedding.rb @@ -38,7 +38,7 @@ class Embedding < OpenStruct def self.find embedding_args = { id: 'default' } - Embedding.settings.each {|s| embedding_args[s] = SiteSetting.send(s) } + Embedding.settings.each { |s| embedding_args[s] = SiteSetting.send(s) } Embedding.new(embedding_args) end end diff --git a/app/models/emoji.rb b/app/models/emoji.rb index 0e81f2aa0f..c6f2e490cf 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -1,6 +1,6 @@ class Emoji # update this to clear the cache - EMOJI_VERSION = "v5" + EMOJI_VERSION = "5" FITZPATRICK_SCALE ||= [ "1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff" ] @@ -46,15 +46,19 @@ class Emoji def self.create_from_db_item(emoji) name = emoji["name"] - filename = "#{emoji['filename'] || name}.png" + filename = emoji['filename'] || name Emoji.new.tap do |e| e.name = name - e.url = "#{Discourse.base_uri}/images/emoji/#{SiteSetting.emoji_set}/#{filename}" + e.url = Emoji.url_for(filename) end end + def self.url_for(name) + "#{Discourse.base_uri}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}" + end + def self.cache_key(name) - "#{name}:#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}" + "#{name}:v#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}" end def self.clear_cache @@ -74,7 +78,7 @@ class Emoji end def self.load_standard - db['emojis'].map {|e| Emoji.create_from_db_item(e) } + db['emojis'].map { |e| Emoji.create_from_db_item(e) } end def self.load_custom @@ -131,7 +135,7 @@ class Emoji if is_tonable_emojis.include?(name) fitzpatrick_scales.each_with_index do |scale, index| toned_code = code.codepoints.insert(1, scale).pack("U*".freeze) - @unicode_replacements[toned_code] = "#{name}:t#{index+2}" + @unicode_replacements[toned_code] = "#{name}:t#{index + 2}" end end end @@ -155,6 +159,12 @@ class Emoji end.join end + def self.gsub_emoji_to_unicode(str) + if str + str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } + end + end + def self.lookup_unicode(name) @reverse_map ||= begin map = {} @@ -170,7 +180,7 @@ class Emoji if is_tonable_emojis.include?(e['name']) FITZPATRICK_SCALE.each_with_index do |scale, index| toned_code = (code.codepoints.insert(1, scale.to_i(16))).pack("U*") - map["#{e['name']}:t#{index+2}"] = toned_code + map["#{e['name']}:t#{index + 2}"] = toned_code end end end diff --git a/app/models/given_daily_like.rb b/app/models/given_daily_like.rb index 87da419453..ccc2974237 100644 --- a/app/models/given_daily_like.rb +++ b/app/models/given_daily_like.rb @@ -16,8 +16,8 @@ class GivenDailyLike < ActiveRecord::Base create(user_id: user_id, given_date: given_date, likes_given: 1) else find_for(user_id, given_date) - .where('limit_reached = false AND likes_given >= :limit', limit: SiteSetting.max_likes_per_day) - .update_all(limit_reached: true) + .where('limit_reached = false AND likes_given >= :limit', limit: SiteSetting.max_likes_per_day) + .update_all(limit_reached: true) end end @@ -28,8 +28,8 @@ class GivenDailyLike < ActiveRecord::Base find_for(user_id, given_date).update_all('likes_given = likes_given - 1') find_for(user_id, given_date) - .where('limit_reached = true AND likes_given < :limit', limit: SiteSetting.max_likes_per_day) - .update_all(limit_reached: false) + .where('limit_reached = true AND likes_given < :limit', limit: SiteSetting.max_likes_per_day) + .update_all(limit_reached: false) end end diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index 2ee90b7bc1..84750ecc69 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -40,7 +40,7 @@ class GlobalSetting token = $redis.without_namespace.get(REDIS_SECRET_KEY) unless token && token =~ VALID_SECRET_KEY token = SecureRandom.hex(64) - $redis.without_namespace.set(REDIS_SECRET_KEY,token) + $redis.without_namespace.set(REDIS_SECRET_KEY, token) end end if !secret_key_base.blank? && token != secret_key_base @@ -74,7 +74,7 @@ class GlobalSetting end def self.database_config - hash = {"adapter" => "postgresql"} + hash = { "adapter" => "postgresql" } %w{pool timeout socket host port username password replica_host replica_port}.each do |s| if val = self.send("db_#{s}") hash[s] = val @@ -91,7 +91,7 @@ class GlobalSetting hash["prepared_statements"] = !!self.db_prepared_statements - {"production" => hash} + { "production" => hash } end # For testing purposes @@ -118,8 +118,8 @@ class GlobalSetting if redis_sentinels.present? c[:sentinels] = redis_sentinels.split(",").map do |address| - host,port = address.split(":") - {host: host, port: port} + host, port = address.split(":") + { host: host, port: port } end.to_a end @@ -134,7 +134,6 @@ class GlobalSetting setting end - def resolve(current, default) BaseProvider.coerce( if current.present? @@ -167,8 +166,7 @@ class GlobalSetting end end - - def lookup(key,default) + def lookup(key, default) var = @data[key] resolve(var, var.nil? ? default : "") end @@ -193,7 +191,7 @@ class GlobalSetting end def keys - ENV.keys.select{|k| k =~ /^DISCOURSE_/}.map{|k| k[10..-1].downcase.to_sym} + ENV.keys.select { |k| k =~ /^DISCOURSE_/ }.map { |k| k[10..-1].downcase.to_sym } end end @@ -207,7 +205,6 @@ class GlobalSetting end end - class << self attr_accessor :provider end diff --git a/app/models/group.rb b/app/models/group.rb index 423b535b6a..f137ec8931 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -37,33 +37,33 @@ class Group < ActiveRecord::Base end validate :name_format_validator - validates_uniqueness_of :name, case_sensitive: false + validates :name, presence: true, uniqueness: { case_sensitive: false } validate :automatic_membership_email_domains_format_validator validate :incoming_email_validator - validate :can_allow_membership_requests - validates :flair_url, url: true, if: Proc.new { |g| g.flair_url && g.flair_url[0,3] != 'fa-' } + validate :can_allow_membership_requests, if: :allow_membership_requests + validates :flair_url, url: true, if: Proc.new { |g| g.flair_url && g.flair_url[0, 3] != 'fa-' } AUTO_GROUPS = { - :everyone => 0, - :admins => 1, - :moderators => 2, - :staff => 3, - :trust_level_0 => 10, - :trust_level_1 => 11, - :trust_level_2 => 12, - :trust_level_3 => 13, - :trust_level_4 => 14 + everyone: 0, + admins: 1, + moderators: 2, + staff: 3, + trust_level_0: 10, + trust_level_1: 11, + trust_level_2: 12, + trust_level_3: 13, + trust_level_4: 14 } AUTO_GROUP_IDS = Hash[*AUTO_GROUPS.to_a.flatten.reverse] STAFF_GROUPS = [:admins, :moderators, :staff] ALIAS_LEVELS = { - :nobody => 0, - :only_admins => 1, - :mods_and_admins => 2, - :members_mods_and_admins => 3, - :everyone => 99 + nobody: 0, + only_admins: 1, + mods_and_admins: 2, + members_mods_and_admins: 3, + everyone: 99 } def self.visibility_levels @@ -75,13 +75,13 @@ class Group < ActiveRecord::Base ) end - validates :alias_level, inclusion: { in: ALIAS_LEVELS.values} + validates :alias_level, inclusion: { in: ALIAS_LEVELS.values } scope :visible_groups, ->(user) { groups = Group.order(name: :asc).where("groups.id > 0") unless user&.admin - sql = <<~SQL + sql = <<~SQL groups.id IN ( SELECT g.id FROM groups g WHERE g.visibility_level = :public @@ -121,7 +121,7 @@ class Group < ActiveRecord::Base groups } - scope :mentionable, lambda {|user| + scope :mentionable, lambda { |user| levels = [ALIAS_LEVELS[:everyone]] @@ -140,7 +140,7 @@ class Group < ActiveRecord::Base ( alias_level = #{ALIAS_LEVELS[:members_mods_and_admins]} AND id in ( SELECT group_id FROM group_users WHERE user_id = :user_id) - )", levels: levels, user_id: user && user.id ) + )", levels: levels, user_id: user && user.id) } def downcase_incoming_email @@ -168,38 +168,38 @@ class Group < ActiveRecord::Base end end - def posts_for(guardian, before_post_id=nil) + def posts_for(guardian, before_post_id = nil) user_ids = group_users.map { |gu| gu.user_id } result = Post.includes(:user, :topic, topic: :category) - .references(:posts, :topics, :category) - .where(user_id: user_ids) - .where('topics.archetype <> ?', Archetype.private_message) - .where(post_type: Post.types[:regular]) + .references(:posts, :topics, :category) + .where(user_id: user_ids) + .where('topics.archetype <> ?', Archetype.private_message) + .where(post_type: Post.types[:regular]) result = guardian.filter_allowed_categories(result) result = result.where('posts.id < ?', before_post_id) if before_post_id result.order('posts.created_at desc') end - def messages_for(guardian, before_post_id=nil) + def messages_for(guardian, before_post_id = nil) result = Post.includes(:user, :topic, topic: :category) - .references(:posts, :topics, :category) - .where('topics.archetype = ?', Archetype.private_message) - .where(post_type: Post.types[:regular]) - .where('topics.id IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id = ?)', self.id) + .references(:posts, :topics, :category) + .where('topics.archetype = ?', Archetype.private_message) + .where(post_type: Post.types[:regular]) + .where('topics.id IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id = ?)', self.id) result = guardian.filter_allowed_categories(result) result = result.where('posts.id < ?', before_post_id) if before_post_id result.order('posts.created_at desc') end - def mentioned_posts_for(guardian, before_post_id=nil) + def mentioned_posts_for(guardian, before_post_id = nil) result = Post.joins(:group_mentions) - .includes(:user, :topic, topic: :category) - .references(:posts, :topics, :category) - .where('topics.archetype <> ?', Archetype.private_message) - .where(post_type: Post.types[:regular]) - .where('group_mentions.group_id = ?', self.id) + .includes(:user, :topic, topic: :category) + .references(:posts, :topics, :category) + .where('topics.archetype <> ?', Archetype.private_message) + .where(post_type: Post.types[:regular]) + .where('group_mentions.group_id = ?', self.id) result = guardian.filter_allowed_categories(result) result = result.where('posts.id < ?', before_post_id) if before_post_id @@ -224,12 +224,9 @@ class Group < ActiveRecord::Base localized_name = I18n.t("groups.default_names.#{name}").downcase validator = UsernameValidator.new(localized_name) - group.name = - if !Group.where("LOWER(name) = ?", localized_name).exists? && validator.valid_format? - localized_name - else - name - end + if !Group.where("LOWER(name) = ?", localized_name).exists? && validator.valid_format? + group.name = localized_name + end # the everyone group is special, it can include non-users so there is no # way to have the membership in a table @@ -240,16 +237,17 @@ class Group < ActiveRecord::Base end # Remove people from groups they don't belong in. - remove_subquery = case name - when :admins - "SELECT id FROM users WHERE NOT admin" - when :moderators - "SELECT id FROM users WHERE NOT moderator" - when :staff - "SELECT id FROM users WHERE NOT admin AND NOT moderator" - when :trust_level_0, :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4 - "SELECT id FROM users WHERE trust_level < #{id - 10}" - end + remove_subquery = + case name + when :admins + "SELECT id FROM users WHERE NOT admin" + when :moderators + "SELECT id FROM users WHERE NOT moderator" + when :staff + "SELECT id FROM users WHERE NOT admin AND NOT moderator" + when :trust_level_0, :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4 + "SELECT id FROM users WHERE trust_level < #{id - 10}" + end exec_sql <<-SQL DELETE FROM group_users @@ -259,18 +257,19 @@ class Group < ActiveRecord::Base SQL # Add people to groups - insert_subquery = case name - when :admins - "SELECT id FROM users WHERE admin" - when :moderators - "SELECT id FROM users WHERE moderator" - when :staff - "SELECT id FROM users WHERE moderator OR admin" - when :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4 - "SELECT id FROM users WHERE trust_level >= #{id - 10}" - when :trust_level_0 - "SELECT id FROM users" - end + insert_subquery = + case name + when :admins + "SELECT id FROM users WHERE admin" + when :moderators + "SELECT id FROM users WHERE moderator" + when :staff + "SELECT id FROM users WHERE moderator OR admin" + when :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4 + "SELECT id FROM users WHERE trust_level >= #{id - 10}" + when :trust_level_0 + "SELECT id FROM users" + end exec_sql <<-SQL INSERT INTO group_users (group_id, user_id, created_at, updated_at) @@ -341,22 +340,19 @@ class Group < ActiveRecord::Base end end - def self.lookup_group_ids(opts) - if group_ids = opts[:group_ids] - group_ids = group_ids.split(",").map(&:to_i) - group_ids = Group.where(id: group_ids).pluck(:id) + def self.lookup_groups(group_ids: [], group_names: []) + if group_ids.present? + group_ids = group_ids.split(",") + group_ids.map!(&:to_i) + groups = Group.where(id: group_ids) if group_ids.present? end - group_ids ||= [] - - if group_names = opts[:group_names] + if group_names.present? group_names = group_names.split(",") - if group_names.present? - group_ids += Group.where(name: group_names).pluck(:id) - end + groups = (groups || Group).where(name: group_names) if group_names.present? end - group_ids + groups || [] end def self.desired_trust_level_groups(trust_level) @@ -394,11 +390,11 @@ class Group < ActiveRecord::Base additions = expected - current deletions = current - expected - map = Hash[*User.where(username: additions+deletions) - .select('id,username') - .map{|u| [u.username,u.id]}.flatten] + map = Hash[*User.where(username: additions + deletions) + .select('id,username') + .map { |u| [u.username, u.id] }.flatten] - deletions = Set.new(deletions.map{|d| map[d]}) + deletions = Set.new(deletions.map { |d| map[d] }) @deletions = [] group_users.each do |gu| @@ -432,9 +428,9 @@ class Group < ActiveRecord::Base def add_owner(user) if group_user = self.group_users.find_by(user: user) - group_user.update_attributes!(owner: true) if !group_user.owner + group_user.update!(owner: true) if !group_user.owner else - GroupUser.create!(user: user, group: self, owner: true) + self.group_users.create!(user: user, owner: true) end end @@ -443,32 +439,51 @@ class Group < ActiveRecord::Base end def bulk_add(user_ids) - if user_ids.present? - Group.exec_sql("INSERT INTO group_users - (group_id, user_id, created_at, updated_at) - SELECT #{self.id}, - u.id, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - FROM users AS u - WHERE u.id IN (#{user_ids.join(', ')}) - AND NOT EXISTS(SELECT 1 FROM group_users AS gu - WHERE gu.user_id = u.id AND - gu.group_id = #{self.id})") + return unless user_ids.present? + + Group.transaction do + sql = <<~SQL + INSERT INTO group_users + (group_id, user_id, created_at, updated_at) + SELECT + #{self.id}, + u.id, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + FROM users AS u + WHERE u.id IN (:user_ids) + AND NOT EXISTS ( + SELECT 1 FROM group_users AS gu + WHERE gu.user_id = u.id AND + gu.group_id = :group_id + ) + SQL + + Group.exec_sql(sql, group_id: self.id, user_ids: user_ids) + + user_attributes = {} if self.primary_group? - User.where(id: user_ids).update_all(primary_group_id: self.id) + user_attributes[:primary_group_id] = self.id end if self.title.present? - User.where(id: user_ids).update_all(title: self.title) + user_attributes[:title] = self.title end - if self.grant_trust_level.present? - Jobs.enqueue(:bulk_grant_trust_level, user_ids: user_ids, trust_level: self.grant_trust_level) + if user_attributes.present? + User.where(id: user_ids).update_all(user_attributes) end end - true + + if self.grant_trust_level.present? + Jobs.enqueue(:bulk_grant_trust_level, + user_ids: user_ids, + trust_level: self.grant_trust_level + ) + end + + self end def staff? @@ -478,6 +493,7 @@ class Group < ActiveRecord::Base protected def name_format_validator + self.name.strip! UsernameValidator.perform_validation(self, 'name') end @@ -558,7 +574,16 @@ class Group < ActiveRecord::Base private def can_allow_membership_requests - if self.allow_membership_requests && !self.group_users.where(owner: true).exists? + valid = true + + valid = + if self.persisted? + self.group_users.where(owner: true).exists? + else + self.group_users.any?(&:owner) + end + + if !valid self.errors.add(:base, I18n.t('groups.errors.cant_allow_membership_requests')) end end @@ -594,11 +619,12 @@ end # flair_color :string # bio_raw :text # bio_cooked :text -# public :boolean default(FALSE), not null +# public_admission :boolean default(FALSE), not null # allow_membership_requests :boolean default(FALSE), not null # full_name :string # default_notification_level :integer default(3), not null -# visibility_level :integer default(0) +# visibility_level :integer default(0), not null +# public_exit :boolean default(FALSE), not null # # Indexes # diff --git a/app/models/group_archived_message.rb b/app/models/group_archived_message.rb index 190dce298c..91d2062cae 100644 --- a/app/models/group_archived_message.rb +++ b/app/models/group_archived_message.rb @@ -5,22 +5,21 @@ class GroupArchivedMessage < ActiveRecord::Base def self.move_to_inbox!(group_id, topic_id) GroupArchivedMessage.where(group_id: group_id, topic_id: topic_id).destroy_all trigger(:move_to_inbox, group_id, topic_id) - MessageBus.publish("/topic/#{topic_id}", {type: "move_to_inbox"}, group_ids: [group_id]) + MessageBus.publish("/topic/#{topic_id}", { type: "move_to_inbox" }, group_ids: [group_id]) end def self.archive!(group_id, topic_id) GroupArchivedMessage.where(group_id: group_id, topic_id: topic_id).destroy_all GroupArchivedMessage.create!(group_id: group_id, topic_id: topic_id) trigger(:archive_message, group_id, topic_id) - MessageBus.publish("/topic/#{topic_id}", {type: "archived"}, group_ids: [group_id]) + MessageBus.publish("/topic/#{topic_id}", { type: "archived" }, group_ids: [group_id]) end - def self.trigger(event, group_id, topic_id) group = Group.find_by(id: group_id) topic = Topic.find_by(id: topic_id) if group && topic - DiscourseEvent.trigger(event, {group: group, topic: topic}) + DiscourseEvent.trigger(event, group: group, topic: topic) end end diff --git a/app/models/group_user.rb b/app/models/group_user.rb index b559580390..3c40b07928 100644 --- a/app/models/group_user.rb +++ b/app/models/group_user.rb @@ -25,36 +25,41 @@ class GroupUser < ActiveRecord::Base def set_primary_group if group.primary_group - self.class.exec_sql("UPDATE users - SET primary_group_id = :id - WHERE id = :user_id", - id: group.id, user_id: user_id) + self.class.exec_sql(" + UPDATE users + SET primary_group_id = :id + WHERE id = :user_id", + id: group.id, user_id: user_id + ) end end def remove_primary_group - self.class.exec_sql("UPDATE users - SET primary_group_id = NULL - WHERE id = :user_id AND primary_group_id = :id", - id: group.id, user_id: user_id) - + self.class.exec_sql(" + UPDATE users + SET primary_group_id = NULL + WHERE id = :user_id AND primary_group_id = :id", + id: group.id, user_id: user_id + ) end def remove_title if group.title.present? - self.class.exec_sql("UPDATE users SET title = NULL - WHERE title = :title AND id = :id", - id: user_id, - title: group.title) + self.class.exec_sql(" + UPDATE users SET title = NULL + WHERE title = :title AND id = :id", + id: user_id, title: group.title + ) end end def update_title if group.title.present? - self.class.exec_sql("UPDATE users SET title = :title - WHERE (title IS NULL OR title = '') AND id = :id", - id: user_id, - title: group.title) + self.class.exec_sql(" + UPDATE users SET title = :title + WHERE (title IS NULL OR title = '') AND id = :id", + id: user_id, title: group.title + ) end end diff --git a/app/models/incoming_link.rb b/app/models/incoming_link.rb index fd3e9e8e8d..e7992e2c72 100644 --- a/app/models/incoming_link.rb +++ b/app/models/incoming_link.rb @@ -35,11 +35,10 @@ class IncomingLink < ActiveRecord::Base post_id = opts[:post_id] post_id ||= Post.where(topic_id: opts[:topic_id], post_number: opts[:post_number] || 1) - .pluck(:id).first + .pluck(:id).first cid = current_user ? (current_user.id) : (nil) - unless cid && cid == user_id create(referer: referer, @@ -53,7 +52,6 @@ class IncomingLink < ActiveRecord::Base end - def referer=(referer) self.incoming_referer_id = nil @@ -87,7 +85,6 @@ class IncomingLink < ActiveRecord::Base end end - # Internal: Update appropriate link counts. def update_link_counts exec_sql("UPDATE topics diff --git a/app/models/incoming_links_report.rb b/app/models/incoming_links_report.rb index 388ea57cc9..cf28011b8d 100644 --- a/app/models/incoming_links_report.rb +++ b/app/models/incoming_links_report.rb @@ -30,24 +30,24 @@ class IncomingLinksReport # Return top 10 users who brought traffic to the site within the last 30 days def self.report_top_referrers(report) - report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") + report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") - num_clicks = link_count_per_user + num_clicks = link_count_per_user num_topics = topic_count_per_user - user_id_lookup = User.where(username: num_clicks.keys).select(:id, :username).inject({}) {|sum,v| sum[v.username] = v.id; sum;} + user_id_lookup = User.where(username: num_clicks.keys).select(:id, :username).inject({}) { |sum, v| sum[v.username] = v.id; sum; } report.data = [] num_clicks.each_key do |username| - report.data << {username: username, user_id: user_id_lookup[username], num_clicks: num_clicks[username], num_topics: num_topics[username]} + report.data << { username: username, user_id: user_id_lookup[username], num_clicks: num_clicks[username], num_topics: num_topics[username] } end - report.data = report.data.sort_by {|x| x[:num_clicks]}.reverse[0,10] + report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10] end def self.per_user @per_user_query ||= IncomingLink - .where('incoming_links.created_at > ? AND incoming_links.user_id IS NOT NULL', 30.days.ago) - .joins(:user) - .group('users.username') + .where('incoming_links.created_at > ? AND incoming_links.user_id IS NOT NULL', 30.days.ago) + .joins(:user) + .group('users.username') end def self.link_count_per_user @@ -58,35 +58,34 @@ class IncomingLinksReport per_user.joins(:post).count("DISTINCT posts.topic_id") end - # Return top 10 domains that brought traffic to the site within the last 30 days def self.report_top_traffic_sources(report) - report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") + report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") report.y_titles[:num_users] = I18n.t("reports.#{report.type}.num_users") - num_clicks = link_count_per_domain + num_clicks = link_count_per_domain num_topics = topic_count_per_domain(num_clicks.keys) report.data = [] num_clicks.each_key do |domain| - report.data << {domain: domain, num_clicks: num_clicks[domain], num_topics: num_topics[domain]} + report.data << { domain: domain, num_clicks: num_clicks[domain], num_topics: num_topics[domain] } end - report.data = report.data.sort_by {|x| x[:num_clicks]}.reverse[0,10] + report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10] end - def self.link_count_per_domain(limit=10) + def self.link_count_per_domain(limit = 10) IncomingLink.where('incoming_links.created_at > ?', 30.days.ago) - .joins(:incoming_referer => :incoming_domain) - .group('incoming_domains.name') - .order('count_all DESC') - .limit(limit).count + .joins(incoming_referer: :incoming_domain) + .group('incoming_domains.name') + .order('count_all DESC') + .limit(limit).count end def self.per_domain(domains) IncomingLink - .joins(:incoming_referer => :incoming_domain) - .where('incoming_links.created_at > ? AND incoming_domains.name IN (?)', 30.days.ago, domains) - .group('incoming_domains.name') + .joins(incoming_referer: :incoming_domain) + .where('incoming_links.created_at > ? AND incoming_domains.name IN (?)', 30.days.ago, domains) + .group('incoming_domains.name') end def self.topic_count_per_domain(domains) @@ -94,17 +93,16 @@ class IncomingLinksReport per_domain(domains).joins(:post).count("DISTINCT posts.topic_id") end - def self.report_top_referred_topics(report) - report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") - num_clicks = link_count_per_topic - num_clicks = num_clicks.to_a.sort_by {|x| x[1]}.last(10).reverse # take the top 10 + report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") + num_clicks = link_count_per_topic + num_clicks = num_clicks.to_a.sort_by { |x| x[1] }.last(10).reverse # take the top 10 report.data = [] - topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map {|z| z[0]}) + topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map { |z| z[0] }) num_clicks.each do |topic_id, num_clicks_element| - topic = topics.find {|t| t.id == topic_id} + topic = topics.find { |t| t.id == topic_id } if topic - report.data << {topic_id: topic_id, topic_title: topic.title, topic_slug: topic.slug, num_clicks: num_clicks_element} + report.data << { topic_id: topic_id, topic_title: topic.title, topic_slug: topic.slug, num_clicks: num_clicks_element } end end report.data @@ -112,8 +110,8 @@ class IncomingLinksReport def self.link_count_per_topic IncomingLink.joins(:post) - .where('incoming_links.created_at > ? AND topic_id IS NOT NULL', 30.days.ago) - .group('topic_id') - .count + .where('incoming_links.created_at > ? AND topic_id IS NOT NULL', 30.days.ago) + .group('topic_id') + .count end end diff --git a/app/models/invite.rb b/app/models/invite.rb index 9925b335b3..1955654c2d 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -32,8 +32,9 @@ class Invite < ActiveRecord::Base def user_doesnt_already_exist @email_already_exists = false return if email.blank? - u = User.find_by("email = ?", Email.downcase(email)) - if u && u.id != self.user_id + user = User.find_by_email(email) + + if user && user.id != self.user_id @email_already_exists = true errors.add(:email) end @@ -69,48 +70,47 @@ class Invite < ActiveRecord::Base end end - def self.invite_by_email(email, invited_by, topic=nil, group_ids=nil, custom_message=nil) - create_invite_by_email(email, invited_by, { + def self.invite_by_email(email, invited_by, topic = nil, group_ids = nil, custom_message = nil) + create_invite_by_email(email, invited_by, topic: topic, group_ids: group_ids, custom_message: custom_message, send_email: true - }) + ) end # generate invite link - def self.generate_invite_link(email, invited_by, topic=nil, group_ids=nil) - invite = create_invite_by_email(email, invited_by, { + def self.generate_invite_link(email, invited_by, topic = nil, group_ids = nil) + invite = create_invite_by_email(email, invited_by, topic: topic, group_ids: group_ids, send_email: false - }) - return "#{Discourse.base_url}/invites/#{invite.invite_key}" if invite + ) + + "#{Discourse.base_url}/invites/#{invite.invite_key}" if invite end # Create an invite for a user, supplying an optional topic # # Return the previously existing invite if already exists. Returns nil if the invite can't be created. - def self.create_invite_by_email(email, invited_by, opts=nil) + def self.create_invite_by_email(email, invited_by, opts = nil) opts ||= {} topic = opts[:topic] group_ids = opts[:group_ids] send_email = opts[:send_email].nil? ? true : opts[:send_email] custom_message = opts[:custom_message] - lower_email = Email.downcase(email) - user = User.find_by(email: lower_email) - if user + if user = User.find_by_email(lower_email) extend_permissions(topic, user, invited_by) if topic raise UserExists.new I18n.t("invite.user_exists", email: lower_email, username: user.username) end invite = Invite.with_deleted - .where(email: lower_email, invited_by_id: invited_by.id) - .order('created_at DESC') - .first + .where(email: lower_email, invited_by_id: invited_by.id) + .order('created_at DESC') + .first if invite && (invite.expired? || invite.deleted_at) invite.destroy @@ -162,23 +162,23 @@ class Invite < ActiveRecord::Base group_ids end - def self.find_all_invites_from(inviter, offset=0, limit=SiteSetting.invites_per_page) + def self.find_all_invites_from(inviter, offset = 0, limit = SiteSetting.invites_per_page) Invite.where(invited_by_id: inviter.id) - .where('invites.email IS NOT NULL') - .includes(:user => :user_stat) - .order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END', + .where('invites.email IS NOT NULL') + .includes(user: :user_stat) + .order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END', 'user_stats.time_read DESC', 'invites.redeemed_at DESC') - .limit(limit) - .offset(offset) - .references('user_stats') + .limit(limit) + .offset(offset) + .references('user_stats') end - def self.find_pending_invites_from(inviter, offset=0) + def self.find_pending_invites_from(inviter, offset = 0) find_all_invites_from(inviter, offset).where('invites.user_id IS NULL').order('invites.created_at DESC') end - def self.find_redeemed_invites_from(inviter, offset=0) + def self.find_redeemed_invites_from(inviter, offset = 0) find_all_invites_from(inviter, offset).where('invites.user_id IS NOT NULL').order('invites.redeemed_at DESC') end @@ -218,16 +218,6 @@ class Invite < ActiveRecord::Base invite end - def self.redeem_from_token(token, email, username=nil, name=nil, topic_id=nil) - invite = Invite.find_by(invite_key: token) - if invite - invite.update_column(:email, email) - invite.topic_invites.create!(invite_id: invite.id, topic_id: topic_id) if topic_id && Topic.find_by_id(topic_id) && !invite.topic_invites.pluck(:topic_id).include?(topic_id) - user = InviteRedeemer.new(invite, username, name).redeem - end - user - end - def resend_invite self.update_columns(created_at: Time.zone.now, updated_at: Time.zone.now) Jobs.enqueue(:invite_email, invite_id: self.id) diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb index df748bf32b..9b68b8b03a 100644 --- a/app/models/invite_redeemer.rb +++ b/app/models/invite_redeemer.rb @@ -18,10 +18,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f end # extracted from User cause it is very specific to invites - def self.create_user_from_invite(invite, username, name, password=nil, user_custom_fields=nil) - user_exists = User.where(admin: false).find_by_email(invite.email) - return user if user_exists - + def self.create_user_from_invite(invite, username, name, password = nil, user_custom_fields = nil) if username && UsernameValidator.new(username).valid_format? && User.username_available?(username) available_username = username else @@ -94,10 +91,9 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f end def get_existing_user - User.find_by(email: invite.email) + User.where(admin: false).find_by_email(invite.email) end - def add_to_private_topics_if_invited invite.topics.private_messages.each do |t| t.topic_allowed_users.create(user_id: invited_user.id) @@ -105,7 +101,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f end def add_user_to_invited_topics - Invite.where('invites.email = ? and invites.id != ?', invite.email, invite.id).includes(:topics).where(topics: {archetype: Archetype::private_message}).each do |i| + Invite.where('invites.email = ? and invites.id != ?', invite.email, invite.id).includes(:topics).where(topics: { archetype: Archetype::private_message }).each do |i| i.topics.each do |t| t.topic_allowed_users.create(user_id: invited_user.id) end @@ -133,8 +129,8 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f def notify_invitee if inviter = invite.invited_by - inviter.notifications.create(notification_type: Notification.types[:invitee_accepted], - data: {display_username: invited_user.username}.to_json) + inviter.notifications.create(notification_type: Notification.types[:invitee_accepted], + data: { display_username: invited_user.username }.to_json) end end diff --git a/app/models/locale_site_setting.rb b/app/models/locale_site_setting.rb index 1cd9246418..607a8e7cdd 100644 --- a/app/models/locale_site_setting.rb +++ b/app/models/locale_site_setting.rb @@ -9,7 +9,7 @@ class LocaleSiteSetting < EnumSiteSetting def self.values supported_locales.map do |l| lang = language_names[l] || language_names[l[0..1]] - {name: lang ? lang['nativeName'] : l, value: l} + { name: lang ? lang['nativeName'] : l, value: l } end end @@ -25,7 +25,7 @@ class LocaleSiteSetting < EnumSiteSetting def self.supported_locales @lock.synchronize do - @supported_locales ||= Dir.glob( File.join(Rails.root, 'config', 'locales', 'client.*.yml') ).map {|x| x.split('.')[-2]}.sort + @supported_locales ||= Dir.glob(File.join(Rails.root, 'config', 'locales', 'client.*.yml')).map { |x| x.split('.')[-2] }.sort end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 7db7a5b8bd..e238bd1f6e 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -8,9 +8,9 @@ class Notification < ActiveRecord::Base validates_presence_of :notification_type scope :unread, lambda { where(read: false) } - scope :recent, lambda { |n=nil| n ||= 10; order('notifications.created_at desc').limit(n) } + scope :recent, lambda { |n = nil| n ||= 10; order('notifications.created_at desc').limit(n) } scope :visible , lambda { joins('LEFT JOIN topics ON notifications.topic_id = topics.id') - .where('topics.id IS NULL OR topics.deleted_at IS NULL') } + .where('topics.id IS NULL OR topics.deleted_at IS NULL') } attr_accessor :skip_send_email @@ -76,20 +76,20 @@ class Notification < ActiveRecord::Base def self.read(user, notification_ids) count = Notification.where(user_id: user.id) - .where(id: notification_ids) - .where(read: false) - .update_all(read: true) + .where(id: notification_ids) + .where(read: false) + .update_all(read: true) user.publish_notifications_state if count > 0 end def self.interesting_after(min_date) - result = where("created_at > ?", min_date) - .includes(:topic) - .visible - .unread - .limit(20) - .order("CASE WHEN notification_type = #{Notification.types[:replied]} THEN 1 + result = where("created_at > ?", min_date) + .includes(:topic) + .visible + .unread + .limit(20) + .order("CASE WHEN notification_type = #{Notification.types[:replied]} THEN 1 WHEN notification_type = #{Notification.types[:mentioned]} THEN 2 ELSE 3 END, created_at DESC").to_a @@ -107,7 +107,7 @@ class Notification < ActiveRecord::Base seen[r.notification_type] << r.topic_id end end - result.reject! {|r| to_remove.include?(r.id) } + result.reject! { |r| to_remove.include?(r.id) } end result @@ -145,9 +145,9 @@ class Notification < ActiveRecord::Base count ||= 10 notifications = user.notifications - .visible - .recent(count) - .includes(:topic) + .visible + .recent(count) + .includes(:topic) if user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:never] notifications = notifications.where('notification_type <> ?', Notification.types[:liked]) @@ -165,7 +165,7 @@ class Notification < ActiveRecord::Base NOT read ORDER BY n.id ASC LIMIT #{count.to_i} - ").values.map do |x,_| + ").values.map do |x, _| x.to_i end @@ -178,7 +178,7 @@ class Notification < ActiveRecord::Base .limit(count) end - notifications.uniq(&:id).sort do |x,y| + notifications.uniq(&:id).sort do |x, y| if x.unread_pm? && !y.unread_pm? -1 elsif y.unread_pm? && !x.unread_pm? diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 52c79698ec..c290f93aca 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -9,7 +9,7 @@ class OptimizedImage < ActiveRecord::Base # BUMP UP if optimized image algorithm changes VERSION = 1 - def self.create_for(upload, width, height, opts={}) + def self.create_for(upload, width, height, opts = {}) return unless width > 0 && height > 0 return if upload.try(:sha1).blank? @@ -90,7 +90,7 @@ class OptimizedImage < ActiveRecord::Base end def local? - !(url =~ /^(https?:)?\/\//) + !(url =~ /^(https?:)?\/\//) end def self.safe_path?(path) @@ -107,8 +107,11 @@ class OptimizedImage < ActiveRecord::Base end end + def self.thumbnail_or_resize + SiteSetting.strip_image_metadata ? "thumbnail" : "resize" + end - def self.resize_instructions(from, to, dimensions, opts={}) + def self.resize_instructions(from, to, dimensions, opts = {}) ensure_safe_paths!(from, to) # NOTE: ORDER is important! @@ -118,7 +121,7 @@ class OptimizedImage < ActiveRecord::Base -auto-orient -gravity center -background transparent - -thumbnail #{dimensions}^ + -#{thumbnail_or_resize} #{dimensions}^ -extent #{dimensions} -interpolate bicubic -unsharp 2x0.5+0.7+0 @@ -129,7 +132,7 @@ class OptimizedImage < ActiveRecord::Base } end - def self.resize_instructions_animated(from, to, dimensions, opts={}) + def self.resize_instructions_animated(from, to, dimensions, opts = {}) ensure_safe_paths!(from, to) %W{ @@ -142,7 +145,7 @@ class OptimizedImage < ActiveRecord::Base } end - def self.crop_instructions(from, to, dimensions, opts={}) + def self.crop_instructions(from, to, dimensions, opts = {}) ensure_safe_paths!(from, to) %W{ @@ -151,7 +154,7 @@ class OptimizedImage < ActiveRecord::Base -auto-orient -gravity north -background transparent - -thumbnail #{opts[:width]} + -#{thumbnail_or_resize} #{opts[:width]} -crop #{dimensions}+0+0 -unsharp 2x0.5+0.7+0 -interlace none @@ -161,7 +164,7 @@ class OptimizedImage < ActiveRecord::Base } end - def self.crop_instructions_animated(from, to, dimensions, opts={}) + def self.crop_instructions_animated(from, to, dimensions, opts = {}) ensure_safe_paths!(from, to) %W{ @@ -174,7 +177,7 @@ class OptimizedImage < ActiveRecord::Base } end - def self.downsize_instructions(from, to, dimensions, opts={}) + def self.downsize_instructions(from, to, dimensions, opts = {}) ensure_safe_paths!(from, to) %W{ @@ -190,24 +193,24 @@ class OptimizedImage < ActiveRecord::Base } end - def self.downsize_instructions_animated(from, to, dimensions, opts={}) + def self.downsize_instructions_animated(from, to, dimensions, opts = {}) resize_instructions_animated(from, to, dimensions, opts) end - def self.resize(from, to, width, height, opts={}) + def self.resize(from, to, width, height, opts = {}) optimize("resize", from, to, "#{width}x#{height}", opts) end - def self.crop(from, to, width, height, opts={}) + def self.crop(from, to, width, height, opts = {}) opts[:width] = width optimize("crop", from, to, "#{width}x#{height}", opts) end - def self.downsize(from, to, dimensions, opts={}) + def self.downsize(from, to, dimensions, opts = {}) optimize("downsize", from, to, dimensions, opts) end - def self.optimize(operation, from, to, dimensions, opts={}) + def self.optimize(operation, from, to, dimensions, opts = {}) method_name = "#{operation}_instructions" if !!opts[:allow_animation] && (from =~ /\.GIF$/i || opts[:filename] =~ /\.GIF$/i) method_name += "_animated" @@ -223,14 +226,14 @@ class OptimizedImage < ActiveRecord::Base return false end - ImageOptim.new.optimize_image!(to) + FileHelper.optimize_image!(to) true rescue Rails.logger.error("Could not optimize image: #{to}") false end - def self.migrate_to_new_scheme(limit=nil) + def self.migrate_to_new_scheme(limit = nil) problems = [] if SiteSetting.migrate_to_new_scheme @@ -268,7 +271,7 @@ class OptimizedImage < ActiveRecord::Base optimized_image.sha1 = Upload.generate_digest(path) end # optimize if image - ImageOptim.new.optimize_image!(path) + FileHelper.optimize_image!(path) # store to new location & update the filesize File.open(path) do |f| optimized_image.url = Discourse.store.store_optimized_image(f, optimized_image) diff --git a/app/models/permalink.rb b/app/models/permalink.rb index e7427d20ba..4bfaa5a9cd 100644 --- a/app/models/permalink.rb +++ b/app/models/permalink.rb @@ -44,8 +44,8 @@ class Permalink < ActiveRecord::Base def normalize(url) return url unless @rules - @rules.each do |(regex,sub)| - url = url.sub(regex,sub) + @rules.each do |(regex, sub)| + url = url.sub(regex, sub) end url @@ -56,10 +56,10 @@ class Permalink < ActiveRecord::Base def self.normalize_url(url) if url url = url.strip - url = url[1..-1] if url[0,1] == '/' + url = url[1..-1] if url[0, 1] == '/' end - normalizations = SiteSetting.permalink_normalizations + normalizations = SiteSetting.permalink_normalizations @normalizer = Normalizer.new(normalizations) unless @normalizer && @normalizer.source == normalizations @normalizer.normalize(url) @@ -81,10 +81,10 @@ class Permalink < ActiveRecord::Base nil end - def self.filter_by(url=nil) + def self.filter_by(url = nil) permalinks = Permalink - .includes(:topic, :post, :category) - .order('permalinks.created_at desc') + .includes(:topic, :post, :category) + .order('permalinks.created_at desc') permalinks.where!('url ILIKE :url OR external_url ILIKE :url', url: "%#{url}%") if url.present? permalinks.limit!(100) diff --git a/app/models/plugin_store.rb b/app/models/plugin_store.rb index 5a7675344e..aebf7f3217 100644 --- a/app/models/plugin_store.rb +++ b/app/models/plugin_store.rb @@ -7,7 +7,7 @@ class PluginStore end def self.set(plugin_name, key, value) - hash = {plugin_name: plugin_name, key: key} + hash = { plugin_name: plugin_name, key: key } row = PluginStoreRow.find_by(hash) || PluginStoreRow.new(hash) row.type_name = determine_type(value) @@ -34,7 +34,7 @@ class PluginStore if item.is_a? Hash ActiveSupport::HashWithIndifferentAccess.new item elsif item.is_a? Array - item.map { |subitem| map_json subitem} + item.map { |subitem| map_json subitem } else item end diff --git a/app/models/post.rb b/app/models/post.rb index ad2b98e2f4..a4df5df778 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -79,7 +79,7 @@ class Post < ActiveRecord::Base scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) } scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) } scope :visible, -> { joins(:topic).where('topics.visible = true').where(hidden: false) } - scope :secured, lambda { |guardian| where('posts.post_type in (?)', Topic.visible_post_types(guardian && guardian.user))} + scope :secured, lambda { |guardian| where('posts.post_type in (?)', Topic.visible_post_types(guardian && guardian.user)) } scope :for_mailing_list, ->(user, since) { q = created_since(since) .joins(:topic) @@ -154,7 +154,7 @@ class Post < ActiveRecord::Base end end - def trash!(trashed_by=nil) + def trash!(trashed_by = nil) self.topic_links.each(&:destroy) super(trashed_by) end @@ -182,7 +182,7 @@ class Post < ActiveRecord::Base def matches_recent_post? post_id = $redis.get(unique_post_key) - post_id != nil and post_id.to_i != id + post_id != (nil) && post_id.to_i != (id) end def raw_hash @@ -218,24 +218,24 @@ class Post < ActiveRecord::Base # case we can skip the rendering pipeline. return raw if cook_method == Post.cook_methods[:raw_html] - cooked = nil - if cook_method == Post.cook_methods[:email] - cooked = EmailCook.new(raw).cook - else - cloned = args.dup - cloned[1] ||= {} + cooked = + if cook_method == Post.cook_methods[:email] + EmailCook.new(raw).cook + else + cloned = args.dup + cloned[1] ||= {} - post_user = self.user - cloned[1][:user_id] = post_user.id if post_user + post_user = self.user + cloned[1][:user_id] = post_user.id if post_user - cooked = if add_nofollow? - post_analyzer.cook(*args) - else - # At trust level 3, we don't apply nofollow to links - cloned[1][:omit_nofollow] = true - post_analyzer.cook(*cloned) - end - end + if add_nofollow? + post_analyzer.cook(*args) + else + # At trust level 3, we don't apply nofollow to links + cloned[1][:omit_nofollow] = true + post_analyzer.cook(*cloned) + end + end new_cooked = Plugin::Filter.apply(:after_post_cook, self, cooked) @@ -267,10 +267,10 @@ class Post < ActiveRecord::Base def whitelisted_spam_hosts hosts = SiteSetting - .white_listed_spam_host_domains - .split('|') - .map{|h| h.strip} - .reject{|h| !h.include?('.')} + .white_listed_spam_host_domains + .split('|') + .map { |h| h.strip } + .reject { |h| !h.include?('.') } hosts << GlobalSetting.hostname hosts << RailsMultisite::ConnectionManagement.current_hostname @@ -290,9 +290,9 @@ class Post < ActiveRecord::Base return hosts if hosts.length == 0 TopicLink.where(domain: hosts.keys, user_id: acting_user.id) - .group(:domain, :post_id) - .count - .each_key do |tuple| + .group(:domain, :post_id) + .count + .each_key do |tuple| domain = tuple[0] hosts[domain] = (hosts[domain] || 0) + 1 end @@ -319,7 +319,7 @@ class Post < ActiveRecord::Base order('sort_order desc, post_number desc') end - def self.summary(topic_id=nil) + def self.summary(topic_id = nil) # PERF: if you pass in nil it is WAY slower # pg chokes getting a reasonable plan topic_id = topic_id ? topic_id.to_i : "posts.topic_id" @@ -410,7 +410,7 @@ class Post < ActiveRecord::Base "#{Discourse.base_url}#{url}" end - def url(opts=nil) + def url(opts = nil) opts ||= {} if topic @@ -424,7 +424,7 @@ class Post < ActiveRecord::Base "#{Discourse.base_url}/email/unsubscribe/#{UnsubscribeKey.create_key_for(user, self)}" end - def self.url(slug, topic_id, post_number, opts=nil) + def self.url(slug, topic_id, post_number, opts = nil) opts ||= {} result = "/t/" @@ -434,12 +434,12 @@ class Post < ActiveRecord::Base end def self.urls(post_ids) - ids = post_ids.map{|u| u} + ids = post_ids.map { |u| u } if ids.length > 0 urls = {} Topic.joins(:posts).where('posts.id' => ids). - select(['posts.id as post_id','post_number', 'topics.slug', 'topics.title', 'topics.id']). - each do |t| + select(['posts.id as post_id', 'post_number', 'topics.slug', 'topics.title', 'topics.id']). + each do |t| urls[t.post_id.to_i] = url(t.slug, t.id, t.post_number) end urls @@ -448,24 +448,24 @@ class Post < ActiveRecord::Base end end - def revise(updated_by, changes={}, opts={}) + def revise(updated_by, changes = {}, opts = {}) PostRevisor.new(self).revise!(updated_by, changes, opts) end def self.rebake_old(limit) problems = [] Post.where('baked_version IS NULL OR baked_version < ?', BAKED_VERSION) - .limit(limit).each do |p| + .limit(limit).each do |p| begin p.rebake! rescue => e - problems << {post: p, ex: e} + problems << { post: p, ex: e } end end problems end - def rebake!(opts=nil) + def rebake!(opts = nil) opts ||= {} new_cooked = cook(raw, topic_id: topic_id, invalidate_oneboxes: opts.fetch(:invalidate_oneboxes, false)) @@ -485,14 +485,14 @@ class Post < ActiveRecord::Base new_cooked != old_cooked end - def set_owner(new_user, actor, skip_revision=false) + def set_owner(new_user, actor, skip_revision = false) return if user_id == new_user.id edit_reason = I18n.t('change_owner.post_revision_text', old_user: (self.user.username_lower rescue nil) || I18n.t('change_owner.deleted_user'), new_user: new_user.username_lower ) - revise(actor, {raw: self.raw, user_id: new_user.id, edit_reason: edit_reason}, {bypass_bump: true, skip_revision: skip_revision}) + revise(actor, { raw: self.raw, user_id: new_user.id, edit_reason: edit_reason }, bypass_bump: true, skip_revision: skip_revision) if post_number == topic.highest_post_number topic.update_columns(last_post_user_id: new_user.id) @@ -516,7 +516,7 @@ class Post < ActiveRecord::Base # This calculates the geometric mean of the post timings and stores it along with # each post. - def self.calculate_avg_time(min_topic_age=nil) + def self.calculate_avg_time(min_topic_age = nil) retry_lock_error do builder = SqlBuilder.new("UPDATE posts SET avg_time = (x.gmean / 1000) @@ -577,7 +577,6 @@ class Post < ActiveRecord::Base self.quote_count = temp_collector.size end - def save_reply_relationships add_to_quoted_post_numbers(reply_to_post_number) return if self.quoted_post_numbers.blank? @@ -602,7 +601,7 @@ class Post < ActiveRecord::Base DiscourseEvent.trigger(:after_trigger_post_process, self) end - def self.public_posts_count_per_day(start_date, end_date, category_id=nil) + def self.public_posts_count_per_day(start_date, end_date, category_id = nil) result = public_posts.where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date) result = result.where('topics.category_id = ?', category_id) if category_id result.group('date(posts.created_at)').order('date(posts.created_at)').count @@ -612,7 +611,7 @@ class Post < ActiveRecord::Base private_posts.with_topic_subtype(topic_subtype).where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date).group('date(posts.created_at)').order('date(posts.created_at)').count end - def reply_history(max_replies=100, guardian=nil) + def reply_history(max_replies = 100, guardian = nil) post_ids = Post.exec_sql("WITH RECURSIVE breadcrumb(id, reply_to_post_number) AS ( SELECT p.id, p.reply_to_post_number FROM posts AS p WHERE p.id = :post_id @@ -622,11 +621,11 @@ class Post < ActiveRecord::Base AND p.topic_id = :topic_id ) SELECT id from breadcrumb ORDER by id", post_id: id, topic_id: topic_id).to_a - post_ids.map! {|r| r['id'].to_i } - .reject! {|post_id| post_id == id} + post_ids.map! { |r| r['id'].to_i } + .reject! { |post_id| post_id == id } # [1,2,3][-10,-1] => nil - post_ids = (post_ids[(0-max_replies)..-1] || post_ids) + post_ids = (post_ids[(0 - max_replies)..-1] || post_ids) Post.secured(guardian).where(id: post_ids).includes(:user, :topic).order(:id).to_a end diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 73fcf5ed37..73a7e583ff 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -42,27 +42,27 @@ class PostAction < ActiveRecord::Base nil end - def self.flag_count_by_date(start_date, end_date, category_id=nil) + def self.flag_count_by_date(start_date, end_date, category_id = nil) result = where('post_actions.created_at >= ? AND post_actions.created_at <= ?', start_date, end_date) result = result.where(post_action_type_id: PostActionType.flag_types.values) result = result.joins(post: :topic).where("topics.category_id = ?", category_id) if category_id result.group('date(post_actions.created_at)') - .order('date(post_actions.created_at)') - .count + .order('date(post_actions.created_at)') + .count end def self.update_flagged_posts_count posts_flagged_count = PostAction.active - .flags - .joins(post: :topic) - .where('posts.deleted_at' => nil) - .where('topics.deleted_at' => nil) - .where('posts.user_id > 0') - .count('DISTINCT posts.id') + .flags + .joins(post: :topic) + .where('posts.deleted_at' => nil) + .where('topics.deleted_at' => nil) + .where('posts.user_id > 0') + .count('DISTINCT posts.id') $redis.set('posts_flagged_count', posts_flagged_count) user_ids = User.staff.pluck(:id) - MessageBus.publish('/flagged_counts', { total: posts_flagged_count }, { user_ids: user_ids }) + MessageBus.publish('/flagged_counts', { total: posts_flagged_count }, user_ids: user_ids) end def self.flagged_posts_count @@ -107,7 +107,6 @@ SQL (map[row.topic_id] ||= []) << row.post_number end - map end @@ -128,21 +127,21 @@ SQL user_actions end - def self.count_per_day_for_type(post_action_type, opts=nil) + def self.count_per_day_for_type(post_action_type, opts = nil) opts ||= {} result = unscoped.where(post_action_type_id: post_action_type) result = result.where('post_actions.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago) result = result.where('post_actions.created_at <= ?', opts[:end_date]) if opts[:end_date] result = result.joins(post: :topic).where('topics.category_id = ?', opts[:category_id]) if opts[:category_id] result.group('date(post_actions.created_at)') - .order('date(post_actions.created_at)') - .count + .order('date(post_actions.created_at)') + .count end - def self.agree_flags!(post, moderator, delete_post=false) + def self.agree_flags!(post, moderator, delete_post = false) actions = PostAction.active - .where(post_id: post.id) - .where(post_action_type_id: PostActionType.flag_types.values) + .where(post_id: post.id) + .where(post_action_type_id: PostActionType.flag_types.values) trigger_spam = false actions.each do |action| @@ -169,7 +168,7 @@ SQL end actions = PostAction.where(post_id: post.id) - .where(post_action_type_id: action_type_ids) + .where(post_action_type_id: action_type_ids) actions.each do |action| action.disagreed_at = Time.zone.now @@ -186,10 +185,10 @@ SQL update_flagged_posts_count end - def self.defer_flags!(post, moderator, delete_post=false) + def self.defer_flags!(post, moderator, delete_post = false) actions = PostAction.active - .where(post_id: post.id) - .where(post_action_type_id: PostActionType.flag_types.values) + .where(post_id: post.id) + .where(post_action_type_id: PostActionType.flag_types.values) actions.each do |action| action.deferred_at = Time.zone.now @@ -202,7 +201,7 @@ SQL update_flagged_posts_count end - def add_moderator_post_if_needed(moderator, disposition, delete_post=false) + def add_moderator_post_if_needed(moderator, disposition, delete_post = false) return if !SiteSetting.auto_respond_to_flag_actions return if related_post.nil? || related_post.topic.nil? return if staff_already_replied?(related_post.topic) @@ -237,33 +236,35 @@ SQL opts[:target_group_names] = target_moderators else opts[:subtype] = TopicSubtype.notify_user - opts[:target_usernames] = if post_action_type == :notify_user - post.user.username - elsif post_action_type != :notify_moderators - # this is a hack to allow a PM with no recipients, we should think through - # a cleaner technique, a PM with myself is valid for flagging - 'x' - end + + opts[:target_usernames] = + if post_action_type == :notify_user + post.user.username + elsif post_action_type != :notify_moderators + # this is a hack to allow a PM with no recipients, we should think through + # a cleaner technique, a PM with myself is valid for flagging + 'x' + end end PostCreator.new(user, opts).create.try(:id) end - def self.limit_action!(user,post,post_action_type_id) + def self.limit_action!(user, post, post_action_type_id) RateLimiter.new(user, "post_action-#{post.id}_#{post_action_type_id}", 4, 1.minute).performed! end def self.act(user, post, post_action_type_id, opts = {}) - limit_action!(user,post,post_action_type_id) + limit_action!(user, post, post_action_type_id) related_post_id = create_message_for_post_action(user, post, post_action_type_id, opts) staff_took_action = opts[:take_action] || false - targets_topic = if opts[:flag_topic] && post.topic - post.topic.reload - post.topic.posts_count != 1 - end + targets_topic = + if opts[:flag_topic] && post.topic + post.topic.reload.posts_count != 1 + end where_attrs = { post_id: post.id, @@ -279,9 +280,9 @@ SQL # First try to revive a trashed record post_action = PostAction.where(where_attrs) - .with_deleted - .where("deleted_at IS NOT NULL") - .first + .with_deleted + .where("deleted_at IS NOT NULL") + .first if post_action post_action.recover! @@ -324,7 +325,7 @@ SQL def self.remove_act(user, post, post_action_type_id) - limit_action!(user,post,post_action_type_id) + limit_action!(user, post, post_action_type_id) finder = PostAction.where(post_id: post.id, user_id: user.id, post_action_type_id: post_action_type_id) finder = finder.with_deleted.includes(:post) if user.try(:staff?) @@ -374,10 +375,10 @@ SQL multiplier = SiteSetting.send("tl#{user.trust_level}_additional_likes_per_day_multiplier").to_f multiplier = 1.0 if multiplier < 1.0 - limit = (limit * multiplier ).to_i + limit = (limit * multiplier).to_i end - @rate_limiter = RateLimiter.new(user, "create_#{type}",limit, 1.day.to_i) + @rate_limiter = RateLimiter.new(user, "create_#{type}", limit, 1.day.to_i) return @rate_limiter end end @@ -386,12 +387,12 @@ SQL before_create do post_action_type_ids = is_flag? ? PostActionType.flag_types.values : post_action_type_id raise AlreadyActed if PostAction.where(user_id: user_id) - .where(post_id: post_id) - .where(post_action_type_id: post_action_type_ids) - .where(deleted_at: nil) - .where(disagreed_at: nil) - .where(targets_topic: targets_topic) - .exists? + .where(post_id: post_id) + .where(post_action_type_id: post_action_type_ids) + .where(deleted_at: nil) + .where(disagreed_at: nil) + .where(targets_topic: targets_topic) + .exists? end # Returns the flag counts for a post, taking into account that some users @@ -427,8 +428,8 @@ SQL # Update denormalized counts column = "#{post_action_type_key}_count" count = PostAction.where(post_id: post_id) - .where(post_action_type_id: post_action_type_id) - .count + .where(post_action_type_id: post_action_type_id) + .count # We probably want to refactor this method to something cleaner. case post_action_type_key @@ -438,18 +439,17 @@ SQL when :like # 'like_score' is weighted higher for staff accounts score = PostAction.joins(:user) - .where(post_id: post_id) - .sum("CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END") + .where(post_id: post_id) + .sum("CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END") Post.where(id: post_id).update_all ["like_count = :count, like_score = :score", count: count, score: score] else Post.where(id: post_id).update_all ["#{column} = ?", count] end - topic_id = Post.with_deleted.where(id: post_id).pluck(:topic_id).first # topic_user - if [:like,:bookmark].include? post_action_type_key + if [:like, :bookmark].include? post_action_type_key TopicUser.update_post_action_cache(user_id: user_id, topic_id: topic_id, post_action_type: post_action_type_key) @@ -501,12 +501,12 @@ SQL return if topic.nil? || topic.closed? flags = PostAction.active - .flags - .joins(:post) - .where("posts.topic_id = ?", topic.id) - .where("post_actions.user_id > 0") - .group("post_actions.user_id") - .pluck("post_actions.user_id, COUNT(post_id)") + .flags + .joins(:post) + .where("posts.topic_id = ?", topic.id) + .where("post_actions.user_id > 0") + .group("post_actions.user_id") + .pluck("post_actions.user_id, COUNT(post_id)") # we need a minimum number of unique flaggers return if flags.count < SiteSetting.num_flaggers_to_close_topic @@ -535,7 +535,7 @@ SQL acting_user.has_trust_level?(TrustLevel[3]) && post.user.trust_level == TrustLevel[0] - hide_post!(post, post_action_type, Post.hidden_reasons[:flagged_by_tl3_user]) + hide_post!(post, post_action_type, Post.hidden_reasons[:flagged_by_tl3_user]) elsif PostActionType.auto_action_flag_types.include?(post_action_type) && SiteSetting.flags_required_to_hide_post > 0 @@ -548,7 +548,7 @@ SQL end end - def self.hide_post!(post, post_action_type, reason=nil) + def self.hide_post!(post, post_action_type, reason = nil) return if post.hidden unless reason diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index 53296c8e59..e0105cce4e 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -3,7 +3,7 @@ require_dependency 'oneboxer' class PostAnalyzer def initialize(raw, topic_id) - @raw = raw + @raw = raw @topic_id = topic_id @found_oneboxes = false end @@ -51,11 +51,11 @@ class PostAnalyzer return @raw_mentions if @raw_mentions.present? raw_mentions = cooked_stripped.css('.mention, .mention-group').map do |e| - if name = e.inner_text - name = name[1..-1] - name.downcase! if name - name - end + if name = e.inner_text + name = name[1..-1] + name.downcase! if name + name + end end raw_mentions.compact! diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index 1d70519eaf..06ac7b5ebc 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -19,7 +19,7 @@ class PostMover end end - def to_new_topic(title, category_id=nil) + def to_new_topic(title, category_id = nil) @move_type = PostMover.move_types[:new_topic] post = Post.find_by(id: post_ids.first) diff --git a/app/models/post_timing.rb b/app/models/post_timing.rb index 8f68209020..c006d29508 100644 --- a/app/models/post_timing.rb +++ b/app/models/post_timing.rb @@ -5,7 +5,6 @@ class PostTiming < ActiveRecord::Base validates_presence_of :post_number validates_presence_of :msecs - def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number) # This is done in SQL cause the logic is quite tricky and we want to do this in one db hit # @@ -39,9 +38,9 @@ class PostTiming < ActiveRecord::Base AND post_number = :post_number)", args) rescue PG::UniqueViolation - # concurrency is hard, we are not running serialized so this can possibly - # still happen, if it happens we just don't care, its an invalid record anyway - return + # concurrency is hard, we are not running serialized so this can possibly + # still happen, if it happens we just don't care, its an invalid record anyway + return end Post.where(['topic_id = :topic_id and post_number = :post_number', args]).update_all 'reads = reads + 1' @@ -60,7 +59,6 @@ class PostTiming < ActiveRecord::Base record_new_timing(args) if rows == 0 end - def self.destroy_for(user_id, topic_ids) PostTiming.transaction do PostTiming.delete_all(['user_id = ? and topic_id in (?)', user_id, topic_ids]) @@ -68,9 +66,9 @@ class PostTiming < ActiveRecord::Base end end - MAX_READ_TIME_PER_BATCH = 60*1000.0 + MAX_READ_TIME_PER_BATCH = 60 * 1000.0 - def self.process_timings(current_user, topic_id, topic_time, timings, opts={}) + def self.process_timings(current_user, topic_id, topic_time, timings, opts = {}) current_user.user_stat.update_time_read! max_time_per_post = ((Time.now - current_user.created_at) * 1000.0) @@ -89,9 +87,8 @@ class PostTiming < ActiveRecord::Base timings.each_with_index do |(post_number, time), index| - join_table << "SELECT #{topic_id.to_i} topic_id, #{post_number.to_i} post_number, - #{current_user.id.to_i} user_id, #{time.to_i} msecs, #{index} idx" - + join_table << "SELECT #{topic_id.to_i} topic_id, #{post_number.to_i} post_number, + #{current_user.id.to_i} user_id, #{time.to_i} msecs, #{index} idx" highest_seen = post_number.to_i > highest_seen ? post_number.to_i : highest_seen @@ -113,19 +110,19 @@ SQL result.type_map = SqlBuilder.pg_type_map existing = Set.new(result.column_values(0)) - timings.each_with_index do |(post_number, time),index| + timings.each_with_index do |(post_number, time), index| unless existing.include?(index) PostTiming.record_new_timing(topic_id: topic_id, - post_number: post_number, - user_id: current_user.id, - msecs: time) + post_number: post_number, + user_id: current_user.id, + msecs: time) end end end total_changed = 0 if timings.length > 0 - total_changed = Notification.mark_posts_read(current_user, topic_id, timings.map{|t| t[0]}) + total_changed = Notification.mark_posts_read(current_user, topic_id, timings.map { |t| t[0] }) end topic_time = max_time_per_post if topic_time > max_time_per_post diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index e1736c8548..f7b3147e8b 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -51,7 +51,7 @@ class QueuedPost < ActiveRecord::Base end def create_options - opts = {raw: raw} + opts = { raw: raw } opts.merge!(post_options.symbolize_keys) opts[:cooking_options].symbolize_keys! if opts[:cooking_options] @@ -102,7 +102,7 @@ class QueuedPost < ActiveRecord::Base end # Update the record in memory too, and clear the dirty flag - updates.each {|k, v| send("#{k}=", v) } + updates.each { |k, v| send("#{k}=", v) } changes_applied QueuedPost.broadcast_new! if visible? diff --git a/app/models/quoted_post.rb b/app/models/quoted_post.rb index 2bd06fce81..ecfe19e73d 100644 --- a/app/models/quoted_post.rb +++ b/app/models/quoted_post.rb @@ -17,8 +17,8 @@ class QuotedPost < ActiveRecord::Base post_number = a['data-post'].to_i next if topic_id == 0 || post_number == 0 - next if uniq[[topic_id,post_number]] - uniq[[topic_id,post_number]] = true + next if uniq[[topic_id, post_number]] + uniq[[topic_id, post_number]] = true begin # It would be so much nicer if we used post_id in quotes diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index b426030dbe..3030b4aa22 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -7,7 +7,7 @@ class RemoteTheme < ActiveRecord::Base has_one :theme - def self.import_theme(url, user=Discourse.system_user) + def self.import_theme(url, user = Discourse.system_user) importer = GitImporter.new(url) importer.import! @@ -37,7 +37,7 @@ class RemoteTheme < ActiveRecord::Base self.remote_version, self.commits_behind = importer.commits_since(remote_version) end - def update_from_remote(importer=nil) + def update_from_remote(importer = nil) return unless remote_url cleanup = false @@ -70,9 +70,9 @@ class RemoteTheme < ActiveRecord::Base end theme.set_field(target: info["target"] || :common, - name: name, - value: info["value"], - type: info["type"] || :theme_var) + name: name, + value: info["value"], + type: info["type"] || :theme_var) end Theme.targets.keys.each do |target| diff --git a/app/models/report.rb b/app/models/report.rb index 76c162ffb1..8144df648b 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -14,7 +14,7 @@ class Report @end_date ||= Time.zone.now.end_of_day end - def as_json(options=nil) + def as_json(options = nil) { type: type, title: I18n.t("reports.#{type}.title"), @@ -34,7 +34,7 @@ class Report singleton_class.instance_eval { define_method("report_#{name}", &block) } end - def self.find(type, opts=nil) + def self.find(type, opts = nil) opts ||= {} # Load the report @@ -56,11 +56,11 @@ class Report report end - def self.req_report(report, filter=nil) + def self.req_report(report, filter = nil) data = if filter == :page_view_total ApplicationRequest.where(req_type: [ - ApplicationRequest.req_types.reject{|k,v| k =~ /mobile/}.map{|k,v| v if k =~ /page_view/}.compact + ApplicationRequest.req_types.reject { |k, v| k =~ /mobile/ }.map { |k, v| v if k =~ /page_view/ }.compact ].flatten) else ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) @@ -68,18 +68,18 @@ class Report report.data = [] data.where('date >= ? AND date <= ?', report.start_date.to_date, report.end_date.to_date) - .order(date: :asc) - .group(:date) - .sum(:count) - .each do |date, count| + .order(date: :asc) + .group(:date) + .sum(:count) + .each do |date, count| report.data << { x: date, y: count } end report.total = data.sum(:count) report.prev30Days = data.where('date >= ? AND date <= ?', (report.start_date - 31.days).to_date, - (report.end_date - 31.days).to_date ) - .sum(:count) + (report.end_date - 31.days).to_date) + .sum(:count) end def self.report_visits(report) @@ -128,7 +128,7 @@ class Report def self.report_time_to_first_response(report) report.data = [] - Topic.time_to_first_response_per_day(report.start_date, report.end_date, {category_id: report.category_id}).each do |r| + Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: report.category_id).each do |r| report.data << { x: Date.parse(r["date"]), y: r["hours"].to_f.round(2) } end report.total = Topic.time_to_first_response_total(category_id: report.category_id) diff --git a/app/models/screened_email.rb b/app/models/screened_email.rb index ee8b005728..a0dec7f12f 100644 --- a/app/models/screened_email.rb +++ b/app/models/screened_email.rb @@ -18,8 +18,8 @@ class ScreenedEmail < ActiveRecord::Base self.email = email.downcase end - def self.block(email, opts={}) - find_by_email(Email.downcase(email)) || create(opts.slice(:action_type, :ip_address).merge({email: email})) + def self.block(email, opts = {}) + find_by_email(Email.downcase(email)) || create(opts.slice(:action_type, :ip_address).merge(email: email)) end def self.should_block?(email) @@ -30,8 +30,8 @@ class ScreenedEmail < ActiveRecord::Base max_distance = SiteSetting.levenshtein_distance_spammer_emails screened_email = screened_emails.select { |se| distances[se.email] <= max_distance } - .sort { |se| distances[se.email] } - .first + .sort { |se| distances[se.email] } + .first screened_email.record_match! if screened_email @@ -46,13 +46,13 @@ class ScreenedEmail < ActiveRecord::Base (1..second.length).each do |i| (1..first.length).each do |j| - if first[j-1] == second[i-1] - matrix[i][j] = matrix[i-1][j-1] + if first[j - 1] == second[i - 1] + matrix[i][j] = matrix[i - 1][j - 1] else matrix[i][j] = [ - matrix[i-1][j], - matrix[i][j-1], - matrix[i-1][j-1], + matrix[i - 1][j], + matrix[i][j - 1], + matrix[i - 1][j - 1], ].min + 1 end end diff --git a/app/models/screened_ip_address.rb b/app/models/screened_ip_address.rb index 248ad97703..f0e51ab045 100644 --- a/app/models/screened_ip_address.rb +++ b/app/models/screened_ip_address.rb @@ -11,7 +11,7 @@ class ScreenedIpAddress < ActiveRecord::Base validates :ip_address, ip_address_format: true, presence: true - def self.watch(ip_address, opts={}) + def self.watch(ip_address, opts = {}) match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address)) end @@ -67,10 +67,10 @@ class ScreenedIpAddress < ActiveRecord::Base exists_for_ip_address_and_action?(ip_address, actions[:do_nothing]) end - def self.exists_for_ip_address_and_action?(ip_address, action_type, opts={}) + def self.exists_for_ip_address_and_action?(ip_address, action_type, opts = {}) b = match_for_ip_address(ip_address) - found = (!!b and b.action_type == action_type) - b.record_match! if found and opts[:record_match] != false + found = (!!b && b.action_type == (action_type)) + b.record_match! if found && opts[:record_match] != (false) found end @@ -125,7 +125,7 @@ class ScreenedIpAddress < ActiveRecord::Base ScreenedIpAddress.exec_sql(star_star_subnets_query, min_count: 10, roll_up_weight: weight).values.flatten end - def self.roll_up(current_user=Discourse.system_user) + def self.roll_up(current_user = Discourse.system_user) # 1 - retrieve all subnets that needs roll up subnets = [star_subnets, star_star_subnets].flatten @@ -158,9 +158,9 @@ class ScreenedIpAddress < ActiveRecord::Base # 5 - remove old matches ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]) - .where("family(ip_address) = 4") - .where("ip_address << ?", subnet) - .delete_all + .where("family(ip_address) = 4") + .where("ip_address << ?", subnet) + .delete_all end # return the subnets diff --git a/app/models/screened_url.rb b/app/models/screened_url.rb index 3bd7cc1b89..47e6e949db 100644 --- a/app/models/screened_url.rb +++ b/app/models/screened_url.rb @@ -21,7 +21,7 @@ class ScreenedUrl < ActiveRecord::Base self.domain = self.domain.downcase.sub(/^www\./, '') if self.domain end - def self.watch(url, domain, opts={}) + def self.watch(url, domain, opts = {}) find_match(url) || create(opts.slice(:action_type, :ip_address).merge(url: url, domain: domain)) end diff --git a/app/models/search_log.rb b/app/models/search_log.rb new file mode 100644 index 0000000000..acaf269c56 --- /dev/null +++ b/app/models/search_log.rb @@ -0,0 +1,70 @@ +require_dependency 'enum' + +class SearchLog < ActiveRecord::Base + validates_presence_of :term, :ip_address + + def self.search_types + @search_types ||= Enum.new( + header: 1, + full_page: 2 + ) + end + + def self.log(term:, search_type:, ip_address:, user_id: nil) + + search_type = search_types[search_type] + return [:error] unless search_type.present? && ip_address.present? + + update_sql = <<~SQL + UPDATE search_logs + SET term = :term, + created_at = :created_at + WHERE created_at > :timeframe AND + position(term IN :term) = 1 AND + ((:user_id IS NULL AND ip_address = :ip_address) OR + (user_id = :user_id)) + RETURNING id + SQL + + rows = exec_sql( + update_sql, + term: term, + created_at: Time.zone.now, + timeframe: 5.seconds.ago, + user_id: user_id, + ip_address: ip_address + ) + + if rows.cmd_tuples == 0 + result = create( + term: term, + search_type: search_type, + ip_address: ip_address, + user_id: user_id + ) + [:created, result.id] + else + [:updated, rows[0]['id'].to_i] + end + end + + def self.clean_up + search_id = SearchLog.order(:id).offset(SiteSetting.search_query_log_max_size).limit(1).pluck(:id) + if search_id.present? + SearchLog.where('id < ?', search_id[0]).delete_all + end + end +end + +# == Schema Information +# +# Table name: search_logs +# +# id :integer not null, primary key +# term :string not null +# user_id :integer +# ip_address :inet not null +# clicked_topic_id :integer +# search_type :integer not null +# created_at :datetime not null +# diff --git a/app/models/site.rb b/app/models/site.rb index 37312625ba..9d90b2c1bc 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -121,7 +121,7 @@ class Site def self.clear_anon_cache! # publishing forces the sequence up # the cache is validated based on the sequence - MessageBus.publish('/site_json','') + MessageBus.publish('/site_json', '') end end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index ef88e14516..2e4fa838f3 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -34,7 +34,7 @@ class SiteSetting < ActiveRecord::Base client_settings << :available_locales def self.available_locales - LocaleSiteSetting.values.map{ |e| e[:value] }.join('|') + LocaleSiteSetting.values.map { |e| e[:value] }.join('|') end def self.topic_title_length @@ -71,8 +71,8 @@ class SiteSetting < ActiveRecord::Base def self.anonymous_homepage top_menu_items.map { |item| item.name } - .select { |item| anonymous_menu_items.include?(item) } - .first + .select { |item| anonymous_menu_items.include?(item) } + .first end def self.should_download_images?(src) diff --git a/app/models/slug_setting.rb b/app/models/slug_setting.rb index b2be27fcbd..86dc666d91 100644 --- a/app/models/slug_setting.rb +++ b/app/models/slug_setting.rb @@ -10,7 +10,7 @@ class SlugSetting < EnumSiteSetting def self.values VALUES.map do |l| - {name: l, value: l} + { name: l, value: l } end end diff --git a/app/models/stylesheet_cache.rb b/app/models/stylesheet_cache.rb index 2c568072cb..b421c0df7a 100644 --- a/app/models/stylesheet_cache.rb +++ b/app/models/stylesheet_cache.rb @@ -3,7 +3,7 @@ class StylesheetCache < ActiveRecord::Base MAX_TO_KEEP = 50 - def self.add(target,digest,content,source_map) + def self.add(target, digest, content, source_map) return false if where(target: target, digest: digest).exists? @@ -13,11 +13,11 @@ class StylesheetCache < ActiveRecord::Base if count > MAX_TO_KEEP remove_lower = StylesheetCache - .where(target: target) - .limit(MAX_TO_KEEP) - .order('id desc') - .pluck(:id) - .last + .where(target: target) + .limit(MAX_TO_KEEP) + .order('id desc') + .pluck(:id) + .last exec_sql("DELETE FROM stylesheet_cache where id < :id", id: remove_lower) end diff --git a/app/models/tag.rb b/app/models/tag.rb index 95a944776c..a9223b670a 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -18,23 +18,23 @@ class Tag < ActiveRecord::Base # fetch the result with .count(Tag::COUNT_ARG). # # e.g., Tag.tags_by_count_query.where("topics.category_id = ?", category.id).count(Tag::COUNT_ARG) - def self.tags_by_count_query(opts={}) + def self.tags_by_count_query(opts = {}) q = Tag.joins("LEFT JOIN topic_tags ON tags.id = topic_tags.tag_id") - .joins("LEFT JOIN topics ON topics.id = topic_tags.topic_id") - .group("tags.id, tags.name") - .order('count_topic_tags_id DESC') + .joins("LEFT JOIN topics ON topics.id = topic_tags.topic_id") + .group("tags.id, tags.name") + .order('count_topic_tags_id DESC') q = q.limit(opts[:limit]) if opts[:limit] q end - def self.category_tags_by_count_query(category, opts={}) + def self.category_tags_by_count_query(category, opts = {}) tags_by_count_query(opts).where("tags.id in (select tag_id from category_tags where category_id = ?)", category.id) - .where("topics.category_id = ?", category.id) + .where("topics.category_id = ?", category.id) end def self.top_tags(limit_arg: nil, category: nil, guardian: nil) limit = limit_arg || SiteSetting.max_tags_in_filter_list - scope_category_ids = (guardian||Guardian.new).allowed_category_ids + scope_category_ids = (guardian || Guardian.new).allowed_category_ids if category scope_category_ids &= ([category.id] + category.subcategories.pluck(:id)) @@ -46,7 +46,7 @@ class Tag < ActiveRecord::Base category: category ) - tags.count(COUNT_ARG).map {|name, _| name} + tags.count(COUNT_ARG).map { |name, _| name } end def self.include_tags? diff --git a/app/models/theme.rb b/app/models/theme.rb index ab1fceb91f..f38668e1fa 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -50,7 +50,7 @@ class Theme < ActiveRecord::Base ColorScheme .where(theme_id: self.id) .where("id NOT IN (SELECT color_scheme_id FROM themes where color_scheme_id IS NOT NULL)") - .destroy_all + .destroy_all ColorScheme .where(theme_id: self.id) @@ -114,7 +114,7 @@ class Theme < ActiveRecord::Base (@cache[cache_key] = val || "").html_safe end - def self.remove_from_cache!(themes=nil) + def self.remove_from_cache!(themes = nil) clear_cache! end @@ -122,13 +122,11 @@ class Theme < ActiveRecord::Base @cache.clear end - def self.targets @targets ||= Enum.new(common: 0, desktop: 1, mobile: 2) end - - def notify_scheme_change(clear_manager_cache=true) + def notify_scheme_change(clear_manager_cache = true) Stylesheet::Manager.cache.clear if clear_manager_cache message = refresh_message_for_targets(["desktop", "mobile", "admin"], self) MessageBus.publish('/file-change', message) @@ -140,7 +138,7 @@ class Theme < ActiveRecord::Base themes = [self] + dependant_themes message = themes.map do |theme| - refresh_message_for_targets([:mobile_theme,:desktop_theme], theme) + refresh_message_for_targets([:mobile_theme, :desktop_theme], theme) end.compact.flatten MessageBus.publish('/file-change', message) end @@ -168,7 +166,7 @@ class Theme < ActiveRecord::Base def resolve_dependant_themes(direction) - select_field,where_field=nil + select_field, where_field = nil if direction == :up select_field = "parent_theme_id" @@ -212,7 +210,7 @@ class Theme < ActiveRecord::Base end def resolve_baked_field(target, name) - list_baked_fields(target,name).map{|f| f.value_baked || f.value}.join("\n") + list_baked_fields(target, name).map { |f| f.value_baked || f.value }.join("\n") end def list_baked_fields(target, name) @@ -221,12 +219,15 @@ class Theme < ActiveRecord::Base theme_ids = [self.id] + (included_themes.map(&:id) || []) fields = ThemeField.where(target_id: [Theme.targets[target], Theme.targets[:common]]) - .where(name: name.to_s) - .includes(:theme) - .joins("JOIN ( - SELECT #{theme_ids.map.with_index{|id,idx| "#{id} AS theme_id, #{idx} AS sort_column"}.join(" UNION ALL SELECT ")} - ) as X ON X.theme_id = theme_fields.theme_id") - .order('sort_column, target_id') + .where(name: name.to_s) + .includes(:theme) + .joins(" + JOIN ( + SELECT #{theme_ids.map.with_index { |id, idx| "#{id} AS theme_id, #{idx} AS sort_column" }.join(" UNION ALL SELECT ")} + ) as X ON X.theme_id = theme_fields.theme_id" + ) + .order('sort_column, target_id') + fields.each(&:ensure_baked!) fields end @@ -254,7 +255,7 @@ class Theme < ActiveRecord::Base value ||= "" - field = theme_fields.find{|f| f.name==name && f.target_id == target_id && f.type_id == type_id} + field = theme_fields.find { |f| f.name == name && f.target_id == target_id && f.type_id == type_id } if field if value.blank? && !upload_id theme_fields.delete field.destroy diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 34d09eb189..26e67329ee 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -11,7 +11,7 @@ class ThemeField < ActiveRecord::Base end def self.theme_var_type_ids - @theme_var_type_ids ||= [2,3,4] + @theme_var_type_ids ||= [2, 3, 4] end COMPILER_VERSION = 5 @@ -19,7 +19,7 @@ class ThemeField < ActiveRecord::Base belongs_to :theme def transpile(es6_source, version) - template = Tilt::ES6ModuleTranspilerTemplate.new {} + template = Tilt::ES6ModuleTranspilerTemplate.new {} wrapped = < { #{es6_source} @@ -90,7 +90,6 @@ COMPILED @scss_fields ||= %w(scss embedded_scss) end - def ensure_baked! if ThemeField.html_fields.include?(self.name) if !self.value_baked || compiler_version != COMPILER_VERSION diff --git a/app/models/top_topic.rb b/app/models/top_topic.rb index 6172f7cf0a..709148167c 100644 --- a/app/models/top_topic.rb +++ b/app/models/top_topic.rb @@ -16,7 +16,7 @@ class TopTopic < ActiveRecord::Base # We don't have to refresh these as often def self.refresh_older! - older_periods = periods - [:daily,:all] + older_periods = periods - [:daily, :all] transaction do older_periods.each do |period| @@ -32,7 +32,6 @@ class TopTopic < ActiveRecord::Base refresh_older! end - def self.periods @@periods ||= [:all, :yearly, :quarterly, :monthly, :weekly, :daily].freeze end @@ -203,11 +202,11 @@ class TopTopic < ActiveRecord::Base def self.start_of(period) case period - when :yearly then 1.year.ago - when :monthly then 1.month.ago - when :quarterly then 3.months.ago - when :weekly then 1.week.ago - when :daily then 1.day.ago + when :yearly then 1.year.ago + when :monthly then 1.month.ago + when :quarterly then 3.months.ago + when :weekly then 1.week.ago + when :daily then 1.day.ago end end diff --git a/app/models/topic.rb b/app/models/topic.rb index f9fcfadf52..b076191a8d 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -30,14 +30,14 @@ class Topic < ActiveRecord::Base attr_accessor :allowed_user_ids, :tags_changed def self.max_sort_order - @max_sort_order ||= (2 ** 31) - 1 + @max_sort_order ||= (2**31) - 1 end def featured_users @featured_users ||= TopicFeaturedUsers.new(self) end - def trash!(trashed_by=nil) + def trash!(trashed_by = nil) update_category_topic_count_by(-1) if deleted_at.nil? super(trashed_by) update_flagged_posts_count @@ -57,27 +57,27 @@ class Topic < ActiveRecord::Base rate_limit :limit_topics_per_day rate_limit :limit_private_messages_per_day - validates :title, :if => Proc.new { |t| t.new_record? || t.title_changed? }, - :presence => true, - :topic_title_length => true, - :censored_words => true, - :quality_title => { :unless => :private_message? }, - :unique_among => { :unless => Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) }, - :message => :has_already_been_used, - :allow_blank => true, - :case_sensitive => false, - :collection => Proc.new{ Topic.listable_topics } } + validates :title, if: Proc.new { |t| t.new_record? || t.title_changed? }, + presence: true, + topic_title_length: true, + censored_words: true, + quality_title: { unless: :private_message? }, + unique_among: { unless: Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) }, + message: :has_already_been_used, + allow_blank: true, + case_sensitive: false, + collection: Proc.new { Topic.listable_topics } } validates :category_id, - :presence => true, - :exclusion => { - :in => Proc.new{[SiteSetting.uncategorized_category_id]} + presence: true, + exclusion: { + in: Proc.new { [SiteSetting.uncategorized_category_id] } }, - :if => Proc.new { |t| - (t.new_record? || t.category_id_changed?) && - !SiteSetting.allow_uncategorized_topics && - (t.archetype.nil? || t.archetype == Archetype.default) && - (!t.user_id || !t.user.staff?) + if: Proc.new { |t| + (t.new_record? || t.category_id_changed?) && + !SiteSetting.allow_uncategorized_topics && + (t.archetype.nil? || t.archetype == Archetype.default) && + (!t.user_id || !t.user.staff?) } validates :featured_link, allow_nil: true, format: URI::regexp(%w(http https)) @@ -124,7 +124,7 @@ class Topic < ActiveRecord::Base has_many :topic_timers, dependent: :destroy has_one :user_warning - has_one :first_post, -> {where post_number: 1}, class_name: Post + has_one :first_post, -> { where post_number: 1 }, class_name: Post has_one :topic_embed, dependent: :destroy @@ -152,7 +152,7 @@ class Topic < ActiveRecord::Base scope :created_since, lambda { |time_ago| where('topics.created_at > ?', time_ago) } - scope :secured, lambda { |guardian=nil| + scope :secured, lambda { |guardian = nil| ids = guardian.secure_category_ids if guardian # Query conditions @@ -242,7 +242,7 @@ class Topic < ActiveRecord::Base end end - def self.visible_post_types(viewed_by=nil) + def self.visible_post_types(viewed_by = nil) types = Post.types result = [types[:regular], types[:moderator_action], types[:small_action]] result << types[:whisper] if viewed_by.try(:staff?) @@ -267,8 +267,8 @@ class Topic < ActiveRecord::Base def has_flags? FlagQuery.flagged_post_actions("active") - .where("topics.id" => id) - .exists? + .where("topics.id" => id) + .exists? end def is_official_warning? @@ -324,21 +324,21 @@ class Topic < ActiveRecord::Base end # Returns hot topics since a date for display in email digest. - def self.for_digest(user, since, opts=nil) + def self.for_digest(user, since, opts = nil) opts = opts || {} score = "#{ListController.best_period_for(since)}_score" topics = Topic - .visible - .secured(Guardian.new(user)) - .joins("LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{user.id.to_i}") - .joins("LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id.to_i}") - .joins("LEFT OUTER JOIN users ON users.id = topics.user_id") - .where(closed: false, archived: false) - .where("COALESCE(topic_users.notification_level, 1) <> ?", TopicUser.notification_levels[:muted]) - .created_since(since) - .listable_topics - .includes(:category) + .visible + .secured(Guardian.new(user)) + .joins("LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{user.id.to_i}") + .joins("LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id.to_i}") + .joins("LEFT OUTER JOIN users ON users.id = topics.user_id") + .where(closed: false, archived: false) + .where("COALESCE(topic_users.notification_level, 1) <> ?", TopicUser.notification_levels[:muted]) + .created_since(since) + .listable_topics + .includes(:category) unless opts[:include_tl0] || user.user_option.try(:include_tl0_in_digests) topics = topics.where("COALESCE(users.trust_level, 0) > 0") @@ -346,7 +346,7 @@ class Topic < ActiveRecord::Base if !!opts[:top_order] topics = topics.joins("LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id") - .order(TopicQuerySQL.order_top_with_notification_levels(score)) + .order(TopicQuerySQL.order_top_with_notification_levels(score)) end if opts[:limit] @@ -383,7 +383,7 @@ class Topic < ActiveRecord::Base end # Using the digest query, figure out what's new for a user since last seen - def self.new_since_last_seen(user, since, featured_topic_ids=nil) + def self.new_since_last_seen(user, since, featured_topic_ids = nil) topics = Topic.for_digest(user, since) featured_topic_ids ? topics.where("topics.id NOT IN (?)", featured_topic_ids) : topics end @@ -401,7 +401,7 @@ class Topic < ActiveRecord::Base save end - def reload(options=nil) + def reload(options = nil) @post_numbers = nil @public_topic_timer = nil super(options) @@ -423,7 +423,7 @@ class Topic < ActiveRecord::Base custom_fields[key.to_s] end - def self.listable_count_per_day(start_date, end_date, category_id=nil) + def self.listable_count_per_day(start_date, end_date, category_id = nil) result = listable_topics.where('created_at >= ? and created_at <= ?', start_date, end_date) result = result.where(category_id: category_id) if category_id result.group('date(created_at)').order('date(created_at)').count @@ -435,7 +435,7 @@ class Topic < ActiveRecord::Base MAX_SIMILAR_BODY_LENGTH = 200 # Search for similar topics - def self.similar_to(title, raw, user=nil) + def self.similar_to(title, raw, user = nil) return [] unless title.present? return [] unless raw.present? @@ -445,12 +445,12 @@ class Topic < ActiveRecord::Base # Exclude category definitions from similar topic suggestions candidates = Topic.visible - .secured(Guardian.new(user)) - .listable_topics - .joins('JOIN topic_search_data s ON topics.id = s.topic_id') - .where("search_data @@ #{ts_query}") - .order("ts_rank(search_data, #{ts_query}) DESC") - .limit(SiteSetting.max_similar_results * 3) + .secured(Guardian.new(user)) + .listable_topics + .joins('JOIN topic_search_data s ON topics.id = s.topic_id') + .where("search_data @@ #{ts_query}") + .order("ts_rank(search_data, #{ts_query}) DESC") + .limit(SiteSetting.max_similar_results * 3) exclude_topic_ids = Category.pluck(:topic_id).compact! if exclude_topic_ids.present? @@ -462,16 +462,16 @@ class Topic < ActiveRecord::Base return [] unless candidate_ids.present? similar = Topic.select(sanitize_sql_array(["topics.*, similarity(topics.title, :title) + similarity(topics.title, :raw) AS similarity, p.cooked as blurb", title: title, raw: raw])) - .joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1") - .limit(SiteSetting.max_similar_results) - .where("topics.id IN (?)", candidate_ids) - .where("similarity(topics.title, :title) + similarity(topics.title, :raw) > 0.2", raw: raw, title: title) - .order('similarity desc') + .joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1") + .limit(SiteSetting.max_similar_results) + .where("topics.id IN (?)", candidate_ids) + .where("similarity(topics.title, :title) + similarity(topics.title, :raw) > 0.2", raw: raw, title: title) + .order('similarity desc') similar end - def update_status(status, enabled, user, opts={}) + def update_status(status, enabled, user, opts = {}) TopicStatusUpdater.new(self, user).update!(status, enabled, opts) DiscourseEvent.trigger(:topic_status_updated, self.id, status, enabled) end @@ -504,7 +504,6 @@ class Topic < ActiveRecord::Base end end - def self.reset_all_highest! exec_sql < ?", NotificationLevels.all[:muted] ).find_each do |u| @@ -733,7 +733,8 @@ SQL post_number: 1, data: { topic_title: self.title, - display_username: user.username + display_username: user.username, + group_id: group_id }.to_json ) end @@ -743,10 +744,11 @@ SQL end # Invite a user to the topic by username or email. Returns success/failure - def invite(invited_by, username_or_email, group_ids=nil, custom_message=nil) + def invite(invited_by, username_or_email, group_ids = nil, custom_message = nil) + user = User.find_by_username_or_email(username_or_email) + if private_message? # If the user exists, add them to the message. - user = User.find_by_username_or_email(username_or_email) raise UserExists.new I18n.t("topic_invite.user_exists") if user.present? && topic_allowed_users.where(user_id: user.id).exists? if user && topic_allowed_users.create!(user_id: user.id) @@ -764,18 +766,27 @@ SQL end if username_or_email =~ /^.+@.+$/ && Guardian.new(invited_by).can_invite_via_email?(self) - # rate limit topic invite RateLimiter.new(invited_by, "topic-invitations-per-day", SiteSetting.max_topic_invitations_per_day, 1.day.to_i).performed! - # NOTE callers expect an invite object if an invite was sent via email - invite_by_email(invited_by, username_or_email, group_ids, custom_message) + if user.present? + # add existing users + Invite.extend_permissions(self, user, invited_by) + + # Notify the user they've been invited + user.notifications.create(notification_type: Notification.types[:invited_to_topic], + topic_id: id, + post_number: 1, + data: { topic_title: title, + display_username: invited_by.username }.to_json) + return true + else + # NOTE callers expect an invite object if an invite was sent via email + invite_by_email(invited_by, username_or_email, group_ids, custom_message) + end else - # invite existing member to a topic - user = User.find_by_username(username_or_email) raise UserExists.new I18n.t("topic_invite.user_exists") if user.present? && topic_allowed_users.where(user_id: user.id).exists? if user && topic_allowed_users.create!(user_id: user.id) - # rate limit topic invite RateLimiter.new(invited_by, "topic-invitations-per-day", SiteSetting.max_topic_invitations_per_day, 1.day.to_i).performed! # Notify the user they've been invited @@ -791,16 +802,16 @@ SQL end end - def invite_by_email(invited_by, email, group_ids=nil, custom_message=nil) + def invite_by_email(invited_by, email, group_ids = nil, custom_message = nil) Invite.invite_by_email(email, invited_by, self, group_ids, custom_message) end def email_already_exists_for?(invite) - invite.email_already_exists and private_message? + invite.email_already_exists && private_message? end def grant_permission_to_user(lower_email) - user = User.find_by(email: lower_email) + user = User.find_by_email(lower_email) topic_allowed_users.create!(user_id: user.id) end @@ -895,7 +906,7 @@ SQL slug = Slug.for(t.to_s) write_attribute(:slug, slug) write_attribute(:fancy_title, nil) - write_attribute(:title,t) + write_attribute(:title, t) end # NOTE: These are probably better off somewhere else. @@ -904,7 +915,7 @@ SQL "#{Discourse.base_uri}/t/#{slug}/#{id}/#{posts_count}" end - def self.url(id, slug, post_number=nil) + def self.url(id, slug, post_number = nil) url = "#{Discourse.base_url}/t/#{slug}/#{id}" url << "/#{post_number}" if post_number.to_i > 1 url @@ -914,7 +925,7 @@ SQL self.class.url id, slug, post_number end - def self.relative_url(id, slug, post_number=nil) + def self.relative_url(id, slug, post_number = nil) url = "#{Discourse.base_uri}/t/" url << "#{slug}/" if slug.present? url << id.to_s @@ -922,11 +933,11 @@ SQL url end - def slugless_url(post_number=nil) + def slugless_url(post_number = nil) Topic.relative_url(id, nil, post_number) end - def relative_url(post_number=nil) + def relative_url(post_number = nil) Topic.relative_url(id, slug, post_number) end @@ -944,7 +955,7 @@ SQL TopicUser.change(user.id, id, cleared_pinned_at: nil) end - def update_pinned(status, global=false, pinned_until=nil) + def update_pinned(status, global = false, pinned_until = nil) pinned_until = Time.parse(pinned_until) rescue nil update_columns( @@ -1123,7 +1134,7 @@ SQL ) t SQL - def self.time_to_first_response(sql, opts=nil) + def self.time_to_first_response(sql, opts = nil) opts ||= {} builder = SqlBuilder.new(sql) builder.where("t.created_at >= :start_date", start_date: opts[:start_date]) if opts[:start_date] @@ -1134,17 +1145,17 @@ SQL builder.where("p.deleted_at IS NULL") builder.where("p.post_number > 1") builder.where("p.user_id != t.user_id") - builder.where("p.user_id in (:user_ids)", {user_ids: opts[:user_ids]}) if opts[:user_ids] + builder.where("p.user_id in (:user_ids)", user_ids: opts[:user_ids]) if opts[:user_ids] builder.where("p.post_type = :post_type", post_type: Post.types[:regular]) builder.where("EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0") builder.exec end - def self.time_to_first_response_per_day(start_date, end_date, opts={}) - time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, opts.merge({start_date: start_date, end_date: end_date})) + def self.time_to_first_response_per_day(start_date, end_date, opts = {}) + time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, opts.merge(start_date: start_date, end_date: end_date)) end - def self.time_to_first_response_total(opts=nil) + def self.time_to_first_response_total(opts = nil) total = time_to_first_response(TIME_TO_FIRST_RESPONSE_TOTAL_SQL, opts) total.first["hours"].to_f.round(2) end @@ -1163,7 +1174,7 @@ SQL ORDER BY tt.created_at SQL - def self.with_no_response_per_day(start_date, end_date, category_id=nil) + def self.with_no_response_per_day(start_date, end_date, category_id = nil) builder = SqlBuilder.new(WITH_NO_RESPONSE_SQL) builder.where("t.created_at >= :start_date", start_date: start_date) if start_date builder.where("t.created_at < :end_date", end_date: end_date) if end_date @@ -1185,7 +1196,7 @@ SQL WHERE tt.first_reply IS NULL OR tt.first_reply < 2 SQL - def self.with_no_response_total(opts={}) + def self.with_no_response_total(opts = {}) builder = SqlBuilder.new(WITH_NO_RESPONSE_TOTAL_SQL) builder.where("t.category_id = :category_id", category_id: opts[:category_id]) if opts[:category_id] builder.where("t.archetype <> '#{Archetype.private_message}'") @@ -1232,7 +1243,6 @@ SQL end - # == Schema Information # # Table name: topics diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index dd5bc30559..3ec37b314a 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -17,7 +17,8 @@ class TopicConverter else Category.where(read_restricted: false) .where.not(id: SiteSetting.uncategorized_category_id) - .first.id + .order('id asc') + .pluck(:id).first end @topic.archetype = Archetype.default diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 0f1cc78f7d..9904b05c98 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -113,7 +113,7 @@ class TopicEmbed < ActiveRecord::Base response.title = title doc = Nokogiri::HTML(read_doc.content) - tags = {'img' => 'src', 'script' => 'src', 'a' => 'href'} + tags = { 'img' => 'src', 'script' => 'src', 'a' => 'href' } doc.search(tags.keys.join(',')).each do |node| url_param = tags[node.name] src = node[url_param] @@ -132,7 +132,7 @@ class TopicEmbed < ActiveRecord::Base # only allow classes in the whitelist allowed_classes = if embed_classname_whitelist.blank? then [] else embed_classname_whitelist.split(/[ ,]+/i) end doc.search('[class]:not([class=""])').each do |classnode| - classes = classnode[:class].split(' ').select{ |classname| allowed_classes.include?(classname) } + classes = classnode[:class].split(' ').select { |classname| allowed_classes.include?(classname) } if classes.length === 0 classnode.delete('class') else @@ -145,7 +145,7 @@ class TopicEmbed < ActiveRecord::Base response end - def self.import_remote(import_user, url, opts=nil) + def self.import_remote(import_user, url, opts = nil) opts = opts || {} response = find_remote(url) response.title = opts[:title] if opts[:title].present? diff --git a/app/models/topic_featured_users.rb b/app/models/topic_featured_users.rb index f28c555b4c..d3f705149b 100644 --- a/app/models/topic_featured_users.rb +++ b/app/models/topic_featured_users.rb @@ -10,7 +10,7 @@ class TopicFeaturedUsers end # Chooses which topic users to feature - def choose(args={}) + def choose(args = {}) self.class.ensure_consistency!(topic.id.to_i) update_participant_count end @@ -22,7 +22,7 @@ class TopicFeaturedUsers topic.featured_user4_id].uniq.compact end - def self.ensure_consistency!(topic_id=nil) + def self.ensure_consistency!(topic_id = nil) filter = "#{"AND t.id = #{topic_id.to_i}" if topic_id}" filter2 = "#{"AND tt.id = #{topic_id.to_i}" if topic_id}" diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index 12ae7dcaab..4576a0a601 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -57,6 +57,8 @@ SQL builder.where('ftl.topic_id = :topic_id', topic_id: topic_id) builder.where('ft.deleted_at IS NULL') + # note that ILIKE means "case insensitive LIKE" + builder.where("NOT(ftl.url ILIKE '%.png' OR ftl.url ILIKE '%.jpg' OR ftl.url ILIKE '%.gif')") builder.where("COALESCE(ft.archetype, 'regular') <> :archetype", archetype: Archetype.private_message) builder.secure_category(guardian.secure_category_ids) @@ -65,7 +67,7 @@ SQL end - def self.counts_for(guardian,topic, posts) + def self.counts_for(guardian, topic, posts) return {} if posts.blank? # Sam: I don't know how to write this cleanly in AR, @@ -91,13 +93,13 @@ SQL builder.where('l.post_id IN (:post_ids)', post_ids: posts.map(&:id)) builder.secure_category(guardian.secure_category_ids) - builder.map_exec(OpenStruct).each_with_object({}) do |l,result| + builder.map_exec(OpenStruct).each_with_object({}) do |l, result| result[l.post_id] ||= [] - result[l.post_id] << {url: l.url, - clicks: l.clicks, - title: l.title, - internal: l.internal, - reflection: l.reflection} + result[l.post_id] << { url: l.url, + clicks: l.clicks, + title: l.title, + internal: l.internal, + reflection: l.reflection } end end @@ -122,17 +124,12 @@ SQL internal = false topic_id = nil post_number = nil - parsed_path = parsed.path || "" if Discourse.store.has_been_uploaded?(url) internal = Discourse.store.internal? - elsif (parsed.host == Discourse.current_hostname && parsed_path.start_with?(Discourse.base_uri)) || !parsed.host + elsif route = Discourse.route_for(parsed) internal = true - parsed_path.slice!(Discourse.base_uri) - - route = Rails.application.routes.recognize_path(parsed_path) - # We aren't interested in tracking internal links to users next if route[:controller] == 'users' @@ -164,6 +161,7 @@ SQL added_urls << url unless TopicLink.exists?(topic_id: post.topic_id, post_id: post.id, url: url) + file_extension = File.extname(parsed.path)[1..10].downcase unless File.extname(parsed.path).empty? begin TopicLink.create!(post_id: post.id, user_id: post.user_id, @@ -173,7 +171,8 @@ SQL internal: internal, link_topic_id: topic_id, link_post_id: reflected_post.try(:id), - quote: link.is_quote) + quote: link.is_quote, + extension: file_extension) rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation # it's fine end @@ -192,14 +191,14 @@ SQL unless tl tl = TopicLink.create(user_id: post.user_id, - topic_id: topic_id, - post_id: reflected_post.try(:id), - url: reflected_url, - domain: Discourse.current_hostname, - reflection: true, - internal: true, - link_topic_id: post.topic_id, - link_post_id: post.id) + topic_id: topic_id, + post_id: reflected_post.try(:id), + url: reflected_url, + domain: Discourse.current_hostname, + reflection: true, + internal: true, + link_topic_id: post.topic_id, + link_post_id: post.id) end @@ -238,11 +237,11 @@ SQL def self.duplicate_lookup(topic) results = TopicLink - .includes(:post, :user) - .joins(:post, :user) - .where("posts.id IS NOT NULL AND users.id IS NOT NULL") - .where(topic_id: topic.id, reflection: false) - .last(200) + .includes(:post, :user) + .joins(:post, :user) + .where("posts.id IS NOT NULL AND users.id IS NOT NULL") + .where(topic_id: topic.id, reflection: false) + .last(200) lookup = {} results.each do |tl| diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index 6bd932c796..825067384f 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -12,7 +12,7 @@ class TopicLinkClick < ActiveRecord::Base WHITELISTED_REDIRECT_HOSTNAMES = Set.new(%W{www.youtube.com youtu.be}) # Create a click from a URL and post_id - def self.create_from(args={}) + def self.create_from(args = {}) url = args[:url][0...TopicLink.max_url_length] return nil if url.blank? @@ -31,7 +31,7 @@ class TopicLinkClick < ActiveRecord::Base query = url.index('?') unless query.nil? endpos = url.index('#') || url.size - urls << url[0..query-1] + url[endpos..-1] + urls << url[0..query - 1] + url[endpos..-1] end # add a cdn link diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index efa8089202..375659dc5c 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -22,7 +22,7 @@ class TopicList def self.preload(topics, object) if @preload - @preload.each{|preload| preload.call(topics, object)} + @preload.each { |preload| preload.call(topics, object) } end end @@ -37,7 +37,7 @@ class TopicList :tags, :current_user - def initialize(filter, current_user, topics, opts=nil) + def initialize(filter, current_user, topics, opts = nil) @filter = filter @current_user = current_user @topics_input = topics @@ -86,7 +86,7 @@ class TopicList # Include bookmarks if you have bookmarked topics if @current_user && !post_action_type - post_action_type = PostActionType.types[:bookmark] if @topic_lookup.any?{|_,tu| tu && tu.bookmarked} + post_action_type = PostActionType.types[:bookmark] if @topic_lookup.any? { |_, tu| tu && tu.bookmarked } end # Data for bookmarks or likes @@ -105,7 +105,7 @@ class TopicList ft.user_data = @topic_lookup[ft.id] if @topic_lookup.present? if ft.user_data && post_action_lookup && actions = post_action_lookup[ft.id] - ft.user_data.post_action_data = {post_action_type => actions} + ft.user_data.post_action_data = { post_action_type => actions } end ft.posters = ft.posters_summary( @@ -127,6 +127,6 @@ class TopicList end def attributes - {'more_topics_url' => page} + { 'more_topics_url' => page } end end diff --git a/app/models/topic_notifier.rb b/app/models/topic_notifier.rb index db30a8e471..46d43c3755 100644 --- a/app/models/topic_notifier.rb +++ b/app/models/topic_notifier.rb @@ -3,10 +3,10 @@ class TopicNotifier @topic = topic end - { :watch! => :watching, - :track! => :tracking, - :regular! => :regular, - :mute! => :muted }.each_pair do |method_name, level| + { watch!: :watching, + track!: :tracking, + regular!: :regular, + mute!: :muted }.each_pair do |method_name, level| define_method method_name do |user_id| change_level user_id, level @@ -34,8 +34,8 @@ class TopicNotifier @notification_levels ||= TopicUser.notification_levels end - def change_level(user_id, level, reason=nil) - attrs = {notification_level: levels[level]} + def change_level(user_id, level, reason = nil) + attrs = { notification_level: levels[level] } attrs.merge!(notifications_reason_id: TopicUser.notification_reasons[reason]) if reason TopicUser.change(user_id, @topic.id, attrs) end diff --git a/app/models/topic_posters_summary.rb b/app/models/topic_posters_summary.rb index 7723bf338c..c6cf8255d5 100644 --- a/app/models/topic_posters_summary.rb +++ b/app/models/topic_posters_summary.rb @@ -40,7 +40,7 @@ class TopicPostersSummary def shuffle_last_poster_to_back_in(summary) unless last_poster_is_topic_creator? - summary.reject!{ |u| u.id == topic.last_post_user_id } + summary.reject! { |u| u.id == topic.last_post_user_id } summary << avatar_lookup[topic.last_post_user_id] end summary diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index ff30630353..3c104f4fdb 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -38,7 +38,7 @@ class TopicTrackingState publish_read(topic.id, 1, topic.user_id) end - def self.publish_latest(topic, staff_only=false) + def self.publish_latest(topic, staff_only = false) return unless topic.archetype == "regular" message = { @@ -73,9 +73,9 @@ class TopicTrackingState end TopicUser - .tracking(post.topic_id) - .select([:user_id,:last_read_post_number, :notification_level]) - .each do |tu| + .tracking(post.topic_id) + .select([:user_id, :last_read_post_number, :notification_level]) + .each do |tu| message = { topic_id: post.topic_id, @@ -125,7 +125,7 @@ class TopicTrackingState MessageBus.publish("/delete", message.as_json, group_ids: group_ids) end - def self.publish_read(topic_id, last_read_post_number, user_id, notification_level=nil) + def self.publish_read(topic_id, last_read_post_number, user_id, notification_level = nil) highest_post_number = Topic.where(id: topic_id).pluck(:highest_post_number).first @@ -179,8 +179,7 @@ class TopicTrackingState end - - def self.report_raw_sql(opts=nil) + def self.report_raw_sql(opts = nil) unread = if opts && opts[:skip_unread] @@ -205,7 +204,6 @@ class TopicTrackingState c.id AS category_id, tu.notification_level" - sql = <= :tracking", + .where("COALESCE(topic_users.notification_level, :regular) >= :tracking", regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking]) } @@ -159,22 +159,22 @@ SQL unless attrs[:notification_level] category_notification_level = CategoryUser.where(user_id: user_id) - .where("category_id IN (SELECT category_id FROM topics WHERE id = :id)", id: topic_id) - .where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching], + .where("category_id IN (SELECT category_id FROM topics WHERE id = :id)", id: topic_id) + .where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching], CategoryUser.notification_levels[:tracking]]) - .order("notification_level DESC") - .limit(1) - .pluck(:notification_level) - .first + .order("notification_level DESC") + .limit(1) + .pluck(:notification_level) + .first tag_notification_level = TagUser.where(user_id: user_id) - .where("tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :id)", id: topic_id) - .where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching], + .where("tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :id)", id: topic_id) + .where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching], CategoryUser.notification_levels[:tracking]]) - .order("notification_level DESC") - .limit(1) - .pluck(:notification_level) - .first + .order("notification_level DESC") + .limit(1) + .pluck(:notification_level) + .first if category_notification_level && !(tag_notification_level && (tag_notification_level > category_notification_level)) attrs[:notification_level] = category_notification_level @@ -191,7 +191,6 @@ SQL TopicUser.notification_reasons[:auto_track_tag] end - end unless attrs[:notification_level] @@ -203,7 +202,7 @@ SQL end end - TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id, first_visited_at: now ,last_visited_at: now)) + TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id, first_visited_at: now , last_visited_at: now)) end def track_visit!(topic_id, user_id) @@ -256,7 +255,7 @@ SQL INSERT_TOPIC_USER_SQL_STAFF = INSERT_TOPIC_USER_SQL.gsub("highest_post_number", "highest_staff_post_number") - def update_last_read(user, topic_id, post_number, msecs, opts={}) + def update_last_read(user, topic_id, post_number, msecs, opts = {}) return if post_number.blank? msecs = 0 if msecs.to_i < 0 @@ -276,11 +275,12 @@ SQL # ... user visited the topic but did not read the posts # # 86400000 = 1 day - rows = if user.staff? - exec_sql(UPDATE_TOPIC_USER_SQL_STAFF,args).values - else - exec_sql(UPDATE_TOPIC_USER_SQL,args).values - end + rows = + if user.staff? + exec_sql(UPDATE_TOPIC_USER_SQL_STAFF, args).values + else + exec_sql(UPDATE_TOPIC_USER_SQL, args).values + end if rows.length == 1 before = rows[0][1].to_i @@ -332,7 +332,7 @@ SQL end - def self.update_post_action_cache(opts={}) + def self.update_post_action_cache(opts = {}) user_id = opts[:user_id] post_id = opts[:post_id] topic_id = opts[:topic_id] @@ -412,7 +412,7 @@ SQL TopicUser.exec_sql(sql, user_id: user_id, count: count) end - def self.ensure_consistency!(topic_id=nil) + def self.ensure_consistency!(topic_id = nil) update_post_action_cache(topic_id: topic_id) # TODO this needs some reworking, when we mark stuff skipped diff --git a/app/models/topic_view_item.rb b/app/models/topic_view_item.rb index 2b6fa6b49c..04b231fa9b 100644 --- a/app/models/topic_view_item.rb +++ b/app/models/topic_view_item.rb @@ -6,7 +6,7 @@ class TopicViewItem < ActiveRecord::Base belongs_to :user validates_presence_of :topic_id, :ip_address, :viewed_at - def self.add(topic_id, ip, user_id=nil, at=nil, skip_redis=false) + def self.add(topic_id, ip, user_id = nil, at = nil, skip_redis = false) # Only store a view once per day per thing per user per ip at ||= Date.today redis_key = "view:#{topic_id}:#{at}" @@ -28,7 +28,6 @@ class TopicViewItem < ActiveRecord::Base /*where*/ )" - builder = SqlBuilder.new(sql) if !user_id diff --git a/app/models/translation_override.rb b/app/models/translation_override.rb index 11aba82a46..3cdff253b5 100644 --- a/app/models/translation_override.rb +++ b/app/models/translation_override.rb @@ -30,7 +30,7 @@ class TranslationOverride < ActiveRecord::Base def self.i18n_changed I18n.reload! - MessageBus.publish('/i18n-flush', { refresh: true }) + MessageBus.publish('/i18n-flush', refresh: true) end def check_interpolation_keys @@ -42,7 +42,6 @@ class TranslationOverride < ActiveRecord::Base original_interpolation_keys = I18nInterpolationKeysFinder.find(original_text) new_interpolation_keys = I18nInterpolationKeysFinder.find(value) missing_keys = (original_interpolation_keys - new_interpolation_keys) - invalid_keys = (original_interpolation_keys | new_interpolation_keys) - original_interpolation_keys if missing_keys.present? self.errors.add(:base, I18n.t( @@ -52,17 +51,6 @@ class TranslationOverride < ActiveRecord::Base return false end - - invalid_keys = (original_interpolation_keys | new_interpolation_keys) - original_interpolation_keys - - if invalid_keys.present? - self.errors.add(:base, I18n.t( - 'activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys', - keys: invalid_keys.join(', ') - )) - - return false - end end end diff --git a/app/models/trust_level3_requirements.rb b/app/models/trust_level3_requirements.rb index 2579216f86..448f94e672 100644 --- a/app/models/trust_level3_requirements.rb +++ b/app/models/trust_level3_requirements.rb @@ -130,11 +130,11 @@ class TrustLevel3Requirements def num_flagged_posts PostAction.with_deleted - .where(post_id: flagged_post_ids) - .where.not(user_id: @user.id) - .where.not(agreed_at: nil) - .pluck(:post_id) - .uniq.count + .where(post_id: flagged_post_ids) + .where.not(user_id: @user.id) + .where.not(agreed_at: nil) + .pluck(:post_id) + .uniq.count end def max_flagged_posts @@ -143,11 +143,11 @@ class TrustLevel3Requirements def num_flagged_by_users @_num_flagged_by_users ||= PostAction.with_deleted - .where(post_id: flagged_post_ids) - .where.not(user_id: @user.id) - .where.not(agreed_at: nil) - .pluck(:user_id) - .uniq.count + .where(post_id: flagged_post_ids) + .where.not(user_id: @user.id) + .where.not(agreed_at: nil) + .pluck(:user_id) + .uniq.count end def max_flagged_by_users @@ -194,13 +194,11 @@ class TrustLevel3Requirements (min_likes_received.to_f / 4.0).ceil end - def self.clear_cache $redis.del NUM_TOPICS_KEY $redis.del NUM_POSTS_KEY end - CACHE_DURATION = 1.day.seconds - 60 NUM_TOPICS_KEY = "tl3_num_topics" NUM_POSTS_KEY = "tl3_num_posts" @@ -223,8 +221,8 @@ class TrustLevel3Requirements def flagged_post_ids @_flagged_post_ids ||= @user.posts - .with_deleted - .where('created_at > ? AND (spam_count > 0 OR inappropriate_count > 0)', time_period.days.ago) - .pluck(:id) + .with_deleted + .where('created_at > ? AND (spam_count > 0 OR inappropriate_count > 0)', time_period.days.ago) + .pluck(:id) end end diff --git a/app/models/upload.rb b/app/models/upload.rb index d40ac15836..f9886d8998 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -30,7 +30,7 @@ class Upload < ActiveRecord::Base thumbnail(width, height).present? end - def create_thumbnail!(width, height, crop=false) + def create_thumbnail!(width, height, crop = false) return unless SiteSetting.create_thumbnails? opts = { @@ -53,10 +53,6 @@ class Upload < ActiveRecord::Base end end - def extension - File.extname(original_filename) - end - def self.generate_digest(path) Digest::SHA1.file(path).hexdigest end @@ -75,7 +71,7 @@ class Upload < ActiveRecord::Base Upload.find_by(url: url) end - def self.migrate_to_new_scheme(limit=nil) + def self.migrate_to_new_scheme(limit = nil) problems = [] if SiteSetting.migrate_to_new_scheme @@ -109,9 +105,7 @@ class Upload < ActiveRecord::Base upload.sha1 = Upload.generate_digest(path) end # optimize if image - if FileHelper.is_image?(File.basename(path)) - ImageOptim.new.optimize_image!(path) - end + FileHelper.optimize_image!(path) if FileHelper.is_image?(File.basename(path)) # store to new location & update the filesize File.open(path) do |f| upload.url = Discourse.store.store_upload(f, upload) diff --git a/app/models/user.rb b/app/models/user.rb index 38db6852c9..59c7981181 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,7 +42,9 @@ class User < ActiveRecord::Base has_many :email_change_requests, dependent: :destroy has_many :directory_items, dependent: :delete_all has_many :user_auth_tokens, dependent: :destroy + has_many :user_emails, dependent: :destroy + has_one :primary_email, -> { where(primary: true) }, class_name: 'UserEmail', dependent: :destroy has_one :user_option, dependent: :destroy has_one :user_avatar, dependent: :destroy @@ -72,18 +74,15 @@ class User < ActiveRecord::Base has_many :acting_group_histories, dependent: :destroy, foreign_key: :acting_user_id, class_name: GroupHistory has_many :targeted_group_histories, dependent: :destroy, foreign_key: :target_user_id, class_name: GroupHistory - delegate :last_sent_email_address, :to => :email_logs - - before_validation :strip_downcase_email + delegate :last_sent_email_address, to: :email_logs validates_presence_of :username validate :username_validator, if: :username_changed? - validates :email, presence: true, uniqueness: true - validates :email, format: { with: EmailValidator.email_regex }, if: :email_changed? - validates :email, email: true, if: :should_validate_email? validate :password_validator validates :name, user_full_name: true, if: :name_changed?, length: { maximum: 255 } - validates :ip_address, allowed_ip_address: {on: :create, message: :signup_not_allowed} + validates :ip_address, allowed_ip_address: { on: :create, message: :signup_not_allowed } + validates :primary_email, presence: true + validates_associated :primary_email, if: :should_validate_primary_email? after_initialize :add_trust_level @@ -123,6 +122,8 @@ class User < ActiveRecord::Base # set to true to optimize creation and save for imports attr_accessor :import_mode + scope :with_email, ->(email) { joins(:user_emails).where(user_emails: { email: email }) } + scope :human_users, -> { where('users.id > 0') } # excluding fake users like the system user or anonymous users @@ -232,20 +233,19 @@ class User < ActiveRecord::Base end def self.find_by_email(email) - find_by(email: Email.downcase(email)) + self.with_email(Email.downcase(email)).first end def self.find_by_username(username) find_by(username_lower: username.downcase) end - def enqueue_welcome_message(message_type) return unless SiteSetting.send_welcome_message? Jobs.enqueue(:send_system_message, user_id: id, message_type: message_type) end - def change_username(new_username, actor=nil) + def change_username(new_username, actor = nil) UsernameChanger.change(self, new_username, actor) end @@ -267,12 +267,12 @@ class User < ActiveRecord::Base used_invite.try(:invited_by) end - def should_validate_email? - return !skip_email_validation && !staged? && email_changed? + def should_validate_primary_email? + !skip_email_validation && !staged? end # Approve this user - def approve(approved_by, send_mail=true) + def approve(approved_by, send_mail = true) self.approved = true if approved_by.is_a?(Integer) @@ -317,7 +317,7 @@ class User < ActiveRecord::Base n.user_id = :user_id AND NOT read" - User.exec_sql(sql, user_id: id, type: notification_type).getvalue(0,0).to_i + User.exec_sql(sql, user_id: id, type: notification_type).getvalue(0, 0).to_i end def unread_private_messages @@ -341,7 +341,7 @@ class User < ActiveRecord::Base User.exec_sql(sql, user_id: id, seen_notification_id: seen_notification_id, pm: Notification.types[:private_message]) - .getvalue(0,0).to_i + .getvalue(0, 0).to_i end end @@ -376,7 +376,6 @@ class User < ActiveRecord::Base notification = notifications.visible.order('notifications.id desc').first json = NotificationSerializer.new(notification).as_json if notification - sql = " SELECT * FROM ( SELECT n.id, n.read FROM notifications n @@ -403,18 +402,18 @@ class User < ActiveRecord::Base " recent = User.exec_sql(sql, user_id: id, - type: Notification.types[:private_message]).values.map do |id, read| + type: Notification.types[:private_message]).values.map do |id, read| [id.to_i, read == 't'.freeze] end MessageBus.publish("/notification/#{id}", - {unread_notifications: unread_notifications, - unread_private_messages: unread_private_messages, - total_unread_notifications: total_unread_notifications, - read_first_notification: read_first_notification?, - last_notification: json, - recent: recent, - seen_notification_id: seen_notification_id + { unread_notifications: unread_notifications, + unread_private_messages: unread_private_messages, + total_unread_notifications: total_unread_notifications, + read_first_notification: read_first_notification?, + last_notification: json, + recent: recent, + seen_notification_id: seen_notification_id }, user_ids: [id] # only publish the notification to this user ) @@ -474,7 +473,7 @@ class User < ActiveRecord::Base last_seen_at.present? end - def create_visit_record!(date, opts={}) + def create_visit_record!(date, opts = {}) user_stat.update_column(:days_visited, user_stat.days_visited + 1) user_visits.create!(visited_at: date, posts_read: opts[:posts_read] || 0, mobile: opts[:mobile] || false) end @@ -487,7 +486,7 @@ class User < ActiveRecord::Base create_visit_record!(date) unless visit_record_for(date) end - def update_posts_read!(num_posts, opts={}) + def update_posts_read!(num_posts, opts = {}) now = opts[:at] || Time.zone.now _retry = opts[:retry] || false @@ -501,7 +500,7 @@ class User < ActiveRecord::Base create_visit_record!(now.to_date, posts_read: num_posts, mobile: opts.fetch(:mobile, false)) rescue ActiveRecord::RecordNotUnique if !_retry - update_posts_read!(num_posts, opts.merge( retry: true )) + update_posts_read!(num_posts, opts.merge(retry: true)) else raise end @@ -515,7 +514,7 @@ class User < ActiveRecord::Base end end - def update_last_seen!(now=Time.zone.now) + def update_last_seen!(now = Time.zone.now) now_date = now.to_date # Only update last seen once every minute redis_key = "user:#{id}:#{now_date}" @@ -702,7 +701,7 @@ class User < ActiveRecord::Base save end - def change_trust_level!(level, opts=nil) + def change_trust_level!(level, opts = nil) Promotion.new(self).change_trust_level!(level, opts) end @@ -715,26 +714,26 @@ class User < ActiveRecord::Base user_badges.select('distinct badge_id').count end - def featured_user_badges(limit=3) + def featured_user_badges(limit = 3) tl_badge_ids = Badge.trust_level_badge_ids query = user_badges - .group(:badge_id) - .select(UserBadge.attribute_names.map { |x| "MAX(user_badges.#{x}) AS #{x}" }, + .group(:badge_id) + .select(UserBadge.attribute_names.map { |x| "MAX(user_badges.#{x}) AS #{x}" }, 'COUNT(*) AS "count"', 'MAX(badges.badge_type_id) AS badges_badge_type_id', 'MAX(badges.grant_count) AS badges_grant_count') - .joins(:badge) - .order('badges_badge_type_id ASC, badges_grant_count ASC, badge_id DESC') - .includes(:user, :granted_by, { badge: :badge_type }, { post: :topic }) + .joins(:badge) + .order('badges_badge_type_id ASC, badges_grant_count ASC, badge_id DESC') + .includes(:user, :granted_by, { badge: :badge_type }, post: :topic) tl_badge = query.where("user_badges.badge_id IN (:tl_badge_ids)", tl_badge_ids: tl_badge_ids) - .limit(1) + .limit(1) other_badges = query.where("user_badges.badge_id NOT IN (:tl_badge_ids)", tl_badge_ids: tl_badge_ids) - .limit(limit) + .limit(limit) (tl_badge + other_badges).take(limit) end @@ -749,7 +748,6 @@ class User < ActiveRecord::Base result.group('date(users.created_at)').order('date(users.created_at)').count end - def secure_category_ids cats = self.admin? ? Category.where(read_restricted: true) : secure_categories.references(:categories) cats.pluck('categories.id').sort @@ -759,16 +757,15 @@ class User < ActiveRecord::Base Category.topic_create_allowed(self.id).select(:id) end - # Flag all posts from a user as spam def flag_linked_posts_as_spam disagreed_flag_post_ids = PostAction.where(post_action_type_id: PostActionType.types[:spam]) - .where.not(disagreed_at: nil) - .pluck(:post_id) + .where.not(disagreed_at: nil) + .pluck(:post_id) topic_links.includes(:post) - .where.not(post_id: disagreed_flag_post_ids) - .each do |tl| + .where.not(post_id: disagreed_flag_post_ids) + .each do |tl| begin message = I18n.t('flag_reason.spam_hosts', domain: tl.domain) PostAction.act(Discourse.system_user, tl.post, PostActionType.types[:spam], message: message) @@ -811,7 +808,6 @@ class User < ActiveRecord::Base .exists? end - def refresh_avatar return if @import_mode @@ -867,25 +863,25 @@ class User < ActiveRecord::Base def number_of_deleted_posts Post.with_deleted - .where(user_id: self.id) - .where.not(deleted_at: nil) - .count + .where(user_id: self.id) + .where.not(deleted_at: nil) + .count end def number_of_flagged_posts Post.with_deleted - .where(user_id: self.id) - .where(id: PostAction.where(post_action_type_id: PostActionType.notify_flag_type_ids) + .where(user_id: self.id) + .where(id: PostAction.where(post_action_type_id: PostActionType.notify_flag_type_ids) .where(disagreed_at: nil) .select(:post_id)) - .count + .count end def number_of_flags_given PostAction.where(user_id: self.id) - .where(disagreed_at: nil) - .where(post_action_type_id: PostActionType.notify_flag_type_ids) - .count + .where(disagreed_at: nil) + .where(post_action_type_id: PostActionType.notify_flag_type_ids) + .count end def number_of_suspensions @@ -923,8 +919,8 @@ class User < ActiveRecord::Base return unless active && email_confirmed? && !staged Group.where(automatic: false) - .where("LENGTH(COALESCE(automatic_membership_email_domains, '')) > 0") - .each do |group| + .where("LENGTH(COALESCE(automatic_membership_email_domains, '')) > 0") + .each do |group| domains = group.automatic_membership_email_domains.gsub('.', '\.') @@ -935,6 +931,18 @@ class User < ActiveRecord::Base end end + def email + primary_email.email + end + + def email=(email) + if primary_email + self.new_record? ? primary_email.email = email : primary_email.update(email: email) + else + build_primary_email(email: email) + end + end + protected def badge_grant @@ -1011,13 +1019,6 @@ class User < ActiveRecord::Base self.username_lower = username.downcase end - def strip_downcase_email - if self.email - self.email = self.email.strip - self.email = self.email.downcase - end - end - def username_validator username_format_validator || begin lower = username.downcase @@ -1058,10 +1059,10 @@ class User < ActiveRecord::Base # Delete unactivated accounts (without verified email) that are over a week old def self.purge_unactivated to_destroy = User.where(active: false) - .joins('INNER JOIN user_stats AS us ON us.user_id = users.id') - .where("created_at < ?", SiteSetting.purge_unactivated_users_grace_period_days.days.ago) - .where('NOT admin AND NOT moderator') - .limit(200) + .joins('INNER JOIN user_stats AS us ON us.user_id = users.id') + .where("created_at < ?", SiteSetting.purge_unactivated_users_grace_period_days.days.ago) + .where('NOT admin AND NOT moderator') + .limit(200) destroyer = UserDestroyer.new(Discourse.system_user) to_destroy.each do |u| @@ -1104,7 +1105,7 @@ end # name :string # seen_notification_id :integer default(0), not null # last_posted_at :datetime -# email :string(513) not null +# email :string(513) # password_hash :string(64) # salt :string(32) # active :boolean default(FALSE), not null diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 5e3599a381..b65f207af3 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -11,7 +11,7 @@ class UserAction < ActiveRecord::Base BOOKMARK = 3 NEW_TOPIC = 4 REPLY = 5 - RESPONSE= 6 + RESPONSE = 6 MENTION = 7 QUOTE = 9 EDIT = 11 @@ -75,7 +75,7 @@ SQL apply_common_filters(builder, user_id, guardian) results = builder.exec.to_a - results.sort! { |a,b| ORDER[a.action_type] <=> ORDER[b.action_type] } + results.sort! { |a, b| ORDER[a.action_type] <=> ORDER[b.action_type] } results end @@ -109,10 +109,10 @@ SQL GROUP BY g.name SQL - result = { all: all, mine: mine, unread: unread} + result = { all: all, mine: mine, unread: unread } exec_sql(sql, user_id: user_id).each do |row| - (result[:groups] ||= []) << {name: row["name"], count: row["count"].to_i} + (result[:groups] ||= []) << { name: row["name"], count: row["count"].to_i } end result @@ -123,7 +123,7 @@ SQL stream(action_id: action_id, guardian: guardian).first end - def self.stream_queued(opts=nil) + def self.stream_queued(opts = nil) opts ||= {} offset = opts[:offset] || 0 @@ -156,7 +156,7 @@ SQL .map_exec(UserActionRow) end - def self.stream(opts=nil) + def self.stream(opts = nil) opts ||= {} action_types = opts[:action_types] @@ -276,7 +276,7 @@ SQL require_parameters(hash, :action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id) if action = UserAction.find_by(hash.except(:created_at)) action.destroy - MessageBus.publish("/user/#{hash[:user_id]}", {user_action_id: action.id, remove: true}) + MessageBus.publish("/user/#{hash[:user_id]}", user_action_id: action.id, remove: true) end if !Topic.where(id: hash[:target_topic_id], archetype: Archetype.private_message).exists? @@ -331,7 +331,7 @@ SQL end end - def self.apply_common_filters(builder,user_id,guardian,ignore_private_messages=false) + def self.apply_common_filters(builder, user_id, guardian, ignore_private_messages = false) # We never return deleted topics in activity builder.where("t.deleted_at is null") @@ -341,7 +341,7 @@ SQL current_user_id = -2 current_user_id = guardian.user.id if guardian.user - builder.where("NOT COALESCE(p.hidden, false) OR p.user_id = :current_user_id", current_user_id: current_user_id ) + builder.where("NOT COALESCE(p.hidden, false) OR p.user_id = :current_user_id", current_user_id: current_user_id) end visible_post_types = Topic.visible_post_types(guardian.user) @@ -381,7 +381,7 @@ SQL if allowed.present? builder.where("( c.read_restricted IS NULL OR NOT c.read_restricted OR - (c.read_restricted and c.id in (:cats)) )", cats: guardian.secure_category_ids ) + (c.read_restricted and c.id in (:cats)) )", cats: guardian.secure_category_ids) else builder.where("(c.read_restricted IS NULL OR NOT c.read_restricted)") end diff --git a/app/models/user_api_key.rb b/app/models/user_api_key.rb index be0e85b7f8..39f8d4a80f 100644 --- a/app/models/user_api_key.rb +++ b/app/models/user_api_key.rb @@ -30,7 +30,7 @@ class UserApiKey < ActiveRecord::Base # not a rails route, special handling return true if action == "message_bus" && env["PATH_INFO"] =~ /^\/message-bus\/.*\/poll/ - params = env['action_dispatch.request.path_parameters'] + params = env['action_dispatch.request.path_parameters'] return false unless params diff --git a/app/models/user_archived_message.rb b/app/models/user_archived_message.rb index 1cb45c8165..36c3ccd1ac 100644 --- a/app/models/user_archived_message.rb +++ b/app/models/user_archived_message.rb @@ -11,21 +11,21 @@ class UserArchivedMessage < ActiveRecord::Base UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all trigger(:move_to_inbox, user_id, topic_id) - MessageBus.publish("/topic/#{topic_id}", {type: "move_to_inbox"}, user_ids: [user_id]) + MessageBus.publish("/topic/#{topic_id}", { type: "move_to_inbox" }, user_ids: [user_id]) end def self.archive!(user_id, topic_id) UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all UserArchivedMessage.create!(user_id: user_id, topic_id: topic_id) trigger(:archive_message, user_id, topic_id) - MessageBus.publish("/topic/#{topic_id}", {type: "archived"}, user_ids: [user_id]) + MessageBus.publish("/topic/#{topic_id}", { type: "archived" }, user_ids: [user_id]) end def self.trigger(event, user_id, topic_id) user = User.find_by(id: user_id) topic = Topic.find_by(id: topic_id) if user && topic - DiscourseEvent.trigger(event, {user: user, topic: topic}) + DiscourseEvent.trigger(event, user: user, topic: topic) end end end diff --git a/app/models/user_auth_token.rb b/app/models/user_auth_token.rb index 7a22ea5931..5597d52367 100644 --- a/app/models/user_auth_token.rb +++ b/app/models/user_auth_token.rb @@ -40,7 +40,7 @@ class UserAuthToken < ActiveRecord::Base user_auth_token end - def self.lookup(unhashed_token, opts=nil) + def self.lookup(unhashed_token, opts = nil) mark_seen = opts && opts[:seen] @@ -123,7 +123,7 @@ class UserAuthToken < ActiveRecord::Base end - def rotate!(info=nil) + def rotate!(info = nil) user_agent = (info && info[:user_agent] || self.user_agent) client_ip = (info && info[:client_ip] || self.client_ip) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 0c49693ddb..8e9de17baf 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -67,7 +67,7 @@ class UserAvatar < ActiveRecord::Base "#{upload_id}_#{OptimizedImage::VERSION}" end - def self.import_url_for_user(avatar_url, user, options=nil) + def self.import_url_for_user(avatar_url, user, options = nil) tempfile = FileHelper.download( avatar_url, max_file_size: SiteSetting.max_image_size_kb.kilobytes, diff --git a/app/models/user_badge.rb b/app/models/user_badge.rb index a50bf1be72..f7b8b55569 100644 --- a/app/models/user_badge.rb +++ b/app/models/user_badge.rb @@ -5,7 +5,7 @@ class UserBadge < ActiveRecord::Base belongs_to :notification, dependent: :destroy belongs_to :post - validates :badge_id, presence: true, uniqueness: {scope: :user_id}, if: 'badge.single_grant?' + validates :badge_id, presence: true, uniqueness: { scope: :user_id }, if: 'badge.single_grant?' validates :user_id, presence: true validates :granted_at, presence: true validates :granted_by, presence: true diff --git a/app/models/user_badges.rb b/app/models/user_badges.rb index 187ee1bc9f..bd68c4c037 100644 --- a/app/models/user_badges.rb +++ b/app/models/user_badges.rb @@ -4,7 +4,7 @@ class UserBadges attr_accessor :user_badges, :username, :grant_count - def initialize(opts={}) + def initialize(opts = {}) @user_badges = opts[:user_badges] @username = opts[:username] @grant_count = opts[:grant_count] diff --git a/app/models/user_email.rb b/app/models/user_email.rb new file mode 100644 index 0000000000..b67a6cd3ca --- /dev/null +++ b/app/models/user_email.rb @@ -0,0 +1,45 @@ +require_dependency 'email_validator' + +class UserEmail < ActiveRecord::Base + belongs_to :user + + before_validation :strip_downcase_email + + validates :email, presence: true, uniqueness: true + + validates :email, email: true, format: { with: EmailValidator.email_regex }, + if: :skip_email_validation? + + validates :primary, uniqueness: { scope: [:user_id] } + + private + + def strip_downcase_email + if self.email + self.email = self.email.strip + self.email = self.email.downcase + end + end + + def skip_email_validation? + return true if user && user.skip_email_validation + email_changed? + end +end + +# == Schema Information +# +# Table name: user_emails +# +# id :integer not null, primary key +# user_id :integer not null +# email :string(513) not null +# primary :boolean default(FALSE), not null +# created_at :datetime +# updated_at :datetime +# +# Indexes +# +# index_user_emails_on_user_id (user_id) +# index_user_emails_on_user_id_and_primary (user_id,primary) UNIQUE +# diff --git a/app/models/user_history.rb b/app/models/user_history.rb index e177cd8de4..c166587468 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -11,7 +11,7 @@ class UserHistory < ActiveRecord::Base validates_presence_of :action - scope :only_staff_actions, ->{ where("action IN (?)", UserHistory.staff_action_ids) } + scope :only_staff_actions, -> { where("action IN (?)", UserHistory.staff_action_ids) } before_save :set_admin_only @@ -121,7 +121,7 @@ class UserHistory < ActiveRecord::Base query = query.where(custom_type: filters[:custom_type]) if filters[:custom_type].present? [:acting_user, :target_user].each do |key| - if filters[key] and obj_id = User.where(username_lower: filters[key].downcase).pluck(:id) + if filters[key] && (obj_id = User.where(username_lower: filters[key].downcase).pluck(:id)) query = query.where("#{key}_id = ?", obj_id) end end @@ -133,7 +133,7 @@ class UserHistory < ActiveRecord::Base self.where(target_user_id: user.id, action: UserHistory.actions[action_type]) end - def self.exists_for_user?(user, action_type, opts=nil) + def self.exists_for_user?(user, action_type, opts = nil) opts = opts || {} result = self.where(target_user_id: user.id, action: UserHistory.actions[action_type]) result = result.where(topic_id: opts[:topic_id]) if opts[:topic_id] @@ -144,14 +144,13 @@ class UserHistory < ActiveRecord::Base [:action_id, :custom_type, :acting_user, :target_user, :subject] end - def self.staff_action_records(viewer, opts=nil) + def self.staff_action_records(viewer, opts = nil) opts ||= {} query = self.with_filters(opts.slice(*staff_filters)).only_staff_actions.limit(200).order('id DESC').includes(:acting_user, :target_user) query = query.where(admin_only: false) unless viewer && viewer.admin? query end - def set_admin_only self.admin_only = UserHistory.admin_only_action_ids.include?(self.action) self diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 551380335d..ee46d5b77e 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -8,7 +8,7 @@ class UserOption < ActiveRecord::Base def self.ensure_consistency! exec_sql("SELECT u.id FROM users u LEFT JOIN user_options o ON o.user_id = u.id - WHERE o.user_id IS NULL").values.each do |id,_| + WHERE o.user_id IS NULL").values.each do |id, _| UserOption.create(user_id: id.to_i) end end @@ -94,7 +94,6 @@ class UserOption < ActiveRecord::Base # top must be in the top_menu return unless SiteSetting.top_menu =~ /(^|\|)top(\||$)/i - # not enough topics return unless period = SiteSetting.min_redirected_to_top_period(1.days.ago) @@ -118,14 +117,18 @@ class UserOption < ActiveRecord::Base def treat_as_new_topic_start_date duration = new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes.to_i - times = [case duration + times = [ + case duration when User::NewTopicDuration::ALWAYS user.created_at when User::NewTopicDuration::LAST_VISIT user.previous_visit_at || user.user_stat.new_since else duration.minutes.ago - end, user.user_stat.new_since, Time.at(SiteSetting.min_new_topics_time).to_datetime] + end, + user.user_stat.new_since, + Time.at(SiteSetting.min_new_topics_time).to_datetime + ] times.max end diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index a6ceec4858..8ce2d186d4 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -19,7 +19,7 @@ class UserProfile < ActiveRecord::Base BAKED_VERSION = 1 - def bio_excerpt(length=350, opts={}) + def bio_excerpt(length = 350, opts = {}) excerpt = PrettyText.excerpt(bio_cooked, length, opts).sub(/
    $/, '') return excerpt if excerpt.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) PrettyText.strip_links(excerpt) @@ -63,11 +63,11 @@ class UserProfile < ActiveRecord::Base def self.rebake_old(limit) problems = [] UserProfile.where('bio_cooked_version IS NULL OR bio_cooked_version < ?', BAKED_VERSION) - .limit(limit).each do |p| + .limit(limit).each do |p| begin p.rebake! rescue => e - problems << {profile: p, ex: e} + problems << { profile: p, ex: e } end end problems diff --git a/app/models/user_profile_view.rb b/app/models/user_profile_view.rb index efe43cd42e..716b26c07c 100644 --- a/app/models/user_profile_view.rb +++ b/app/models/user_profile_view.rb @@ -3,7 +3,7 @@ class UserProfileView < ActiveRecord::Base belongs_to :user_profile - def self.add(user_profile_id, ip, user_id=nil, at=nil, skip_redis=false) + def self.add(user_profile_id, ip, user_id = nil, at = nil, skip_redis = false) at ||= Time.zone.now redis_key = "user-profile-view:#{user_profile_id}:#{at.to_date}" if user_id @@ -41,7 +41,7 @@ class UserProfileView < ActiveRecord::Base end end - def self.profile_views_by_day(start_date, end_date, group_id=nil) + def self.profile_views_by_day(start_date, end_date, group_id = nil) profile_views = self.where("viewed_at >= ? AND viewed_at < ?", start_date, end_date + 1.day) if group_id profile_views = profile_views.joins("INNER JOIN users ON users.id = user_profile_views.user_id") diff --git a/app/models/user_search.rb b/app/models/user_search.rb index 9b6c02c6ac..6fe2db9b22 100644 --- a/app/models/user_search.rb +++ b/app/models/user_search.rb @@ -3,7 +3,7 @@ require_dependency 'search' class UserSearch - def initialize(term, opts={}) + def initialize(term, opts = {}) @term = term @term_like = "#{term.downcase.gsub("_", "\\_")}%" @topic_id = opts[:topic_id] @@ -34,8 +34,8 @@ class UserSearch if topic.category && topic.category.read_restricted users = users.includes(:secure_categories) - .where("users.admin = TRUE OR categories.id = ?", topic.category.id) - .references(:categories) + .where("users.admin = TRUE OR categories.id = ?", topic.category.id) + .references(:categories) end end @@ -50,9 +50,9 @@ class UserSearch query = Search.ts_query(@term, "simple") users = users.includes(:user_search_data) - .references(:user_search_data) - .where("user_search_data.search_data @@ #{query}") - .order(User.sql_fragment("CASE WHEN username_lower LIKE ? THEN 0 ELSE 1 END ASC", @term_like)) + .references(:user_search_data) + .where("user_search_data.search_data @@ #{query}") + .order(User.sql_fragment("CASE WHEN username_lower LIKE ? THEN 0 ELSE 1 END ASC", @term_like)) else users = users.where("username_lower LIKE :term_like", term_like: @term_like) @@ -68,9 +68,9 @@ class UserSearch # 1. exact username matches if @term.present? scoped_users.where(username_lower: @term.downcase) - .limit(@limit) - .pluck(:id) - .each { |id| users << id } + .limit(@limit) + .pluck(:id) + .each { |id| users << id } end @@ -79,19 +79,19 @@ class UserSearch # 2. in topic if @topic_id filtered_by_term_users.where('users.id IN (SELECT p.user_id FROM posts p WHERE topic_id = ?)', @topic_id) - .order('last_seen_at DESC') - .limit(@limit - users.length) - .pluck(:id) - .each { |id| users << id } + .order('last_seen_at DESC') + .limit(@limit - users.length) + .pluck(:id) + .each { |id| users << id } end return users.to_a if users.length >= @limit # 3. global matches filtered_by_term_users.order('last_seen_at DESC') - .limit(@limit - users.length) - .pluck(:id) - .each { |id| users << id } + .limit(@limit - users.length) + .pluck(:id) + .each { |id| users << id } users.to_a end @@ -103,7 +103,7 @@ class UserSearch User.joins("JOIN (SELECT unnest uid, row_number() OVER () AS rn FROM unnest('{#{ids.join(",")}}'::int[]) ) x on uid = users.id") - .order("rn") + .order("rn") end end diff --git a/app/models/user_stat.rb b/app/models/user_stat.rb index 50761737e3..51ca626c3a 100644 --- a/app/models/user_stat.rb +++ b/app/models/user_stat.rb @@ -10,8 +10,8 @@ class UserStat < ActiveRecord::Base def self.reset_bounce_scores UserStat.where("reset_bounce_score_after < now()") - .where("bounce_score > 0") - .update_all(bounce_score: 0) + .where("bounce_score > 0") + .update_all(bounce_score: 0) end # Updates the denormalized view counts for all users @@ -55,14 +55,14 @@ class UserStat < ActiveRecord::Base def update_topic_reply_count self.topic_reply_count = Topic - .where(['id in ( + .where(['id in ( SELECT topic_id FROM posts p JOIN topics t2 ON t2.id = p.topic_id WHERE p.deleted_at IS NULL AND t2.user_id <> p.user_id AND p.user_id = ? )', self.user_id]) - .count + .count end MAX_TIME_READ_DIFF = 100 diff --git a/app/models/user_summary.rb b/app/models/user_summary.rb index abfc9b8f51..0cf714cfac 100644 --- a/app/models/user_summary.rb +++ b/app/models/user_summary.rb @@ -53,18 +53,18 @@ class UserSummary def most_liked_by_users likers = {} UserAction.joins(:target_topic, :target_post) - .merge(Topic.listable_topics.visible.secured(@guardian)) - .where(user: @user) - .where(action_type: UserAction::WAS_LIKED) - .group(:acting_user_id) - .order('COUNT(*) DESC') - .limit(MAX_SUMMARY_RESULTS) - .pluck('acting_user_id, COUNT(*)') - .each { |l| likers[l[0].to_s] = l[1] } + .merge(Topic.listable_topics.visible.secured(@guardian)) + .where(user: @user) + .where(action_type: UserAction::WAS_LIKED) + .group(:acting_user_id) + .order('COUNT(*) DESC') + .limit(MAX_SUMMARY_RESULTS) + .pluck('acting_user_id, COUNT(*)') + .each { |l| likers[l[0].to_s] = l[1] } User.where(id: likers.keys) - .pluck(:id, :username, :name, :uploaded_avatar_id) - .map do |u| + .pluck(:id, :username, :name, :uploaded_avatar_id) + .map do |u| UserWithCount.new( id: u[0], username: u[1], @@ -78,18 +78,18 @@ class UserSummary def most_liked_users liked_users = {} UserAction.joins(:target_topic, :target_post) - .merge(Topic.listable_topics.visible.secured(@guardian)) - .where(action_type: UserAction::WAS_LIKED) - .where(acting_user_id: @user.id) - .group(:user_id) - .order('COUNT(*) DESC') - .limit(MAX_SUMMARY_RESULTS) - .pluck('user_actions.user_id, COUNT(*)') - .each { |l| liked_users[l[0].to_s] = l[1] } + .merge(Topic.listable_topics.visible.secured(@guardian)) + .where(action_type: UserAction::WAS_LIKED) + .where(acting_user_id: @user.id) + .group(:user_id) + .order('COUNT(*) DESC') + .limit(MAX_SUMMARY_RESULTS) + .pluck('user_actions.user_id, COUNT(*)') + .each { |l| liked_users[l[0].to_s] = l[1] } User.where(id: liked_users.keys) - .pluck(:id, :username, :name, :uploaded_avatar_id) - .map do |u| + .pluck(:id, :username, :name, :uploaded_avatar_id) + .map do |u| UserWithCount.new( id: u[0], username: u[1], @@ -120,8 +120,8 @@ class UserSummary .each { |r| replied_users[r[0].to_s] = r[1] } User.where(id: replied_users.keys) - .pluck(:id, :username, :name, :uploaded_avatar_id) - .map do |u| + .pluck(:id, :username, :name, :uploaded_avatar_id) + .map do |u| UserWithCount.new( id: u[0], username: u[1], diff --git a/app/models/user_visit.rb b/app/models/user_visit.rb index 5ebe059167..63734ef207 100644 --- a/app/models/user_visit.rb +++ b/app/models/user_visit.rb @@ -1,6 +1,6 @@ class UserVisit < ActiveRecord::Base - def self.counts_by_day_query(start_date, end_date, group_id=nil) + def self.counts_by_day_query(start_date, end_date, group_id = nil) result = where('visited_at >= ? and visited_at <= ?', start_date.to_date, end_date.to_date) if group_id @@ -12,11 +12,11 @@ class UserVisit < ActiveRecord::Base end # A count of visits in a date range by day - def self.by_day(start_date, end_date, group_id=nil) + def self.by_day(start_date, end_date, group_id = nil) counts_by_day_query(start_date, end_date, group_id).count end - def self.mobile_by_day(start_date, end_date, group_id=nil) + def self.mobile_by_day(start_date, end_date, group_id = nil) counts_by_day_query(start_date, end_date, group_id).where(mobile: true).count end diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb new file mode 100644 index 0000000000..d9af0071de --- /dev/null +++ b/app/models/watched_word.rb @@ -0,0 +1,68 @@ +require_dependency 'enum' + +class WatchedWord < ActiveRecord::Base + + def self.actions + @actions ||= Enum.new( + block: 1, + censor: 2, + require_approval: 3, + flag: 4 + ) + end + + MAX_WORDS_PER_ACTION = 1000 + + before_validation do + self.word = self.class.normalize_word(self.word) + end + + validates :word, presence: true, uniqueness: true, length: { maximum: 50 } + validates :action, presence: true + validates_each :word do |record, attr, val| + if WatchedWord.where(action: record.action).count >= MAX_WORDS_PER_ACTION + record.errors.add(:word, :too_many) + end + end + + after_save :clear_cache + after_destroy :clear_cache + + scope :by_action, -> { order("action ASC, word ASC") } + + def self.normalize_word(w) + w.strip.downcase.squeeze('*') + end + + def self.create_or_update_word(params) + w = find_or_initialize_by(word: normalize_word(params[:word])) + w.action_key = params[:action_key] if params[:action_key] + w.action = params[:action] if params[:action] + w.save + w + end + + def action_key=(arg) + self.action = self.class.actions[arg.to_sym] + end + + def clear_cache + WordWatcher.clear_cache! + end + +end + +# == Schema Information +# +# Table name: watched_words +# +# id :integer not null, primary key +# word :string not null +# action :integer not null +# created_at :datetime +# updated_at :datetime +# +# Indexes +# +# index_watched_words_on_action_and_word (action,word) UNIQUE +# diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index 40fed09de6..e3335e0204 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -30,8 +30,8 @@ class WebHook < ActiveRecord::Base def self.find_by_type(type) WebHook.where(active: true) - .joins(:web_hook_event_types) - .where("web_hooks.wildcard_web_hook = ? OR web_hook_event_types.name = ?", true, type.to_s) + .joins(:web_hook_event_types) + .where("web_hooks.wildcard_web_hook = ? OR web_hook_event_types.name = ?", true, type.to_s) end def self.enqueue_hooks(type, opts = {}) @@ -40,11 +40,11 @@ class WebHook < ActiveRecord::Base end end - def self.enqueue_topic_hooks(event, topic, user=nil) + def self.enqueue_topic_hooks(event, topic, user = nil) WebHook.enqueue_hooks(:topic, topic_id: topic.id, category_id: topic&.category_id, event_name: event.to_s) end - def self.enqueue_post_hooks(event, post, user=nil) + def self.enqueue_post_hooks(event, post, user = nil) WebHook.enqueue_hooks(:post, post_id: post.id, category_id: post&.topic&.category_id, event_name: event.to_s) end end diff --git a/app/models/web_hook_event.rb b/app/models/web_hook_event.rb index f10b5cde31..0626c7fecc 100644 --- a/app/models/web_hook_event.rb +++ b/app/models/web_hook_event.rb @@ -6,12 +6,14 @@ class WebHookEvent < ActiveRecord::Base default_scope { order('created_at DESC') } def update_web_hook_delivery_status - web_hook.last_delivery_status = case status - when 200..299 - WebHook.last_delivery_statuses[:successful] - else - WebHook.last_delivery_statuses[:failed] - end + web_hook.last_delivery_status = + case status + when 200..299 + WebHook.last_delivery_statuses[:successful] + else + WebHook.last_delivery_statuses[:failed] + end + web_hook.save! end end diff --git a/app/serializers/admin_badge_serializer.rb b/app/serializers/admin_badge_serializer.rb index 535d27d1bd..4f57896d90 100644 --- a/app/serializers/admin_badge_serializer.rb +++ b/app/serializers/admin_badge_serializer.rb @@ -2,6 +2,6 @@ class AdminBadgeSerializer < BadgeSerializer attributes :query, :trigger, :target_posts, :auto_revoke, :show_posts def include_long_description? - true + true end end diff --git a/app/serializers/admin_user_action_serializer.rb b/app/serializers/admin_user_action_serializer.rb index 0cc58aa27a..dfe2cbc631 100644 --- a/app/serializers/admin_user_action_serializer.rb +++ b/app/serializers/admin_user_action_serializer.rb @@ -75,8 +75,8 @@ class AdminUserActionSerializer < ApplicationSerializer def action_type object.user_actions.select { |ua| ua.user_id = object.user_id } - .select { |ua| [UserAction::REPLY, UserAction::RESPONSE].include? ua.action_type } - .first.try(:action_type) + .select { |ua| [UserAction::REPLY, UserAction::RESPONSE].include? ua.action_type } + .first.try(:action_type) end private diff --git a/app/serializers/basic_group_serializer.rb b/app/serializers/basic_group_serializer.rb index e9cac82f26..bb4fd9ff86 100644 --- a/app/serializers/basic_group_serializer.rb +++ b/app/serializers/basic_group_serializer.rb @@ -18,7 +18,8 @@ class BasicGroupSerializer < ApplicationSerializer :flair_color, :bio_raw, :bio_cooked, - :public, + :public_admission, + :public_exit, :allow_membership_requests, :full_name, :default_notification_level diff --git a/app/serializers/basic_group_user_serializer.rb b/app/serializers/basic_group_user_serializer.rb index fa2015b8c4..3b40272470 100644 --- a/app/serializers/basic_group_user_serializer.rb +++ b/app/serializers/basic_group_user_serializer.rb @@ -1,3 +1,7 @@ class BasicGroupUserSerializer < ApplicationSerializer - attributes :group_id, :user_id, :notification_level + attributes :group_id, :user_id, :notification_level, :owner + + def include_owner? + object.user_id == scope&.user&.id + end end diff --git a/app/serializers/category_and_topic_lists_serializer.rb b/app/serializers/category_and_topic_lists_serializer.rb index 239bc07981..173b417d46 100644 --- a/app/serializers/category_and_topic_lists_serializer.rb +++ b/app/serializers/category_and_topic_lists_serializer.rb @@ -5,7 +5,7 @@ class CategoryAndTopicListsSerializer < ApplicationSerializer def users users = object.topic_list.topics.map do |t| - t.posters.map{|poster| poster.try(:user)} + t.posters.map { |poster| poster.try(:user) } end users.flatten! users.compact! diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index f482dd7e13..dae914fc6d 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -35,7 +35,7 @@ class CategorySerializer < BasicCategorySerializer end def available_groups - Group.order(:name).pluck(:name) - group_permissions.map{|g| g[:group_name]} + Group.order(:name).pluck(:name) - group_permissions.map { |g| g[:group_name] } end def can_delete @@ -44,7 +44,7 @@ class CategorySerializer < BasicCategorySerializer def include_is_special? [SiteSetting.meta_category_id, SiteSetting.staff_category_id, SiteSetting.uncategorized_category_id] - .include? object.id + .include? object.id end def is_special @@ -76,7 +76,7 @@ class CategorySerializer < BasicCategorySerializer end def notification_level - user = scope && scope.user + user = scope && scope.user object.notification_level || (user && CategoryUser.where(user: user, category: object).first.try(:notification_level)) end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 80de2e205c..a02c99d02b 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -137,7 +137,7 @@ class CurrentUserSerializer < BasicUserSerializer def muted_category_ids @muted_category_ids ||= CategoryUser.where(user_id: object.id, notification_level: TopicUser.notification_levels[:muted]) - .pluck(:category_id) + .pluck(:category_id) end def dismissed_banner_key diff --git a/app/serializers/detailed_user_badge_serializer.rb b/app/serializers/detailed_user_badge_serializer.rb index 1fa408844d..b32f72b6b8 100644 --- a/app/serializers/detailed_user_badge_serializer.rb +++ b/app/serializers/detailed_user_badge_serializer.rb @@ -10,7 +10,6 @@ class DetailedUserBadgeSerializer < BasicUserBadgeSerializer alias :include_topic_id? :include_post_number? alias :include_topic_title? :include_post_number? - def post_number object.post.post_number if object.post end diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb index dc9e0c7649..3bd6bf31d1 100644 --- a/app/serializers/group_show_serializer.rb +++ b/app/serializers/group_show_serializer.rb @@ -1,8 +1,8 @@ class GroupShowSerializer < BasicGroupSerializer - attributes :is_group_user, :is_group_owner + attributes :is_group_user, :is_group_owner, :mentionable def include_is_group_user? - scope.authenticated? + authenticated? end def is_group_user @@ -10,15 +10,27 @@ class GroupShowSerializer < BasicGroupSerializer end def include_is_group_owner? - scope.authenticated? + authenticated? end def is_group_owner scope.is_admin? || fetch_group_user&.owner end + def include_mentionable? + authenticated? + end + + def mentionable + Group.mentionable(scope.user).exists?(id: object.id) + end + private + def authenticated? + scope.authenticated? + end + def fetch_group_user @group_user ||= object.group_users.find_by(user: scope.user) end diff --git a/app/serializers/grouped_search_result_serializer.rb b/app/serializers/grouped_search_result_serializer.rb index d50cdd565d..bc4a5a6a7e 100644 --- a/app/serializers/grouped_search_result_serializer.rb +++ b/app/serializers/grouped_search_result_serializer.rb @@ -2,5 +2,14 @@ class GroupedSearchResultSerializer < ApplicationSerializer has_many :posts, serializer: SearchPostSerializer has_many :users, serializer: SearchResultUserSerializer has_many :categories, serializer: BasicCategorySerializer - attributes :more_posts, :more_users, :more_categories, :term + attributes :more_posts, :more_users, :more_categories, :term, :search_log_id, :more_full_page_results + + def search_log_id + object.search_log_id + end + + def include_search_log_id? + search_log_id.present? + end + end diff --git a/app/serializers/post_action_type_serializer.rb b/app/serializers/post_action_type_serializer.rb index 4e66641105..e0bf6f7c97 100644 --- a/app/serializers/post_action_type_serializer.rb +++ b/app/serializers/post_action_type_serializer.rb @@ -8,7 +8,7 @@ class PostActionTypeSerializer < ApplicationSerializer def is_custom_flag object.id == PostActionType.types[:notify_user] || - object.id == PostActionType.types[:notify_moderators] + object.id == PostActionType.types[:notify_moderators] end def name @@ -20,16 +20,16 @@ class PostActionTypeSerializer < ApplicationSerializer end def description - i18n('description', {tos_url: tos_path}) + i18n('description', tos_url: tos_path) end def short_description - i18n('short_description', {tos_url: tos_path}) + i18n('short_description', tos_url: tos_path) end protected - def i18n(field, vars=nil) + def i18n(field, vars = nil) key = "post_action_types.#{object.name_key}.#{field}" vars ? I18n.t(key, vars) : I18n.t(key) end diff --git a/app/serializers/post_item_excerpt.rb b/app/serializers/post_item_excerpt.rb index be74db9bdf..7f0cc6322b 100644 --- a/app/serializers/post_item_excerpt.rb +++ b/app/serializers/post_item_excerpt.rb @@ -22,4 +22,3 @@ module PostItemExcerpt end end - diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb index 5c1a05f603..11b13677f4 100644 --- a/app/serializers/post_revision_serializer.rb +++ b/app/serializers/post_revision_serializer.rb @@ -27,7 +27,6 @@ class PostRevisionSerializer < ApplicationSerializer :wiki, :can_edit - # Creates a field called field_name_changes with previous and # current members if a field has changed in this revision def self.add_compared_field(field) @@ -59,8 +58,8 @@ class PostRevisionSerializer < ApplicationSerializer def previous_revision @previous_revision ||= revisions.select { |r| r["revision"] >= first_revision } - .select { |r| r["revision"] < current_revision } - .last.try(:[], "revision") + .select { |r| r["revision"] < current_revision } + .last.try(:[], "revision") end def current_revision @@ -69,8 +68,8 @@ class PostRevisionSerializer < ApplicationSerializer def next_revision @next_revision ||= revisions.select { |r| r["revision"] <= last_revision } - .select { |r| r["revision"] > current_revision } - .first.try(:[], "revision") + .select { |r| r["revision"] > current_revision } + .first.try(:[], "revision") end def last_revision diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 5ea12e8cbe..ecd93e02ae 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -25,7 +25,8 @@ class SiteSerializer < ApplicationSerializer :top_tags, :wizard_required, :topic_featured_link_allowed_category_ids, - :user_themes + :user_themes, + :censored_words has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :trust_levels, embed: :objects @@ -36,16 +37,16 @@ class SiteSerializer < ApplicationSerializer cache_fragment("user_themes") do Theme.where('key = :default OR user_selectable', default: SiteSetting.default_theme_key) - .order(:name) - .pluck(:key, :name) - .map{|k,n| {theme_key: k, name: n, default: k == SiteSetting.default_theme_key}} - .as_json + .order(:name) + .pluck(:key, :name) + .map { |k, n| { theme_key: k, name: n, default: k == SiteSetting.default_theme_key } } + .as_json end end def groups cache_fragment("group_names") do - Group.order(:name).pluck(:id,:name).map { |id,name| { id: id, name: name } }.as_json + Group.order(:name).pluck(:id, :name).map { |id, name| { id: id, name: name } }.as_json end end @@ -142,4 +143,8 @@ class SiteSerializer < ApplicationSerializer def topic_featured_link_allowed_category_ids scope.topic_featured_link_allowed_category_ids end + + def censored_words + WordWatcher.words_for_action(:censor).join('|') + end end diff --git a/app/serializers/site_text_serializer.rb b/app/serializers/site_text_serializer.rb index c686a08008..f0e3c1e4c3 100644 --- a/app/serializers/site_text_serializer.rb +++ b/app/serializers/site_text_serializer.rb @@ -19,4 +19,3 @@ class SiteTextSerializer < ApplicationSerializer alias_method :can_revert?, :overridden? end - diff --git a/app/serializers/topic_flag_type_serializer.rb b/app/serializers/topic_flag_type_serializer.rb index a764d1a820..ee81618448 100644 --- a/app/serializers/topic_flag_type_serializer.rb +++ b/app/serializers/topic_flag_type_serializer.rb @@ -2,9 +2,9 @@ class TopicFlagTypeSerializer < PostActionTypeSerializer protected - def i18n(field, vars=nil) + def i18n(field, vars = nil) key = "topic_flag_types.#{object.name_key}.#{field}" - vars ? I18n.t(key,vars) : I18n.t(key) + vars ? I18n.t(key, vars) : I18n.t(key) end end diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index f9a548372e..49be96b8c7 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -90,13 +90,13 @@ class TopicViewSerializer < ApplicationSerializer if object.post_counts_by_user.present? result[:participants] = object.post_counts_by_user.map do |pc| - TopicPostCountSerializer.new({user: object.participants[pc[0]], post_count: pc[1]}, scope: scope, root: false) + TopicPostCountSerializer.new({ user: object.participants[pc[0]], post_count: pc[1] }, scope: scope, root: false) end end if object.suggested_topics.try(:topics).present? - result[:suggested_topics] = object.suggested_topics.topics.map do |topic| - SuggestedTopicSerializer.new(topic, scope: scope, root: false) + result[:suggested_topics] = object.suggested_topics.topics.map do |t| + SuggestedTopicSerializer.new(t, scope: scope, root: false) end end @@ -220,7 +220,7 @@ class TopicViewSerializer < ApplicationSerializer result << { id: id, count: 0, hidden: false, - can_act: scope.post_can_act?(post, sym)} + can_act: scope.post_can_act?(post, sym) } # TODO: other keys? :can_defer_flags, :acted, :can_undo end result @@ -277,7 +277,7 @@ class TopicViewSerializer < ApplicationSerializer end def unicode_title - gsub_emoji_to_unicode(object.topic.title) + Emoji.gsub_emoji_to_unicode(object.topic.title) end def include_pm_with_non_human_user? diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 81b2cb2fe6..c6abea6a5c 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -31,7 +31,6 @@ class UserActionSerializer < ApplicationSerializer :closed, :archived - def avatar_template User.avatar_template(object.username, object.uploaded_avatar_id) end diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index a6a7caeb23..b8253b40d7 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -22,7 +22,6 @@ class UserOptionSerializer < ApplicationSerializer :theme_key, :theme_key_seq - def auto_track_topics_after_msecs object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 0e75983daf..f41f762948 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -125,12 +125,12 @@ class UserSerializer < BasicUserSerializer def mailing_list_posts_per_day val = Post.estimate_posts_per_day - [val,SiteSetting.max_emails_per_day_per_user].min + [val, SiteSetting.max_emails_per_day_per_user].min end def groups object.groups.order(:id) - .visible_groups(scope.user) + .visible_groups(scope.user) end def group_users @@ -145,13 +145,12 @@ class UserSerializer < BasicUserSerializer !(SiteSetting.enable_sso && SiteSetting.sso_overrides_bio) end - def user_api_keys keys = object.user_api_keys.where(revoked_at: nil).map do |k| { id: k.id, application_name: k.application_name, - scopes: k.scopes.map{|s| I18n.t("user_api_key.scopes.#{s}")}, + scopes: k.scopes.map { |s| I18n.t("user_api_key.scopes.#{s}") }, created_at: k.created_at } end @@ -178,7 +177,7 @@ class UserSerializer < BasicUserSerializer def website_name uri = URI(website.to_s) rescue nil return if uri.nil? || uri.host.nil? - uri.host.sub(/^www\./,'') + uri.path + uri.host.sub(/^www\./, '') + uri.path end def include_website_name @@ -248,7 +247,7 @@ class UserSerializer < BasicUserSerializer end def bio_excerpt - object.user_profile.bio_excerpt(350 , { keep_newlines: true, keep_emoji_images: true }) + object.user_profile.bio_excerpt(350 , keep_newlines: true, keep_emoji_images: true) end def include_suspend_reason? diff --git a/app/serializers/watched_word_list_serializer.rb b/app/serializers/watched_word_list_serializer.rb new file mode 100644 index 0000000000..e38d56365c --- /dev/null +++ b/app/serializers/watched_word_list_serializer.rb @@ -0,0 +1,13 @@ +class WatchedWordListSerializer < ApplicationSerializer + attributes :actions, :words + + def actions + WatchedWord.actions.keys + end + + def words + object.map do |word| + WatchedWordSerializer.new(word, root: false) + end + end +end diff --git a/app/serializers/watched_word_serializer.rb b/app/serializers/watched_word_serializer.rb new file mode 100644 index 0000000000..ffa46da512 --- /dev/null +++ b/app/serializers/watched_word_serializer.rb @@ -0,0 +1,7 @@ +class WatchedWordSerializer < ApplicationSerializer + attributes :id, :word, :action + + def action + WatchedWord.actions[object.action] + end +end diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb index cfde16aa13..f76ab063e8 100644 --- a/app/services/badge_granter.rb +++ b/app/services/badge_granter.rb @@ -1,17 +1,17 @@ class BadgeGranter - def initialize(badge, user, opts={}) + def initialize(badge, user, opts = {}) @badge, @user, @opts = badge, user, opts @granted_by = opts[:granted_by] || Discourse.system_user @post_id = opts[:post_id] end - def self.grant(badge, user, opts={}) + def self.grant(badge, user, opts = {}) BadgeGranter.new(badge, user, opts).grant end def grant - return if @granted_by and !Guardian.new(@granted_by).can_grant_badges?(@user) + return if @granted_by && !Guardian.new(@granted_by).can_grant_badges?(@user) return unless @badge.enabled? find_by = { badge_id: @badge.id, user_id: @user.id } @@ -51,7 +51,7 @@ class BadgeGranter data: { badge_id: @badge.id, badge_name: @badge.display_name, badge_slug: @badge.slug, - username: @user.username}.to_json + username: @user.username }.to_json ) user_badge.update_attributes notification_id: notification.id end @@ -63,7 +63,7 @@ class BadgeGranter user_badge end - def self.revoke(user_badge, options={}) + def self.revoke(user_badge, options = {}) UserBadge.transaction do user_badge.destroy! if options[:revoked_by] @@ -178,7 +178,7 @@ class BadgeGranter # :trigger - the Badge::Trigger id # :explain - return the EXPLAIN query def self.preview(sql, opts = {}) - params = {user_ids: [], post_ids: [], backfill: true} + params = { user_ids: [], post_ids: [], backfill: true } BadgeGranter.contract_checks!(sql, opts) @@ -188,19 +188,19 @@ class BadgeGranter grants_sql = if opts[:target_posts] - "SELECT u.id, u.username, q.post_id, t.title, q.granted_at - FROM(#{sql}) q - JOIN users u on u.id = q.user_id - LEFT JOIN badge_posts p on p.id = q.post_id - LEFT JOIN topics t on t.id = p.topic_id - WHERE :backfill = :backfill - LIMIT 10" + "SELECT u.id, u.username, q.post_id, t.title, q.granted_at + FROM(#{sql}) q + JOIN users u on u.id = q.user_id + LEFT JOIN badge_posts p on p.id = q.post_id + LEFT JOIN topics t on t.id = p.topic_id + WHERE :backfill = :backfill + LIMIT 10" else - "SELECT u.id, u.username, q.granted_at - FROM(#{sql}) q - JOIN users u on u.id = q.user_id - WHERE :backfill = :backfill - LIMIT 10" + "SELECT u.id, u.username, q.granted_at + FROM(#{sql}) q + JOIN users u on u.id = q.user_id + WHERE :backfill = :backfill + LIMIT 10" end query_plan = nil @@ -218,13 +218,13 @@ class BadgeGranter end end - {grant_count: grant_count, sample: sample, query_plan: query_plan} + { grant_count: grant_count, sample: sample, query_plan: query_plan } rescue => e - {errors: e.message} + { errors: e.message } end MAX_ITEMS_FOR_DELTA ||= 200 - def self.backfill(badge, opts=nil) + def self.backfill(badge, opts = nil) return unless SiteSetting.enable_badges return unless badge.enabled return unless badge.query.present? @@ -304,14 +304,15 @@ class BadgeGranter user_ids: user_ids || [-2]).each do |row| # old bronze badges do not matter - next if badge.badge_type_id == BadgeType::Bronze and row.granted_at < 2.days.ago + next if badge.badge_type_id == (BadgeType::Bronze) && row.granted_at < (2.days.ago) # Try to use user locale in the badge notification if possible without too much resources - notification_locale = if SiteSetting.allow_user_locale && row.locale.present? - row.locale - else - SiteSetting.default_locale - end + notification_locale = + if SiteSetting.allow_user_locale && row.locale.present? + row.locale + else + SiteSetting.default_locale + end # Make this variable in this scope notification = nil @@ -327,7 +328,7 @@ class BadgeGranter badge_name: badge.display_name, badge_slug: badge.slug, username: row.username - }.to_json ) + }.to_json) end Badge.exec_sql("UPDATE user_badges SET notification_id = :notification_id WHERE id = :id", diff --git a/app/services/color_scheme_revisor.rb b/app/services/color_scheme_revisor.rb index ce66c37332..1fec2875f5 100644 --- a/app/services/color_scheme_revisor.rb +++ b/app/services/color_scheme_revisor.rb @@ -1,6 +1,6 @@ class ColorSchemeRevisor - def initialize(color_scheme, params={}) + def initialize(color_scheme, params = {}) @color_scheme = color_scheme @params = params end @@ -12,7 +12,7 @@ class ColorSchemeRevisor def revise ColorScheme.transaction do - @color_scheme.name = @params[:name] if @params.has_key?(:name) + @color_scheme.name = @params[:name] if @params.has_key?(:name) @color_scheme.base_scheme_id = @params[:base_scheme_id] if @params.has_key?(:base_scheme_id) has_colors = @params[:colors] diff --git a/app/services/group_action_logger.rb b/app/services/group_action_logger.rb index 9f1f429a8f..58674016c3 100644 --- a/app/services/group_action_logger.rb +++ b/app/services/group_action_logger.rb @@ -24,7 +24,7 @@ class GroupActionLogger end def log_add_user_to_group(target_user) - @group.public || can_edit? + (target_user == @acting_user && @group.public_admission) || can_edit? GroupHistory.create!(default_params.merge( action: GroupHistory.actions[:add_user_to_group], @@ -33,7 +33,7 @@ class GroupActionLogger end def log_remove_user_from_group(target_user) - @group.public || can_edit? + (target_user == @acting_user && @group.public_exit) || can_edit? GroupHistory.create!(default_params.merge( action: GroupHistory.actions[:remove_user_from_group], diff --git a/app/services/group_message.rb b/app/services/group_message.rb index 16e18ddbb8..4e5a55f99a 100644 --- a/app/services/group_message.rb +++ b/app/services/group_message.rb @@ -16,11 +16,11 @@ class GroupMessage include Rails.application.routes.url_helpers - def self.create(group_name, message_type, opts={}) + def self.create(group_name, message_type, opts = {}) GroupMessage.new(group_name, message_type, opts).create end - def initialize(group_name, message_type, opts={}) + def initialize(group_name, message_type, opts = {}) @group_name = group_name @message_type = message_type @opts = opts @@ -47,10 +47,8 @@ class GroupMessage @message_params ||= begin h = { base_url: Discourse.base_url }.merge(@opts[:message_params] || {}) if @opts[:user] - h.merge!({ - username: @opts[:user].username, - user_url: user_path(@opts[:user].username) - }) + h.merge!(username: @opts[:user].username, + user_url: user_path(@opts[:user].username)) end h end diff --git a/app/services/handle_chunk_upload.rb b/app/services/handle_chunk_upload.rb index c71a656dad..de5bf7837d 100644 --- a/app/services/handle_chunk_upload.rb +++ b/app/services/handle_chunk_upload.rb @@ -1,6 +1,6 @@ class HandleChunkUpload - def initialize(chunk, params={}) + def initialize(chunk, params = {}) @chunk = chunk @params = params end diff --git a/app/services/notification_emailer.rb b/app/services/notification_emailer.rb index 534d4037a1..bdbc5d7d35 100644 --- a/app/services/notification_emailer.rb +++ b/app/services/notification_emailer.rb @@ -66,12 +66,12 @@ class NotificationEmailer EMAILABLE_POST_TYPES ||= Set.new [Post.types[:regular], Post.types[:whisper]] - def enqueue(type, delay=default_delay) + def enqueue(type, delay = default_delay) return unless notification.user.user_option.email_direct? perform_enqueue(type, delay) end - def enqueue_private(type, delay=private_delay) + def enqueue_private(type, delay = private_delay) return unless notification.user.user_option.email_private_messages? perform_enqueue(type, delay) end diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 9672512e1b..130eec1285 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -48,11 +48,11 @@ class PostAlerter if post.last_editor_id != post.user_id # Mention comes from an edit by someone else, so notification should say who added the mention. editor = post.last_editor - mentioned_opts = {user_id: editor.id, original_username: editor.username, display_username: editor.username} + mentioned_opts = { user_id: editor.id, original_username: editor.username, display_username: editor.username } end expand_group_mentions(mentioned_groups, post) do |group, users| - notify_non_pm_users(users - notified, :group_mentioned, post, mentioned_opts.merge({group: group})) + notify_non_pm_users(users - notified, :group_mentioned, post, mentioned_opts.merge(group: group)) notified += users end @@ -84,14 +84,18 @@ class PostAlerter if new_record if post.topic.private_message? # users that aren't part of any mentioned groups - directly_targeted_users(post).each do |user| + users = directly_targeted_users(post) + DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) + users.each do |user| notification_level = TopicUser.get(post.topic, user).try(:notification_level) if notified.include?(user) || notification_level == TopicUser.notification_levels[:watching] create_notification(user, Notification.types[:private_message], post) end end # users that are part of all mentionned groups - indirectly_targeted_users(post).each do |user| + users = indirectly_targeted_users(post) + DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) + users.each do |user| # only create a notification when watching the group notification_level = TopicUser.get(post.topic, user).try(:notification_level) @@ -118,17 +122,17 @@ class PostAlerter if topic.present? cat_watchers = topic.category_users - .where(notification_level: CategoryUser.notification_levels[:watching_first_post]) - .pluck(:user_id) + .where(notification_level: CategoryUser.notification_levels[:watching_first_post]) + .pluck(:user_id) tag_watchers = topic.tag_users - .where(notification_level: TagUser.notification_levels[:watching_first_post]) - .pluck(:user_id) + .where(notification_level: TagUser.notification_levels[:watching_first_post]) + .pluck(:user_id) group_ids = topic.allowed_groups.pluck(:group_id) group_watchers = GroupUser.where(group_id: group_ids, notification_level: GroupUser.notification_levels[:watching_first_post]) - .pluck(:user_id) + .pluck(:user_id) watchers = [cat_watchers, tag_watchers, group_watchers].flatten @@ -145,7 +149,9 @@ class PostAlerter # Don't notify the OP user_ids -= [post.user_id] - User.where(id: user_ids).each do |u| + users = User.where(id: user_ids) + DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) + users.each do |u| create_notification(u, Notification.types[:watching_first_post], post) end end @@ -161,17 +167,17 @@ class PostAlerter def unread_posts(user, topic) Post.secured(Guardian.new(user)) - .where('post_number > COALESCE(( + .where('post_number > COALESCE(( SELECT last_read_post_number FROM topic_users tu WHERE tu.user_id = ? AND tu.topic_id = ? ),0)', user.id, topic.id) - .where('reply_to_user_id = ? OR exists( + .where('reply_to_user_id = ? OR exists( SELECT 1 from topic_users tu WHERE tu.user_id = ? AND tu.topic_id = ? AND notification_level = ? )', user.id, user.id, topic.id, TopicUser.notification_levels[:watching]) - .where(topic_id: topic.id) + .where(topic_id: topic.id) end def first_unread_post(user, topic) @@ -192,7 +198,7 @@ class PostAlerter user.reload end - NOTIFIABLE_TYPES = [:mentioned, :replied, :quoted, :posted, :linked, :private_message, :group_mentioned].map{ |t| + NOTIFIABLE_TYPES = [:mentioned, :replied, :quoted, :posted, :linked, :private_message, :group_mentioned].map { |t| Notification.types[t] } @@ -211,18 +217,18 @@ class PostAlerter end end - def notify_group_summary(user,post) + def notify_group_summary(user, post) @group_stats ||= {} stats = (@group_stats[post.topic_id] ||= group_stats(post.topic)) return unless stats group_id = post.topic - .topic_allowed_groups - .where(group_id: user.groups.pluck(:id)) - .pluck(:group_id).first + .topic_allowed_groups + .where(group_id: user.groups.pluck(:id)) + .pluck(:group_id).first - stat = stats.find{|s| s[:group_id] == group_id} + stat = stats.find { |s| s[:group_id] == group_id } return unless stat && stat[:inbox_count] > 0 notification_type = Notification.types[:group_message_summary] @@ -277,6 +283,8 @@ class PostAlerter def create_notification(user, type, post, opts = {}) opts = @default_opts.merge(opts) + DiscourseEvent.trigger(:before_create_notification, user, type, post, opts) + return if user.blank? return if user.id < 0 @@ -289,9 +297,9 @@ class PostAlerter # apply muting here return if notifier_id && MutedUser.where(user_id: user.id, muted_user_id: notifier_id) - .joins(:muted_user) - .where('NOT admin AND NOT moderator') - .exists? + .joins(:muted_user) + .where('NOT admin AND NOT moderator') + .exists? # skip if muted on the topic return if TopicUser.where( @@ -311,10 +319,10 @@ class PostAlerter # Don't notify the same user about the same notification on the same post existing_notification = user.notifications - .order("notifications.id DESC") - .find_by(topic_id: post.topic_id, - post_number: post.post_number, - notification_type: type) + .order("notifications.id DESC") + .find_by(topic_id: post.topic_id, + post_number: post.post_number, + notification_type: type) return if existing_notification && !should_notify_previous?(user, existing_notification, opts) @@ -368,13 +376,11 @@ class PostAlerter end end - notification_data.merge!({ - topic_title: topic_title, - original_post_id: original_post.id, - original_post_type: original_post.post_type, - original_username: original_username, - display_username: opts[:display_username] || post.user.username - }) + notification_data.merge!(topic_title: topic_title, + original_post_id: original_post.id, + original_post_type: original_post.post_type, + original_username: original_username, + display_username: opts[:display_username] || post.user.username) if group = opts[:group] notification_data[:group_id] = group.id @@ -393,15 +399,15 @@ class PostAlerter # we may have an invalid post somehow, dont blow up post_url = original_post.url rescue nil if post_url - payload = { - notification_type: type, - post_number: original_post.post_number, - topic_title: original_post.topic.title, - topic_id: original_post.topic.id, - excerpt: original_post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true), - username: original_username, - post_url: post_url - } + payload = { + notification_type: type, + post_number: original_post.post_number, + topic_title: original_post.topic.title, + topic_id: original_post.topic.id, + excerpt: original_post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true), + username: original_username, + post_url: post_url + } MessageBus.publish("/notification-alert/#{user.id}", payload, user_ids: [user.id]) push_notification(user, payload) @@ -414,9 +420,9 @@ class PostAlerter def push_notification(user, payload) if SiteSetting.allow_user_api_key_scopes.split("|").include?("push") && SiteSetting.allowed_user_api_push_urls.present? clients = user.user_api_keys - .where("('push' = ANY(scopes) OR 'notifications' = ANY(scopes)) AND push_url IS NOT NULL AND position(push_url in ?) > 0 AND revoked_at IS NULL", + .where("('push' = ANY(scopes) OR 'notifications' = ANY(scopes)) AND push_url IS NOT NULL AND position(push_url in ?) > 0 AND revoked_at IS NULL", SiteSetting.allowed_user_api_push_urls) - .pluck(:client_id, :push_url) + .pluck(:client_id, :push_url) if clients.length > 0 Jobs.enqueue(:push_notification, clients: clients, payload: payload, user_id: user.id) @@ -450,7 +456,6 @@ class PostAlerter [groups, users] end - # TODO: Move to post-analyzer? # Returns a list of users who were quoted in the post def extract_quoted_users(post) @@ -476,6 +481,7 @@ class PostAlerter users = [users] unless users.is_a?(Array) + DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) users.each do |u| create_notification(u, Notification.types[type], post, opts) end @@ -521,6 +527,7 @@ SQL exclude_user_ids = notified.map(&:id) notify = notify.where("id NOT IN (?)", exclude_user_ids) if exclude_user_ids.present? + DiscourseEvent.trigger(:before_create_notifications_for_users, notify, post) notify.each do |user| create_notification(user, Notification.types[:posted], post) end diff --git a/app/services/random_topic_selector.rb b/app/services/random_topic_selector.rb index 9446c4fcbb..f19b1a1718 100644 --- a/app/services/random_topic_selector.rb +++ b/app/services/random_topic_selector.rb @@ -3,7 +3,7 @@ class RandomTopicSelector BACKFILL_SIZE = 3000 BACKFILL_LOW_WATER_MARK = 500 - def self.backfill(category=nil) + def self.backfill(category = nil) exclude = category.try(:topic_id) # don't leak private categories into the "everything" group @@ -20,13 +20,12 @@ class RandomTopicSelector query = TopicQuery.new(user, options) - results = query.latest_results.order('RANDOM()') - .where(closed: false, archived: false) - .where("topics.created_at > ?", SiteSetting.suggested_topics_max_days_old.days.ago) - .limit(BACKFILL_SIZE) - .reorder('RANDOM()') - .pluck(:id) + .where(closed: false, archived: false) + .where("topics.created_at > ?", SiteSetting.suggested_topics_max_days_old.days.ago) + .limit(BACKFILL_SIZE) + .reorder('RANDOM()') + .pluck(:id) key = cache_key(category) results.each do |id| @@ -37,7 +36,7 @@ class RandomTopicSelector results end - def self.next(count, category=nil) + def self.next(count, category = nil) key = cache_key(category) results = [] @@ -45,12 +44,12 @@ class RandomTopicSelector return results if count < 1 results = $redis.multi do - $redis.lrange(key, 0, count-1) + $redis.lrange(key, 0, count - 1) $redis.ltrim(key, count, -1) end if !results.is_a?(Array) # Redis is in readonly mode - results = $redis.lrange(key, 0, count-1) + results = $redis.lrange(key, 0, count - 1) else results = results[0] end @@ -77,7 +76,7 @@ class RandomTopicSelector results end - def self.cache_key(category=nil) + def self.cache_key(category = nil) "random_topic_cache_#{category.try(:id)}" end diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index 72e526a301..e294f41131 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -15,7 +15,7 @@ class SearchIndexer end def self.update_index(table, id, raw_data) - raw_data = Search.prepare_data(raw_data) + raw_data = Search.prepare_data(raw_data, :index) table_name = "#{table}_search_data" foreign_key = "#{table}_id" @@ -31,7 +31,7 @@ class SearchIndexer end # for user login and name use "simple" lowercase stemmer - stemmer = table == "user" ? "simple" : Search.long_locale + stemmer = table == "user" ? "simple" : Search.ts_config # Would be nice to use AR here but not sure how to execut Postgres functions # when inserting data like this. @@ -110,7 +110,6 @@ class SearchIndexer end end - class HtmlScrubber < Nokogiri::XML::SAX::Document attr_reader :scrubbed @@ -130,7 +129,7 @@ class SearchIndexer me.scrubbed end - def start_element(name, attributes=[]) + def start_element(name, attributes = []) attributes = Hash[*attributes.flatten] if attributes["alt"] scrubbed << " " diff --git a/app/services/spam_rule/auto_block.rb b/app/services/spam_rule/auto_block.rb index a44ac9a115..03bb1bf67c 100644 --- a/app/services/spam_rule/auto_block.rb +++ b/app/services/spam_rule/auto_block.rb @@ -21,17 +21,17 @@ class SpamRule::AutoBlock return false if @user.staged? return false if @user.has_trust_level?(TrustLevel[1]) - if SiteSetting.num_spam_flags_to_block_new_user > 0 and - SiteSetting.num_users_to_block_new_user > 0 and - num_spam_flags_against_user >= SiteSetting.num_spam_flags_to_block_new_user and - num_users_who_flagged_spam_against_user >= SiteSetting.num_users_to_block_new_user + if SiteSetting.num_spam_flags_to_block_new_user > (0) && + SiteSetting.num_users_to_block_new_user > (0) && + num_spam_flags_against_user >= (SiteSetting.num_spam_flags_to_block_new_user) && + num_users_who_flagged_spam_against_user >= (SiteSetting.num_users_to_block_new_user) return true end - if SiteSetting.num_tl3_flags_to_block_new_user > 0 and - SiteSetting.num_tl3_users_to_block_new_user > 0 and - num_tl3_flags_against_user >= SiteSetting.num_tl3_flags_to_block_new_user and - num_tl3_users_who_flagged >= SiteSetting.num_tl3_users_to_block_new_user + if SiteSetting.num_tl3_flags_to_block_new_user > (0) && + SiteSetting.num_tl3_users_to_block_new_user > (0) && + num_tl3_flags_against_user >= (SiteSetting.num_tl3_flags_to_block_new_user) && + num_tl3_users_who_flagged >= (SiteSetting.num_tl3_users_to_block_new_user) return true end @@ -66,14 +66,14 @@ class SpamRule::AutoBlock def flagged_post_ids Post.where(user_id: @user.id) - .where('spam_count > ? OR off_topic_count > ? OR inappropriate_count > ?', 0, 0, 0) - .pluck(:id) + .where('spam_count > ? OR off_topic_count > ? OR inappropriate_count > ?', 0, 0, 0) + .pluck(:id) end def block_user Post.transaction do if UserBlocker.block(@user, Discourse.system_user, message: :too_many_spam_flags) && SiteSetting.notify_mods_when_user_blocked - GroupMessage.create(Group[:moderators].name, :user_automatically_blocked, {user: @user, limit_once_per: false}) + GroupMessage.create(Group[:moderators].name, :user_automatically_blocked, user: @user, limit_once_per: false) end end end diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 1cd09176af..b58ff9093e 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -10,16 +10,14 @@ class StaffActionLogger raise Discourse::InvalidParameters.new(:admin) unless @admin && @admin.is_a?(User) end - def log_user_deletion(deleted_user, opts={}) + def log_user_deletion(deleted_user, opts = {}) raise Discourse::InvalidParameters.new(:deleted_user) unless deleted_user && deleted_user.is_a?(User) - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:delete_user], - ip_address: deleted_user.ip_address.to_s, - details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join("\n") - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:delete_user], + ip_address: deleted_user.ip_address.to_s, + details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join("\n"))) end - def log_custom(custom_type, details=nil) + def log_custom(custom_type, details = nil) raise Discourse::InvalidParameters.new(:custom_type) unless custom_type details ||= {} @@ -28,7 +26,7 @@ class StaffActionLogger StaffActionLogger.base_attrs.each do |attr| attrs[attr] = details.delete(attr) if details.has_key?(attr) end - attrs[:details] = details.map {|r| "#{r[0]}: #{r[1]}"}.join("\n") + attrs[:details] = details.map { |r| "#{r[0]}: #{r[1]}" }.join("\n") attrs[:acting_user_id] = @admin.id attrs[:action] = UserHistory.actions[:custom_staff] attrs[:custom_type] = custom_type @@ -36,7 +34,7 @@ class StaffActionLogger UserHistory.create(attrs) end - def log_post_deletion(deleted_post, opts={}) + def log_post_deletion(deleted_post, opts = {}) raise Discourse::InvalidParameters.new(:deleted_post) unless deleted_post && deleted_post.is_a?(Post) topic = deleted_post.topic || Topic.with_deleted.find_by(id: deleted_post.topic_id) @@ -54,14 +52,12 @@ class StaffActionLogger "raw: #{deleted_post.raw}" ] - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:delete_post], - post_id: deleted_post.id, - details: details.join("\n") - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:delete_post], + post_id: deleted_post.id, + details: details.join("\n"))) end - def log_topic_deletion(deleted_topic, opts={}) + def log_topic_deletion(deleted_topic, opts = {}) raise Discourse::InvalidParameters.new(:deleted_topic) unless deleted_topic && deleted_topic.is_a?(Topic) user = deleted_topic.user ? "#{deleted_topic.user.username} (#{deleted_topic.user.name})" : "(deleted user)" @@ -77,48 +73,40 @@ class StaffActionLogger details << "raw: #{first_post.raw}" end - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:delete_topic], - topic_id: deleted_topic.id, - details: details.join("\n") - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:delete_topic], + topic_id: deleted_topic.id, + details: details.join("\n"))) end - def log_trust_level_change(user, old_trust_level, new_trust_level, opts={}) + def log_trust_level_change(user, old_trust_level, new_trust_level, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user && user.is_a?(User) raise Discourse::InvalidParameters.new(:old_trust_level) unless TrustLevel.valid? old_trust_level raise Discourse::InvalidParameters.new(:new_trust_level) unless TrustLevel.valid? new_trust_level - UserHistory.create!( params(opts).merge({ - action: UserHistory.actions[:change_trust_level], - target_user_id: user.id, - details: "old trust level: #{old_trust_level}\nnew trust level: #{new_trust_level}" - })) + UserHistory.create!(params(opts).merge(action: UserHistory.actions[:change_trust_level], + target_user_id: user.id, + details: "old trust level: #{old_trust_level}\nnew trust level: #{new_trust_level}")) end - def log_lock_trust_level(user, opts={}) + def log_lock_trust_level(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user && user.is_a?(User) - UserHistory.create!( params(opts).merge({ - action: UserHistory.actions[user.trust_level_locked ? :lock_trust_level : :unlock_trust_level], - target_user_id: user.id - })) + UserHistory.create!(params(opts).merge(action: UserHistory.actions[user.trust_level_locked ? :lock_trust_level : :unlock_trust_level], + target_user_id: user.id)) end - def log_site_setting_change(setting_name, previous_value, new_value, opts={}) + def log_site_setting_change(setting_name, previous_value, new_value, opts = {}) raise Discourse::InvalidParameters.new(:setting_name) unless setting_name.present? && SiteSetting.respond_to?(setting_name) - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:change_site_setting], - subject: setting_name, - previous_value: previous_value, - new_value: new_value - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:change_site_setting], + subject: setting_name, + previous_value: previous_value, + new_value: new_value)) end def theme_json(theme) - ThemeSerializer.new(theme, root:false).to_json + ThemeSerializer.new(theme, root: false).to_json end - def strip_duplicates(old,cur) - return [old,cur] unless old && cur + def strip_duplicates(old, cur) + return [old, cur] unless old && cur old = JSON.parse(old) cur = JSON.parse(cur) @@ -135,144 +123,116 @@ class StaffActionLogger [old.to_json, cur.to_json] end - def log_theme_change(old_json, new_theme, opts={}) + def log_theme_change(old_json, new_theme, opts = {}) raise Discourse::InvalidParameters.new(:new_theme) unless new_theme new_json = theme_json(new_theme) - old_json,new_json = strip_duplicates(old_json,new_json) + old_json, new_json = strip_duplicates(old_json, new_json) - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:change_theme], - subject: new_theme.name, - previous_value: old_json, - new_value: new_json - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:change_theme], + subject: new_theme.name, + previous_value: old_json, + new_value: new_json)) end - def log_theme_destroy(theme, opts={}) + def log_theme_destroy(theme, opts = {}) raise Discourse::InvalidParameters.new(:theme) unless theme - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:delete_theme], - subject: theme.name, - previous_value: theme_json(theme) - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:delete_theme], + subject: theme.name, + previous_value: theme_json(theme))) end - def log_site_text_change(subject, new_text=nil, old_text=nil, opts={}) + def log_site_text_change(subject, new_text = nil, old_text = nil, opts = {}) raise Discourse::InvalidParameters.new(:subject) unless subject.present? - UserHistory.create!( params(opts).merge({ - action: UserHistory.actions[:change_site_text], - subject: subject, - previous_value: old_text, - new_value: new_text - })) + UserHistory.create!(params(opts).merge(action: UserHistory.actions[:change_site_text], + subject: subject, + previous_value: old_text, + new_value: new_text)) end - def log_username_change(user, old_username, new_username, opts={}) + def log_username_change(user, old_username, new_username, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:change_username], - target_user_id: user.id, - previous_value: old_username, - new_value: new_username - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:change_username], + target_user_id: user.id, + previous_value: old_username, + new_value: new_username)) end - def log_name_change(user_id, old_name, new_name, opts={}) + def log_name_change(user_id, old_name, new_name, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user_id - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:change_name], - target_user_id: user_id, - previous_value: old_name, - new_value: new_name - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:change_name], + target_user_id: user_id, + previous_value: old_name, + new_value: new_name)) end - def log_user_suspend(user, reason, opts={}) + def log_user_suspend(user, reason, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:suspend_user], - target_user_id: user.id, - details: reason - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:suspend_user], + target_user_id: user.id, + details: reason)) end - def log_user_unsuspend(user, opts={}) + def log_user_unsuspend(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:unsuspend_user], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:unsuspend_user], + target_user_id: user.id)) end - def log_badge_grant(user_badge, opts={}) + def log_badge_grant(user_badge, opts = {}) raise Discourse::InvalidParameters.new(:user_badge) unless user_badge - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:grant_badge], - target_user_id: user_badge.user_id, - details: user_badge.badge.name - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:grant_badge], + target_user_id: user_badge.user_id, + details: user_badge.badge.name)) end - def log_badge_revoke(user_badge, opts={}) + def log_badge_revoke(user_badge, opts = {}) raise Discourse::InvalidParameters.new(:user_badge) unless user_badge - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:revoke_badge], - target_user_id: user_badge.user_id, - details: user_badge.badge.name - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:revoke_badge], + target_user_id: user_badge.user_id, + details: user_badge.badge.name)) end - def log_check_email(user, opts={}) + def log_check_email(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:check_email], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:check_email], + target_user_id: user.id)) end - def log_show_emails(users, opts={}) + def log_show_emails(users, opts = {}) return if users.blank? - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:check_email], - details: users.map { |u| "[#{u.id}] #{u.username}"}.join("\n") - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:check_email], + details: users.map { |u| "[#{u.id}] #{u.username}" }.join("\n"))) end - def log_impersonate(user, opts={}) + def log_impersonate(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:impersonate], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:impersonate], + target_user_id: user.id)) end - def log_roll_up(subnets, opts={}) - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:roll_up], - details: subnets.join(", ") - })) + def log_roll_up(subnets, opts = {}) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:roll_up], + details: subnets.join(", "))) end - def log_category_settings_change(category, category_params, old_permissions=nil) + def log_category_settings_change(category, category_params, old_permissions = nil) validate_category(category) changed_attributes = category.previous_changes.slice(*category_params.keys) if !old_permissions.empty? && (old_permissions != category_params[:permissions]) - changed_attributes.merge!({ permissions: [old_permissions.to_json, category_params[:permissions].to_json] }) + changed_attributes.merge!(permissions: [old_permissions.to_json, category_params[:permissions].to_json]) end changed_attributes.each do |key, value| - UserHistory.create(params.merge({ - action: UserHistory.actions[:change_category_settings], - category_id: category.id, - context: category.url, - subject: key, - previous_value: value[0], - new_value: value[1] - })) + UserHistory.create(params.merge(action: UserHistory.actions[:change_category_settings], + category_id: category.id, + context: category.url, + subject: key, + previous_value: value[0], + new_value: value[1])) end end @@ -289,12 +249,10 @@ class StaffActionLogger details << "parent_category: #{parent_category.name}" end - UserHistory.create(params.merge({ - action: UserHistory.actions[:delete_category], - category_id: category.id, - details: details.join("\n"), - context: category.url - })) + UserHistory.create(params.merge(action: UserHistory.actions[:delete_category], + category_id: category.id, + details: details.join("\n"), + context: category.url)) end def log_category_creation(category) @@ -305,132 +263,102 @@ class StaffActionLogger "name: #{category.name}" ] - UserHistory.create(params.merge({ - action: UserHistory.actions[:create_category], - details: details.join("\n"), - category_id: category.id, - context: category.url - })) + UserHistory.create(params.merge(action: UserHistory.actions[:create_category], + details: details.join("\n"), + category_id: category.id, + context: category.url)) end - def log_block_user(user, opts={}) + def log_block_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:block_user], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:block_user], + target_user_id: user.id)) end - def log_unblock_user(user, opts={}) + def log_unblock_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:unblock_user], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:unblock_user], + target_user_id: user.id)) end - def log_grant_admin(user, opts={}) + def log_grant_admin(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:grant_admin], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:grant_admin], + target_user_id: user.id)) end - def log_revoke_admin(user, opts={}) + def log_revoke_admin(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:revoke_admin], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:revoke_admin], + target_user_id: user.id)) end - def log_grant_moderation(user, opts={}) + def log_grant_moderation(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:grant_moderation], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:grant_moderation], + target_user_id: user.id)) end - def log_revoke_moderation(user, opts={}) + def log_revoke_moderation(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:revoke_moderation], - target_user_id: user.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:revoke_moderation], + target_user_id: user.id)) end - def log_backup_create(opts={}) - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:backup_create], - ip_address: @admin.ip_address.to_s - })) + def log_backup_create(opts = {}) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:backup_create], + ip_address: @admin.ip_address.to_s)) end - def log_backup_download(backup, opts={}) + def log_backup_download(backup, opts = {}) raise Discourse::InvalidParameters.new(:backup) unless backup - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:backup_download], - ip_address: @admin.ip_address.to_s, - details: backup.filename - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:backup_download], + ip_address: @admin.ip_address.to_s, + details: backup.filename)) end - def log_backup_destroy(backup, opts={}) + def log_backup_destroy(backup, opts = {}) raise Discourse::InvalidParameters.new(:backup) unless backup - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:backup_destroy], - ip_address: @admin.ip_address.to_s, - details: backup.filename - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:backup_destroy], + ip_address: @admin.ip_address.to_s, + details: backup.filename)) end - def log_revoke_email(user, reason, opts={}) - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:revoke_email], - target_user_id: user.id, - details: reason - })) + def log_revoke_email(user, reason, opts = {}) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:revoke_email], + target_user_id: user.id, + details: reason)) end - def log_user_deactivate(user, reason, opts={}) + def log_user_deactivate(user, reason, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:deactivate_user], - target_user_id: user.id, - details: reason - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:deactivate_user], + target_user_id: user.id, + details: reason)) end - def log_user_activate(user, reason, opts={}) + def log_user_activate(user, reason, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:activate_user], - target_user_id: user.id, - details: reason - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:activate_user], + target_user_id: user.id, + details: reason)) end - def log_wizard_step(step, opts={}) + def log_wizard_step(step, opts = {}) raise Discourse::InvalidParameters.new(:step) unless step - UserHistory.create(params(opts).merge({ - action: UserHistory.actions[:wizard_step], - context: step.id - })) + UserHistory.create(params(opts).merge(action: UserHistory.actions[:wizard_step], + context: step.id)) end def log_change_readonly_mode(state) - UserHistory.create(params.merge({ - action: UserHistory.actions[:change_readonly_mode], - previous_value: !state, - new_value: state - })) + UserHistory.create(params.merge(action: UserHistory.actions[:change_readonly_mode], + previous_value: !state, + new_value: state)) end private - def params(opts=nil) + def params(opts = nil) opts ||= {} { acting_user_id: @admin.id, context: opts[:context] } end diff --git a/app/services/topic_status_updater.rb b/app/services/topic_status_updater.rb index a2fc8f4dd6..80b5781ea7 100644 --- a/app/services/topic_status_updater.rb +++ b/app/services/topic_status_updater.rb @@ -1,5 +1,5 @@ TopicStatusUpdater = Struct.new(:topic, :user) do - def update!(status, enabled, opts={}) + def update!(status, enabled, opts = {}) status = Status.new(status, enabled) @topic_status_update = topic.public_topic_timer @@ -19,7 +19,7 @@ TopicStatusUpdater = Struct.new(:topic, :user) do private - def change(status, opts={}) + def change(status, opts = {}) result = true if status.pinned? || status.pinned_globally? @@ -30,7 +30,7 @@ TopicStatusUpdater = Struct.new(:topic, :user) do result = false if rc == 0 else rc = Topic.where(:id => topic.id, status.name => !status.enabled) - .update_all(status.name => status.enabled?) + .update_all(status.name => status.enabled?) topic.send("#{status.name}=", status.enabled?) result = false if rc == 0 @@ -55,7 +55,7 @@ TopicStatusUpdater = Struct.new(:topic, :user) do result end - def create_moderator_post_for(status, message=nil) + def create_moderator_post_for(status, message = nil) topic.add_moderator_post(user, message || message_for(status), options_for(status)) topic.reload end diff --git a/app/services/tracked_topics_updater.rb b/app/services/tracked_topics_updater.rb index 2222fc4c63..0ffb32e7e7 100644 --- a/app/services/tracked_topics_updater.rb +++ b/app/services/tracked_topics_updater.rb @@ -8,11 +8,10 @@ class TrackedTopicsUpdater def call topic_users = TopicUser.where(notifications_reason_id: nil, user_id: @id) if @threshold < 0 - topic_users.update_all({notification_level: TopicUser.notification_levels[:regular]}) + topic_users.update_all(notification_level: TopicUser.notification_levels[:regular]) else topic_users.update_all(["notification_level = CASE WHEN total_msecs_viewed < ? THEN ? ELSE ? END", @threshold, TopicUser.notification_levels[:regular], TopicUser.notification_levels[:tracking]]) end end end - diff --git a/app/services/user_action_creator.rb b/app/services/user_action_creator.rb index bbf21f9fb9..68a5d0847d 100644 --- a/app/services/user_action_creator.rb +++ b/app/services/user_action_creator.rb @@ -7,19 +7,19 @@ class UserActionCreator @disabled = false end - def self.log_notification(post, user, notification_type, acting_user_id=nil) + def self.log_notification(post, user, notification_type, acting_user_id = nil) return if @disabled action = case notification_type - when Notification.types[:quoted] - UserAction::QUOTE - when Notification.types[:replied] - UserAction::RESPONSE - when Notification.types[:mentioned] - UserAction::MENTION - when Notification.types[:edited] - UserAction::EDIT + when Notification.types[:quoted] + UserAction::QUOTE + when Notification.types[:replied] + UserAction::RESPONSE + when Notification.types[:mentioned] + UserAction::MENTION + when Notification.types[:edited] + UserAction::EDIT end # skip any invalid items, eg failed to save post and so on diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index ac692d28d7..dbd58624bf 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -1,10 +1,10 @@ class UserAnonymizer - def initialize(user, actor=nil) + def initialize(user, actor = nil) @user = user @actor = actor end - def self.make_anonymous(user, actor=nil) + def self.make_anonymous(user, actor = nil) self.new(user, actor).make_anonymous end @@ -48,11 +48,11 @@ class UserAnonymizer @user.user_open_ids.find_each { |x| x.destroy } @user.api_key.try(:destroy) - UserHistory.create( action: UserHistory.actions[:anonymize_user], - target_user_id: @user.id, - acting_user_id: @actor ? @actor.id : @user.id, - email: prev_email, - details: "username: #{prev_username}" ) + UserHistory.create(action: UserHistory.actions[:anonymize_user], + target_user_id: @user.id, + acting_user_id: @actor ? @actor.id : @user.id, + email: prev_email, + details: "username: #{prev_username}") end @user end diff --git a/app/services/user_blocker.rb b/app/services/user_blocker.rb index fe0091425e..8a35c5f239 100644 --- a/app/services/user_blocker.rb +++ b/app/services/user_blocker.rb @@ -1,14 +1,14 @@ class UserBlocker - def initialize(user, by_user=nil, opts={}) + def initialize(user, by_user = nil, opts = {}) @user, @by_user, @opts = user, by_user, opts end - def self.block(user, by_user=nil, opts={}) + def self.block(user, by_user = nil, opts = {}) UserBlocker.new(user, by_user, opts).block end - def self.unblock(user, by_user=nil, opts={}) + def self.unblock(user, by_user = nil, opts = {}) UserBlocker.new(user, by_user, opts).unblock end @@ -20,7 +20,7 @@ class UserBlocker message_type = @opts[:message] || :blocked_by_staff post = SystemMessage.create(@user, message_type) if post && @by_user - StaffActionLogger.new(@by_user).log_block_user(@user, {context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}"}) + StaffActionLogger.new(@by_user).log_block_user(@user, context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}") end end else diff --git a/app/services/user_destroyer.rb b/app/services/user_destroyer.rb index 9af5731fc9..e95d4ca60d 100644 --- a/app/services/user_destroyer.rb +++ b/app/services/user_destroyer.rb @@ -7,14 +7,14 @@ class UserDestroyer def initialize(actor) @actor = actor - raise Discourse::InvalidParameters.new('acting user is nil') unless @actor and @actor.is_a?(User) + raise Discourse::InvalidParameters.new('acting user is nil') unless @actor && @actor.is_a?(User) @guardian = Guardian.new(actor) end # Returns false if the user failed to be deleted. # Returns a frozen instance of the User if the delete succeeded. - def destroy(user, opts={}) - raise Discourse::InvalidParameters.new('user is nil') unless user and user.is_a?(User) + def destroy(user, opts = {}) + raise Discourse::InvalidParameters.new('user is nil') unless user && user.is_a?(User) raise PostsExistError if !opts[:delete_posts] && user.posts.count != 0 @guardian.ensure_can_delete_user!(user) @@ -31,18 +31,16 @@ class UserDestroyer # block all external urls if opts[:block_urls] post.topic_links.each do |link| - unless link.internal || - (Oneboxer.engine(link.url) != Onebox::Engine::WhitelistedGenericOnebox) - - ScreenedUrl.watch(link.url, link.domain, ip_address: user.ip_address)&.record_match! - end + next if link.internal + next if Oneboxer.engine(link.url) != Onebox::Engine::WhitelistedGenericOnebox + ScreenedUrl.watch(link.url, link.domain, ip_address: user.ip_address)&.record_match! end end PostDestroyer.new(@actor.staff? ? @actor : Discourse.system_user, post).destroy if post.topic && post.is_first_post? - Topic.unscoped.where(id: post.topic.id).update_all(user_id: nil) + Topic.unscoped.where(id: post.topic_id).update_all(user_id: nil) end end end @@ -51,39 +49,39 @@ class UserDestroyer post_action.remove_act!(Discourse.system_user) end + # keep track of emails used + user_emails = user.user_emails.pluck(:email) + user.destroy.tap do |u| if u - if opts[:block_email] - b = ScreenedEmail.block(u.email, ip_address: u.ip_address) - b.record_match! if b - end - - if opts[:block_ip] && u.ip_address - b = ScreenedIpAddress.watch(u.ip_address) - b.record_match! if b - if u.registration_ip_address && u.ip_address != u.registration_ip_address - b = ScreenedIpAddress.watch(u.registration_ip_address) - b.record_match! if b + user_emails.each do |email| + ScreenedEmail.block(email, ip_address: u.ip_address)&.record_match! end end - Post.with_deleted.where(user_id: user.id).update_all("user_id = NULL") + if opts[:block_ip] && u.ip_address + ScreenedIpAddress.watch(u.ip_address)&.record_match! + if u.registration_ip_address && u.ip_address != u.registration_ip_address + ScreenedIpAddress.watch(u.registration_ip_address)&.record_match! + end + end + + Post.unscoped.where(user_id: u.id).update_all(user_id: nil) # If this user created categories, fix those up: - categories = Category.where(user_id: user.id) - categories.each do |c| - c.user_id = Discourse.system_user.id + Category.where(user_id: u.id).each do |c| + c.user_id = Discourse::SYSTEM_USER_ID c.save! - if topic = Topic.with_deleted.find_by(id: c.topic_id) - topic.try(:recover!) - topic.user_id = Discourse.system_user.id + if topic = Topic.unscoped.find_by(id: c.topic_id) + topic.recover! + topic.user_id = Discourse::SYSTEM_USER_ID topic.save! end end StaffActionLogger.new(@actor == user ? Discourse.system_user : @actor).log_user_deletion(user, opts.slice(:context)) - MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id] + MessageBus.publish "/file-change", ["refresh"], user_ids: [u.id] end end end diff --git a/app/services/username_changer.rb b/app/services/username_changer.rb index 990534e282..a81f089415 100644 --- a/app/services/username_changer.rb +++ b/app/services/username_changer.rb @@ -1,12 +1,12 @@ class UsernameChanger - def initialize(user, new_username, actor=nil) + def initialize(user, new_username, actor = nil) @user = user @new_username = new_username @actor = actor end - def self.change(user, new_username, actor=nil) + def self.change(user, new_username, actor = nil) self.new(user, new_username, actor).change end diff --git a/app/services/username_checker_service.rb b/app/services/username_checker_service.rb index 91bfe42de9..0f8d7a6251 100644 --- a/app/services/username_checker_service.rb +++ b/app/services/username_checker_service.rb @@ -4,7 +4,7 @@ class UsernameCheckerService if username && username.length > 0 validator = UsernameValidator.new(username) if !validator.valid_format? - {errors: validator.errors} + { errors: validator.errors } else check_username_availability(username, email) end @@ -23,7 +23,6 @@ class UsernameCheckerService Rails.configuration.respond_to?(:developer_emails) && Rails.configuration.developer_emails.include?(value) end - def self.is_developer?(email) UsernameCheckerService.new.is_developer?(email) end diff --git a/app/services/word_watcher.rb b/app/services/word_watcher.rb new file mode 100644 index 0000000000..11d61f0f52 --- /dev/null +++ b/app/services/word_watcher.rb @@ -0,0 +1,51 @@ +class WordWatcher + + def initialize(raw) + @raw = raw + end + + def self.words_for_action(action) + WatchedWord.where(action: WatchedWord.actions[action.to_sym]).limit(1000).pluck(:word) + end + + def self.words_for_action_exists?(action) + WatchedWord.where(action: WatchedWord.actions[action.to_sym]).exists? + end + + def self.word_matcher_regexp(action) + s = Discourse.cache.fetch(word_matcher_regexp_key(action), expires_in: 1.day) do + words = words_for_action(action) + words.empty? ? nil : '\b(' + words.map { |w| Regexp.escape(w).gsub("\\*", '\S*') }.join('|'.freeze) + ')\b' + end + + s.present? ? Regexp.new(s, Regexp::IGNORECASE) : nil + end + + def self.word_matcher_regexp_key(action) + "watched-words-regexp:#{action}" + end + + def self.clear_cache! + WatchedWord.actions.sum do |a, i| + Discourse.cache.delete word_matcher_regexp_key(a) + end + end + + def requires_approval? + word_matches_for_action?(:require_approval) + end + + def should_flag? + word_matches_for_action?(:flag) + end + + def should_block? + word_matches_for_action?(:block) + end + + def word_matches_for_action?(action) + r = self.class.word_matcher_regexp(action) + r ? r.match(@raw) : false + end + +end diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb index f30438dd2f..2733cc8167 100644 --- a/app/views/categories/index.html.erb +++ b/app/views/categories/index.html.erb @@ -6,7 +6,7 @@

    <%= c.name %>

    - <%= c.description %> + <%= c.description&.html_safe %>
    <% end %> diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 1d30e8e296..f9b0146793 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -42,9 +42,7 @@ Discourse.Environment = '<%= Rails.env %>'; Discourse.SiteSettings = ps.get('siteSettings'); Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>'; - <%- if SiteSetting.enable_experimental_markdown_it %> Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>'; - <%- end %> I18n.defaultLocale = '<%= SiteSetting.default_locale %>'; Discourse.start(); Discourse.set('assetVersion','<%= Discourse.assets_digest %>'); diff --git a/app/views/common/_google_universal_analytics.html.erb b/app/views/common/_google_universal_analytics.html.erb index 9067c94cb3..7c03711df4 100644 --- a/app/views/common/_google_universal_analytics.html.erb +++ b/app/views/common/_google_universal_analytics.html.erb @@ -5,4 +5,10 @@ })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); ga('create', '<%= SiteSetting.ga_universal_tracking_code %>', <%= ga_universal_json %>); + + <% if SiteSetting.ga_universal_auto_link_domains.present? %> + ga('require', 'linker'); + ga('linker:autoLink', <%= raw SiteSetting.ga_universal_auto_link_domains.split('|').to_json %>); + <% end %> + diff --git a/bin/puma b/bin/puma index 26523fced3..3249ddee00 100755 --- a/bin/puma +++ b/bin/puma @@ -12,7 +12,7 @@ if ARGV[0] == '--kill-existing' pids = `ps aux | grep puma | grep discourse | grep -v grep | awk '{print $2;}'`.strip .split("\n") .map(&:to_i) - .reject{|pid| pid == Process.pid} + .reject { |pid| pid == Process.pid } if pids.length > 0 STDERR.puts "Terminating old version of puma at pid #{pids[0]}" diff --git a/config.ru b/config.ru index c74954f4f4..9a0cd31d67 100644 --- a/config.ru +++ b/config.ru @@ -3,4 +3,3 @@ require ::File.expand_path('../config/environment', __FILE__) map ActionController::Base.config.try(:relative_url_root) || "/" do run Discourse::Application end - diff --git a/config/application.rb b/config/application.rb index f49a372783..a0a9920d48 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,7 +22,6 @@ if defined?(Bundler) Bundler.require(*Rails.groups(assets: %w(development test profile))) end - module Discourse class Application < Rails::Application @@ -169,7 +168,7 @@ module Discourse # we configure rack cache on demand in an initializer # our setup does not use rack cache and instead defers to nginx - config.action_dispatch.rack_cache = nil + config.action_dispatch.rack_cache = nil # ember stuff only used for asset precompliation, production variant plays up config.ember.variant = :development @@ -177,7 +176,7 @@ module Discourse config.ember.handlebars_location = "#{Rails.root}/vendor/assets/javascripts/handlebars.js" require 'auth' - Discourse.activate_plugins! unless Rails.env.test? and ENV['LOAD_PLUGINS'] != "1" + Discourse.activate_plugins! unless Rails.env.test? && ENV['LOAD_PLUGINS'] != ("1") if GlobalSetting.relative_url_root.present? config.relative_url_root = GlobalSetting.relative_url_root @@ -207,7 +206,18 @@ module Discourse # So open id logs somewhere sane OpenID::Util.logger = Rails.logger if plugins = Discourse.plugins - plugins.each{|plugin| plugin.notify_after_initialize} + plugins.each { |plugin| plugin.notify_after_initialize } + end + + # This nasty hack is required for not precompiling QUnit assets + # in test mode. see: https://github.com/rails/sprockets-rails/issues/299#issuecomment-167701012 + ActiveSupport.on_load(:action_view) do + default_checker = ActionView::Base.precompiled_asset_checker + + ActionView::Base.precompiled_asset_checker = -> logical_path do + default_checker[logical_path] || + %w{qunit.js qunit.css test_helper.css test_helper.js wizard/test/test_helper.js}.include?(logical_path) + end end end diff --git a/config/boot.rb b/config/boot.rb index 7180eb1fbb..929f1a62fe 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -10,7 +10,6 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) - if ENV['RAILS_ENV'] != 'production' require 'bootsnap' diff --git a/config/cloud/cloud66/files/production.rb b/config/cloud/cloud66/files/production.rb index db662574be..42d9caa9fc 100644 --- a/config/cloud/cloud66/files/production.rb +++ b/config/cloud/cloud66/files/production.rb @@ -27,13 +27,13 @@ Discourse::Application.configure do config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - :address => ENV['SMTP_ADDRESS'], - :port => ENV['SMTP_PORT'], - :domain => ENV['SMTP_DOMAIN'], - :user_name => ENV['SMTP_USERNAME'], - :password => ENV['SMTP_PASSWORD'], - :authentication => 'plain', - :enable_starttls_auto => true } + address: ENV['SMTP_ADDRESS'], + port: ENV['SMTP_PORT'], + domain: ENV['SMTP_DOMAIN'], + user_name: ENV['SMTP_USERNAME'], + password: ENV['SMTP_PASSWORD'], + authentication: 'plain', + enable_starttls_auto: true } #config.action_mailer.delivery_method = :sendmail #config.action_mailer.sendmail_settings = {arguments: '-i'} diff --git a/config/environments/development.rb b/config/environments/development.rb index 560d560130..d9c10db2e8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -48,8 +48,6 @@ Discourse::Application.configure do config.enable_anon_caching = false require 'rbtrace' - - if emails = GlobalSetting.developer_emails config.developer_emails = emails.split(",").map(&:downcase).map(&:strip) end diff --git a/config/environments/production.rb b/config/environments/production.rb index b4331f15ac..6489a1777e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -35,10 +35,10 @@ Discourse::Application.configure do settings[:openssl_verify_mode] = GlobalSetting.smtp_openssl_verify_mode if GlobalSetting.smtp_openssl_verify_mode - config.action_mailer.smtp_settings = settings.reject{|_, y| y.nil?} + config.action_mailer.smtp_settings = settings.reject { |_, y| y.nil? } else config.action_mailer.delivery_method = :sendmail - config.action_mailer.sendmail_settings = {arguments: '-i'} + config.action_mailer.sendmail_settings = { arguments: '-i' } end # Send deprecation notices to registered listeners diff --git a/config/environments/test.rb b/config/environments/test.rb index 16cab97ce1..5cd785ce22 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -18,7 +18,7 @@ Discourse::Application.configure do config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false + config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the @@ -41,4 +41,9 @@ Discourse::Application.configure do config.assets.digest = false config.eager_load = false + + unless ENV['RAILS_ENABLE_TEST_LOG'] + config.logger = Logger.new(nil) + config.log_level = :fatal + end end diff --git a/config/initializers/001-redis.rb b/config/initializers/001-redis.rb index 73340cc49d..af3e829842 100644 --- a/config/initializers/001-redis.rb +++ b/config/initializers/001-redis.rb @@ -4,12 +4,11 @@ if Rails.env.development? && ENV['DISCOURSE_FLUSH_REDIS'] end if defined?(PhusionPassenger) - PhusionPassenger.on_event(:starting_worker_process) do |forked| - if forked - Discourse.after_fork - else - # We're in conservative spawning mode. We don't need to do anything. - end + PhusionPassenger.on_event(:starting_worker_process) do |forked| + if forked + Discourse.after_fork + else + # We're in conservative spawning mode. We don't need to do anything. end + end end - diff --git a/config/initializers/004-message_bus.rb b/config/initializers/004-message_bus.rb index 5faf89f9e8..fabada48a0 100644 --- a/config/initializers/004-message_bus.rb +++ b/config/initializers/004-message_bus.rb @@ -1,4 +1,4 @@ -MessageBus.site_id_lookup do |env=nil| +MessageBus.site_id_lookup do |env = nil| if env setup_message_bus_env(env) env["__mb"][:site_id] diff --git a/config/initializers/006-ensure_login_hint.rb b/config/initializers/006-ensure_login_hint.rb index e177601737..1cc7b51429 100644 --- a/config/initializers/006-ensure_login_hint.rb +++ b/config/initializers/006-ensure_login_hint.rb @@ -18,5 +18,5 @@ if User.limit(20).count < 20 && User.where(admin: true).human_users.count == 0 SiteSetting.has_login_hint = true end -# we may be booting with no User table eg: first migration, just skip + # we may be booting with no User table eg: first migration, just skip end rescue nil diff --git a/config/initializers/006-mini_profiler.rb b/config/initializers/006-mini_profiler.rb index e39b1a33de..a04c888fce 100644 --- a/config/initializers/006-mini_profiler.rb +++ b/config/initializers/006-mini_profiler.rb @@ -6,7 +6,7 @@ if Rails.configuration.respond_to?(:load_mini_profiler) && Rails.configuration.l begin require 'memory_profiler' rescue => e - STDERR.put "#{e} failed to require mini profiler" + STDERR.put "#{e} failed to require mini profiler" end # initialization is skipped so trigger it @@ -48,7 +48,7 @@ if defined?(Rack::MiniProfiler) path = env['PATH_INFO'] (env['HTTP_USER_AGENT'] !~ /iPad|iPhone|Android/) && - !skip.any?{|re| re =~ path} + !skip.any? { |re| re =~ path } end # without a user provider our results will use the ip address for namespacing @@ -68,7 +68,6 @@ if defined?(Rack::MiniProfiler) Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/silence_logger/ Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/quiet_logger/ - # Rack::MiniProfiler.counter_method(ActiveRecord::QueryMethods, 'build_arel') # Rack::MiniProfiler.counter_method(Array, 'uniq') # require "#{Rails.root}/vendor/backports/notification" @@ -97,9 +96,8 @@ if defined?(Rack::MiniProfiler) # Rack::MiniProfiler.profile_method ActionView::PathResolver, 'find_templates' end - if ENV["PRINT_EXCEPTIONS"] - trace = TracePoint.new(:raise) do |tp| + trace = TracePoint.new(:raise) do |tp| puts tp.raised_exception puts tp.raised_exception.backtrace.join("\n") puts diff --git a/config/initializers/008-rack-cors.rb b/config/initializers/008-rack-cors.rb index 4864b961f6..2ba7b01fb3 100644 --- a/config/initializers/008-rack-cors.rb +++ b/config/initializers/008-rack-cors.rb @@ -8,7 +8,7 @@ if GlobalSetting.enable_cors end def call(env) - if env['REQUEST_METHOD'] == 'OPTIONS' and env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] + if env['REQUEST_METHOD'] == ('OPTIONS') && env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] return [200, apply_headers(env), []] end @@ -16,7 +16,7 @@ if GlobalSetting.enable_cors [status, apply_headers(env, headers), body] end - def apply_headers(env, headers=nil) + def apply_headers(env, headers = nil) headers ||= {} origin = nil diff --git a/config/initializers/050-force_https.rb b/config/initializers/050-force_https.rb index 7ac71e1135..0185fbd742 100644 --- a/config/initializers/050-force_https.rb +++ b/config/initializers/050-force_https.rb @@ -1,7 +1,7 @@ # tiny middleware to force https if needed class Discourse::ForceHttpsMiddleware - def initialize(app, config={}) + def initialize(app, config = {}) @app = app end @@ -11,4 +11,3 @@ class Discourse::ForceHttpsMiddleware end end - diff --git a/config/initializers/099-anon-cache.rb b/config/initializers/099-anon-cache.rb index d2c90beee8..e0a90c71f1 100644 --- a/config/initializers/099-anon-cache.rb +++ b/config/initializers/099-anon-cache.rb @@ -1,13 +1,13 @@ require_dependency "middleware/anonymous_cache" -enabled = if Rails.configuration.respond_to?(:enable_anon_caching) - Rails.configuration.enable_anon_caching - else - Rails.env.production? - end +enabled = + if Rails.configuration.respond_to?(:enable_anon_caching) + Rails.configuration.enable_anon_caching + else + Rails.env.production? + end if !ENV['DISCOURSE_DISABLE_ANON_CACHE'] && enabled # in an ideal world this is position 0, but mobile detection uses ... session and request and params Rails.configuration.middleware.insert_after ActionDispatch::ParamsParser, Middleware::AnonymousCache end - diff --git a/config/initializers/100-logster.rb b/config/initializers/100-logster.rb index d913051336..c7277684c6 100644 --- a/config/initializers/100-logster.rb +++ b/config/initializers/100-logster.rb @@ -44,12 +44,12 @@ end # middleware that logs errors sits before multisite # we need to establish a connection so redis connection is good # and db connection is good -Logster.config.current_context = lambda{|env,&blk| +Logster.config.current_context = lambda { |env, &blk| begin if Rails.configuration.multisite request = Rack::Request.new(env) ActiveRecord::Base.connection_handler.clear_active_connections! - RailsMultisite::ConnectionManagement.establish_connection(:host => request['__ws'] || request.host) + RailsMultisite::ConnectionManagement.establish_connection(host: request['__ws'] || request.host) end blk.call ensure @@ -73,7 +73,7 @@ RailsMultisite::ConnectionManagement.each_connection do if (error_rate_per_minute || 0) > 0 store.register_rate_limit_per_minute(severities, error_rate_per_minute) do |rate| - MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: 'minute', publish_at: Time.current.to_i }) + MessageBus.publish("/logs_error_rate_exceeded", rate: rate, duration: 'minute', publish_at: Time.current.to_i) end end @@ -81,7 +81,7 @@ RailsMultisite::ConnectionManagement.each_connection do if (error_rate_per_hour || 0) > 0 store.register_rate_limit_per_hour(severities, error_rate_per_hour) do |rate| - MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: 'hour', publish_at: Time.current.to_i }) + MessageBus.publish("/logs_error_rate_exceeded", rate: rate, duration: 'hour', publish_at: Time.current.to_i) end end end diff --git a/config/initializers/100-quiet_logger.rb b/config/initializers/100-quiet_logger.rb index 55c0dfcb4c..908b0c2cfd 100644 --- a/config/initializers/100-quiet_logger.rb +++ b/config/initializers/100-quiet_logger.rb @@ -6,7 +6,7 @@ Rails::Rack::Logger.class_eval do def call_with_quiet_assets(env) override = false - if (env['PATH_INFO'].index("/assets/") == 0) or + if (env['PATH_INFO'].index("/assets/") == 0) || (env['PATH_INFO'].index("mini-profiler-resources") == 0) if ::Logster::Logger === Rails.logger override = true diff --git a/config/initializers/100-sidekiq.rb b/config/initializers/100-sidekiq.rb index a155c113cb..efde736012 100644 --- a/config/initializers/100-sidekiq.rb +++ b/config/initializers/100-sidekiq.rb @@ -35,7 +35,7 @@ if Sidekiq.server? manager.tick rescue => e # the show must go on - Discourse.handle_job_exception(e, {message: "While ticking scheduling manager"}) + Discourse.handle_job_exception(e, message: "While ticking scheduling manager") end sleep 1 end diff --git a/config/initializers/100-verify_config.rb b/config/initializers/100-verify_config.rb index c0c0a8965a..2d91431db6 100644 --- a/config/initializers/100-verify_config.rb +++ b/config/initializers/100-verify_config.rb @@ -1,6 +1,6 @@ # Check that the app is configured correctly. Raise some helpful errors if something is wrong. -if defined?(Rails::Server) and Rails.env.production? # Only run these checks when starting up a production server +if defined?(Rails::Server) && Rails.env.production? # Only run these checks when starting up a production server if ['localhost', 'production.localhost'].include?(Discourse.current_hostname) puts <مرشد الإعداد ✨" emails_are_disabled: "لقد عطّل أحد المدراء الرّسائل الصادرة للجميع. لن تُرسل إشعارات عبر البريد الإلكتروني أيا كان نوعها." bootstrap_mode_enabled: "الموقع اﻷن مفعل بالطريقة التمهيدية لكى تتمكن من اطلاق موقعك الجديد بسهولة. سيسجل كل الأعضاء الجدد بمستوى ثقة 1 وسيكون اختيار ارسال الملخص اليومى عن طريق البريد الالكترونى مفعل. سيتم الغاء تفعيل الطريقة التمهيدية تلقائيا عندما يتخطى عدد اﻷعضاء %{min_users} عضو." bootstrap_mode_disabled: "سيتم الغاء تفعيل الطريقة التمهيدية خلال ال 24 ساعة القادمة" themes: - default_description: "افترضى" + default_description: "افتراضى" s3: regions: - us_east_1: "شرق الولايات المتحدة (فرجينيا الشمالية)" - us_west_1: "غرب الولايات المتحدة (كاليفورنيا الشمالية)" - us_west_2: "غرب الولايات المتحدة (أوريغون)" - us_gov_west_1: "إستضافة أمازون الحسابية الحكومية (الولايات المتحدة الأمريكية)" - eu_west_1: "الاتحاد الأوروبي (أيرلندا)" - eu_west_2: "الاتحاد الأوروبي (أيرلندا)" - eu_central_1: "الاتحاد الأوروبي (فرانكفورت)" - ap_southeast_1: "آسيا والمحيط الهادئ (سنغافورة)" - ap_southeast_2: "آسيا والمحيط الهادئ (سيدني)" - ap_south_1: "آسيا والمحيط الهادئ (مومباي)" ap_northeast_1: "آسيا والمحيط الهادئ (طوكيو)" ap_northeast_2: "آسيا والمحيط الهادئ ( سيول)" - sa_east_1: "أمريكا الجنوبية (ساو باولو)" + ap_south_1: "آسيا والمحيط الهادئ (مومباي)" + ap_southeast_1: "آسيا والمحيط الهادئ (سنغافورة)" + ap_southeast_2: "آسيا والمحيط الهادئ (سيدني)" cn_north_1: "الصين (بكين)" + eu_central_1: "الاتحاد الأوروبي (فرانكفورت)" + eu_west_1: "الاتحاد الأوروبي (أيرلندا)" + eu_west_2: "الاتحاد الأوروبي (أيرلندا)" + sa_east_1: "أمريكا الجنوبية (ساو باولو)" + us_east_1: "شرق الولايات المتحدة (فرجينيا الشمالية)" + us_east_2: "غرب الولايات المتحدة (اوهايو)" + us_gov_west_1: "إستضافة أمازون الحسابية الحكومية (الولايات المتحدة الأمريكية)" + us_west_1: "غرب الولايات المتحدة (كاليفورنيا الشمالية)" + us_west_2: "غرب الولايات المتحدة (أوريغون)" edit: 'عدّل عنوان هذا الموضوع وفئته' not_implemented: "لم تُنجز هذه الميزة بعد، نأسف." no_value: "لا" @@ -237,7 +242,7 @@ ar: generic_error: "نأسف، حدث عطل ما." generic_error_with_reason: "حدث عطل ما: %{error}" sign_up: "سجّل حسابا" - log_in: "تسجل الدخول" + log_in: "لِج" age: "العمر" joined: "انضم في" admin_title: "المدير" @@ -308,7 +313,7 @@ ar: bookmark: "انقر لتعليم أوّل مشاركة في هذا الموضوع" unbookmark: "انقر لإزالة كلّ العلامات في هذا الموضوع" bookmarks: - not_logged_in: "نأسف، عليك الدخول لتعليم المشاركات" + not_logged_in: "نأسف، عليك الولوج لتعليم المشاركات" created: "لقد علّمت هذه المشاركة" not_bookmarked: "لقد قرأتَ هذه المشاركة، انقر لتعليمها" last_read: "هذا آخر مشاركة قرأتها، انقر لتعليمها" @@ -452,6 +457,7 @@ ar: add_members: "إضافة أعضاء" delete_member_confirm: "أأزيل ’%{username}‘ من مجموعة ’%{group}‘؟" name_placeholder: "اسم المجموعة، بدون مسافات. يتبع قواعد اسم المستخدم" + public: "السماح للأعضاء بالانظمام\\مغادرة المجموعة بحرية (يتطلب مجموعة مرئية للجميع)" empty: posts: "لا مشاركات من أعضاء هذه المجموعة." members: "لا أعضاء في هذه المجموعة." @@ -466,6 +472,7 @@ ar: automatic_group: مجموعة تلقائية closed_group: مجموعة مغلقة is_group_user: "أنت عضو في هذه المجموعة" + allow_membership_requests: "السماح للأعضاء بأرسالة طلب انضمام لمدراء المجموعة" membership: "العضوية" name: "الاسم" user_count: "عدد اﻷعضاء" @@ -488,6 +495,13 @@ ar: posts: "المشاركات" mentions: "الإشارات" messages: "الرسائل" + notification_level: "المستوى الأفتراضي لأشعارات الرسائل في المجموعة" + visibility_levels: + title: "من يستطيع رؤية هذه المجموعة ؟" + public: "الكل" + members: "مدراء المجموعة, الاعضاء و المشرفون" + staff: "مدراء المجموعة و الطاقم" + owners: "مدراء المجموعة و المشرفون" alias_levels: title: "مَن يمكنه مراسلة هذه المجموعة و@الإشارة إليها؟" nobody: "لا أحد" @@ -577,7 +591,7 @@ ar: location_not_found: (غيرمعرف) organisation: المنظمات phone: هاتف - other_accounts: "حساب آخر بنفس العنوان" + other_accounts: "الحسابات الأخرى بعنوان IP هذا:" delete_other_accounts: "حذف %{count}" username: "إسم المستخدم" trust_level: "TL" @@ -623,6 +637,7 @@ ar: first_notification: "إشعارك الأول! قم بالضغط عليه للبدء." disable_jump_reply: "لا تنتقل إلى مشاركتي بعدما أردّ" dynamic_favicon: "أظهر عدد المواضيع الجديدة/المحدّثة في أيقونة المتصفّح" + theme_default_on_all_devices: "اجعل القالب افتراضياً على جميع اجهزتي" external_links_in_new_tab: "فتح الروابط الخارجية في ألسنة جديدة" enable_quoting: "فعل خاصية إقتباس النصوص المظللة" change: "تغيير" @@ -694,6 +709,15 @@ ar: move_to_archive: "الارشيف" failed_to_move: "فشل في نقل الرسائل المحددة (ربما يكون اتصالك ضعيفاً)" select_all: "إختيار الكل" + preferences_nav: + account: "الحساب" + profile: "اللاحة" + emails: "البُرد الإلكترونية" + notifications: "التنبيهات" + categories: "الفئات" + tags: "الوسوم" + interface: "الواجهة" + apps: "التطبيقات" change_password: success: "(تم ارسال الرسالة)" in_progress: "(يتم ارسال رسالة)" @@ -944,7 +968,7 @@ ar: most_liked_users: "أكثر من أعجبه" most_replied_to_users: "أكثر من رَدّ عليه" no_likes: "لا إعجابات بعد." - associated_accounts: "جلسات الدخول" + associated_accounts: "جلسات الولوج" ip_address: title: "عنوان IP الأخير" registration_ip_address: @@ -987,11 +1011,9 @@ ar: refresh: "تحديث" read_only_mode: enabled: "هذا الموقع في وضع القراءة فقط. نأمل أن تتابع تصفّحه، ولكنّ الرّدّ، والإعجاب وغيرها من إجراءات معطّلة حاليًّا." - login_disabled: "الدخول معطّل في حال كان الموقع في وضع القراءة فقط." + login_disabled: "الولوج معطّل في حال كان الموقع في وضع القراءة فقط." logout_disabled: "الخروج معطّل في حال كان الموقع في وضع القراءة فقط." too_few_topics_and_posts_notice: " يوجد حاليا%{currentTopics} / %{requiredTopics} المواضيع و %{currentPosts} / %{requiredPosts} المشاركات. " - too_few_topics_notice: "قم بإنشاء على الاقل 5 مواضيع عامة و %{posts} عامة للبدء بالمناقشة , المستخدمين الجدد لايمكن أن يحصلوا على مستوى الثقة حتى يكون هناك محتوى لهم , هذه الرسالة تظهر فقط للمدير" - too_few_posts_notice: "دعونا الحصول على هذه المناقشة التي بدأت! يوجد حاليا %{currentPosts} / %{requiredPosts} مشاركات. الزوار الجديدة بحاجة إلى بعض الأحاديث قراءة والرد عليها." logs_error_rate_notice: reached: "%{relativeAge}%{rate} وصل الحد الأعلى لإعدادت الموقع %{siteSettingRate}." exceeded: "%{relativeAge}%{rate} يتجاوز الحد الأعلى لإعدادت الموقع %{siteSettingRate}." @@ -1021,7 +1043,7 @@ ar: sign_up: "إشترك" hide_session: "ذكرني غدا" hide_forever: "لا شكرا" - hidden_for_session: "لا بأس، سأسلك غدًا. يمكنك دومًا استخدام ’تسجل الدخول‘ لإنشاء حساب." + hidden_for_session: "لا بأس، سأسلك غدًا. يمكنك دومًا استخدام ’لِج‘ لإنشاء حساب." intro: "يا مرحبًا! :heart_eyes: يبدو أنّك مستمتع بقراءة هذا النّقاش، ولكنّك لم تسجّل حسابًا بعد." value_prop: "عندما تنتهي من إنشاء الحساب، سترجع إلى حيث توقّفت، فنحن نتذكّر دومًا المكان الذي توقّفت عنده. يمكنك أيضًا استقبال الإشعارات عبر الموقع والبريد الإلكترونيّ متى ما نُشرت مشاركات جديدة. ولا تنسَ إبداء إعجابك بتلك المشاركات التي تحبّها. :heartbeat:" summary: @@ -1063,13 +1085,13 @@ ar: complete_username_not_found: "لا حساب يطابق اسم المستخدم %{username}" complete_email_not_found: "لا حساب يطابق %{email}" login: - title: "سجل دخولك" + title: "لِج" username: "المستخدم" password: "كلمة السّرّ" email_placeholder: "البريد الإلكتروني أو اسم المستخدم" caps_lock_warning: "مفتاح Caps Lock مفعّل" error: "خطأ مجهول" - rate_limit: "رجاءً انتظر قبل أن تسجل الدخول مرّة أخرى." + rate_limit: "رجاءً انتظر قبل أن تلج مرّة أخرى." blank_username_or_password: "رجاءً أدخل اسم المستخدم (أو البريد الإلكترونيّ) وكلمة السّرّ." reset_password: 'صفّر كلمة السّرّ' logging_in: "يلج..." @@ -1077,15 +1099,16 @@ ar: authenticating: "يستوثق..." awaiting_approval: "لم يوافق أيّ من أعضاء الطّاقم على حسابك بعد. سيُرسل إليك بريد حالما يتمّ ذلك." requires_invite: "المَعذرة، الوصول لهذا الموقع خاص بالمدعويين فقط." - not_activated: "لا يمكنك الدخول بعد. لقد أرسلنا سابقًا بريدًا إلى {{sentTo}}. رجاء اتّبع الإرشادات فيه لتفعيل حسابك." - not_allowed_from_ip_address: "لا يمكنك الدخول من عنوان IP هذا." - admin_not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول كمدير من عنوان IP هذا." + not_activated: "لا يمكنك الولوج بعد. لقد أرسلنا سابقًا بريدًا إلى {{sentTo}}. رجاء اتّبع الإرشادات فيه لتفعيل حسابك." + not_allowed_from_ip_address: "لا يمكنك الولوج من عنوان IP هذا." + admin_not_allowed_from_ip_address: "لا يمكنك الولوج كمدير من عنوان IP هذا." resend_activation_email: "انقر هنا لإرسال بريد التّفعيل مرّة أخرى." + submit_new_email: "حدث عنوان البريد الإلكتروني" sent_activation_email_again: "لقد أرسلنا بريد تفعيل آخر إلى {{currentEmail}}. قد يستغرق وصوله بضعة دقائق. تحقّق من فحص مجلّد السّخام." - to_continue: "رجاءً سجل دخولك" - preferences: "عليك تسجيل الدخول لتغيير تفضيلاتك الشّخصيّة." + to_continue: "رجاءً لِج" + preferences: "عليك الولوج لتغيير تفضيلاتك الشّخصيّة." forgot: "لا أذكر معلومات حسابي" - not_approved: "لم تتمّ الموافقة على حسابك بعد. سيصلك إشعار عبر البريد عندما يجهز لتسجل دخولك." + not_approved: "لم تتمّ الموافقة على حسابك بعد. سيصلك إشعار عبر البريد عندما يجهز لتلج." google: title: "عبر غوغل" message: "يستوثق عبر غوغل (تأكّد من تعطيل مانعات المنبثقات)" @@ -1111,10 +1134,13 @@ ar: accept_title: "دعوة" welcome_to: "مرحباً بك في %{site_name}!" invited_by: "لقد دعاك:" - social_login_available: "يمكنك أيضًا الدخول عبر أيّ شبكة اجتماعيّة مستخدمًا هذا البريد." + social_login_available: "يمكنك أيضًا الولوج عبر أيّ شبكة اجتماعيّة مستخدمًا هذا البريد." your_email: "بريدك الإلكتروني %{email}." accept_invite: "قُبول الدعوة " - success: "أُنشئ حسابك وقد سجلت الدخول الآن." + success: "أُنشئ حسابك وقد ولجت الآن." + name_label: "الاسم" + password_label: "ضع كلمة المرور" + optional_description: "(إختياري)" password_reset: continue: "تابع نحو %{site_name}" emoji_set: @@ -1320,6 +1346,7 @@ ar: first: تلك المشاركة الاولى seen: قرأتها unseen: لم أقرأها + all_tags: ضُم كل الوسوم statuses: label: بشرط أن تكون المواضيع open: مفتوحة @@ -1355,8 +1382,10 @@ ar: dismiss_new: "أزل الجديد" toggle: "إيقاف/تشغيل الاختيار المتعدد للمواضيع" actions: "عمليات تنفذ دفعة واحدة" + change_category: "ضع الفئة" close_topics: "إغلاق المواضيع" archive_topics: "أرشفة المواضيع" + notification_level: "التنبيهات" choose_new_category: "اختر القسم الجديد للمواضيع:" selected: zero: "لم تحدد شيئا." @@ -1365,7 +1394,10 @@ ar: few: "حددت {{count}} مواضيع." many: "حددت {{count}} موضوعا." other: "حددت {{count}} موضوع." + change_tags: "غيّر الوسم" + append_tags: "ألحق الوسوم" choose_new_tags: "اختر وسوم جديدة لهذه المواضيع:" + choose_append_tags: "اختر وسوم جديدة لإلحاقها بالمواضيع" changed_tags: "تغيرت وسوم هذه المواضيع." none: unread: "ليست هناك مواضيع غير مقروءة." @@ -1433,7 +1465,7 @@ ar: invalid_access: title: "الموضوع خاص" description: "لا تملك صلاحيات للوصول لهذا الموضوع" - login_required: "عليك بتسجيل الدخول لمشاهدة هذا الموضوع." + login_required: "عليك الولوج لمشاهدة هذا الموضوع." server_error: title: "فشل تحميل الموضوع" description: "آسفون، تعذر علينا تحميل هذا الموضوع، قد يرجع ذلك إلى مشكلة بالاتصال. من فضلك حاول مجددا. أخبرنا بالمشكلة إن استمر حدوثها." @@ -1481,6 +1513,11 @@ ar: jump_reply_up: انتقل إلى أول رد jump_reply_down: انتقل إلى آخر رد deleted: "الموضوع محذوف" + auto_update_input: + tomorrow: "غداً" + this_weekend: "هذا الأسبوع" + next_week: "الاسبوع القادم" + next_month: "الشهر القادم" status_update_notice: auto_open: "سيُفتح هذا الموضوع آليًّا %{timeLeft}." auto_close: "سيُغلق هذا الموضوع آليًّا %{timeLeft}." @@ -1629,7 +1666,7 @@ ar: username_placeholder: "اسم المستخدم" action: 'أرسل دعوة' help: 'ادعُ الغير إلى هذا الموضوع عبر البريد الإلكترونيّ أو الإشعارات' - to_forum: "سنُرسل بريدًا يتيح لصديقك الانضمام مباشرةً بنقر رابط ما، الدخول غير مطلوب." + to_forum: "سنُرسل بريدًا يتيح لصديقك الانضمام مباشرةً بنقر رابط فيه، الولوج غير مطلوب." sso_enabled: "أدخل اسم مَن تريد دعوته إلى هذا الموضوع." to_topic_blank: "أدخل اسم مَن تريد دعوته إلى هذا الموضوع أو بريده." to_topic_email: "لقد أدخلت عنوان بريد إلكترونيّ. سنرسل بريدًا يحتوي دعوة تتيح لصديقك الرّد مباشرة على هذا الموضوع." @@ -1639,7 +1676,7 @@ ar: success_email: "قمنا بإرسال دعوة بالبريد لـ {{emailOrUsername}} . سيتم تنبيهك عند قبول الدعوة , تحقق من تبويب الدعوات في صفحتك الشخصية لمتابعة دعوتك." success_username: "دعونا هذا العضو للمشاركة في هذا الموضوع." error: "نأسف لا يمكنك دعوة هذا المُستَخدم , ربما لأنه مُسَجِل لدينا مسبقاً (الدعوات محدودة)" - login_reply: 'عليك تسجيل الدخول للرّدّ' + login_reply: 'عليك الولوج للرّدّ' filters: n_posts: zero: "لا مشاركات" @@ -1777,7 +1814,7 @@ ar: too_many_dragged_and_dropped_files: "آسفون، يمكنك فقط رفع 10 ملفّات كلّ مرة." image_upload_not_allowed_for_new_user: "نأسف، لا يمكن للمستخدمين الجدد رفع الصّور." attachment_upload_not_allowed_for_new_user: "نأسف، لا يمكن للمستخدمين الجدد رفع المرفقات." - attachment_download_requires_login: "نأسف، عليك تسجيل الدخول لتنزيل المرفقات." + attachment_download_requires_login: "نأسف، عليك الولوج لتنزيل المرفقات." abandon: confirm: "أمتأكد من التخلي عن المشاركة؟" no_value: "لا، أبقها" @@ -1794,7 +1831,7 @@ ar: has_liked: "أعجبتك هذه المشاركة" undo_like: "ألغِ الإعجاب" edit: "عدّل المشاركة" - edit_anonymous: "نأسف، عليك تسجيل الدخول لتعديل المشاركة." + edit_anonymous: "نأسف، عليك الولوج لتعديل المشاركة." flag: "بلَغ عن هذا الموضوع او ارسل تنبيه خاص لأدارة الموقع." delete: "احذف المشاركة" undelete: "التراجع عن حذف المشاركة" @@ -2131,7 +2168,7 @@ ar: two: "إثنتان أخريتان" few: "%{count} أخرى" many: "%{count} أخرى" - other: "%{count} أخرى" + other: "%{count} أكثر" topic_statuses: warning: help: "هذا تحذير رسمي." @@ -2784,7 +2821,10 @@ ar: import: "استيراد" delete: "حذف" delete_confirm: "أأحذف هذه السّمة؟" + color: "لون" + opacity: "الشفافية" copy: "نسخ" + theme_owner: "غير قابل للتعديل ، مملوك بواسطة : " email_templates: title: "قالب البريد الالكتروني " subject: "الموضوع" @@ -2793,6 +2833,30 @@ ar: none_selected: "اختر أحد قوالب البريد لتعدله." revert: "اعاده التغيرات " revert_confirm: "هل انت متاكد من انك تريد اعاده التغيرات؟ " + theme: + title: "السمات" + common: "شائع" + desktop: "سطح المكتب" + mobile: "هاتف محمول" + preview: "مراجعة" + is_default: "السمة مفعلة إفتراضيا" + custom_sections: "مُخصص القسم:" + theme_components: "مكونات السمة" + uploads: "المرفوعة" + upload: "رفع" + child_themes_check: "السمة " + css_html: "مُخصص CSS/HTML" + edit_css_html: "عدل CSS/HTML" + about_theme: "عن السمة" + license: "الرخصة" + update_to_latest: "حدث لآخر" + check_for_updates: "تحقق من التحديثات" + scss: + text: "CSS" + head_tag: + text: "" + body_tag: + text: "" colors: title: "الألوان" long_title: "مخططات الألوان" @@ -2913,6 +2977,8 @@ ar: block: "حظر" do_nothing: "لا تفعل شيء" staff_actions: + all: "الكل" + filter: "رشح:" title: "عمليات المشرفين" clear_filters: "إظهار كل شيء" staff_user: "عضو إداري" @@ -2933,6 +2999,8 @@ ar: change_trust_level: "تغيير مستوى الثقة" change_username: "تغيير اسم المستخدم" change_site_setting: "تغيير اعدادات الموقع" + change_theme: "غيّر السمة" + delete_theme: "أحذف السمة" change_site_text: "تغيير نص الموقع." suspend_user: "حظر المستخدم" unsuspend_user: "رفع الحظر " @@ -2964,6 +3032,7 @@ ar: change_readonly_mode: "غير وضع القراءة فقط" backup_download: "حَمل النسخة الاحتياطية" backup_destroy: "دمر النسخة الاحتياطية" + reviewed_post: "مشاركة مراجعة" screened_emails: title: "عناوين بريد إلكتروني محجوبة." description: "عندما يحاول أحدهم إنشاء حساب جديد، ستُفحص عناوين البريد الإلكتروني وسيُمنع التسجيل إن تطابق إحداها، أو أن يُتّخذ أي إجراء آخر." @@ -3280,7 +3349,7 @@ ar: legal: "قانوني" uncategorized: 'أخرى' backups: "النسخ الإحتياطية" - login: "تسجيل الدخول" + login: "الولوج" plugins: "الإضافات " user_preferences: "تفضيلات العضو" tags: "الوسوم" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 60ba98671b..ce884503d0 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -139,19 +139,19 @@ bs_BA: bootstrap_mode_disabled: "Bootstrap mod će biti ugašen u sljedećih 24 sata." s3: regions: - us_east_1: "SAD Istok (Sjeverna Virdžinija)" - us_west_1: "SAD Zapad (Sjeverna Kalifornija)" - us_west_2: "SAD Zapad (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Irska)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Azija Pacifik (Singapur)" - ap_southeast_2: "Azija Pacifik (Sidnej)" - ap_south_1: "Azija Pacifik (Mumai)" ap_northeast_1: "Azija Pacifik (Tokio)" ap_northeast_2: "Azija Pacifik (Seul)" - sa_east_1: "Južna Amerika (Sao Paulo)" + ap_south_1: "Azija Pacifik (Mumai)" + ap_southeast_1: "Azija Pacifik (Singapur)" + ap_southeast_2: "Azija Pacifik (Sidnej)" cn_north_1: "Kina (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irska)" + sa_east_1: "Južna Amerika (Sao Paulo)" + us_east_1: "SAD Istok (Sjeverna Virdžinija)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "SAD Zapad (Sjeverna Kalifornija)" + us_west_2: "SAD Zapad (Oregon)" edit: 'izmjeni naslov i kategoriju ove teme' not_implemented: "That feature hasn't been implemented yet, sorry!" no_value: "Ne" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index ab0a8bbe57..10307b7a83 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -119,6 +119,7 @@ cs: other: "za %{count} let" previous_month: 'Předchozí měsíc' next_month: 'Další měsíc' + placeholder: Vyber datum share: topic: 'sdílet odkaz na toto téma' post: 'příspěvek #%{postNumber}' @@ -157,21 +158,24 @@ cs: emails_are_disabled: "Všechny odchozí emaily byly administrátorem vypnuty. Žádné odchozí emaily nebudou odeslány." bootstrap_mode_enabled: "Aby se váš web jednodušeji rozjel, nachází se v režimu bootstrap. Všichni noví uživatelé začínají s důveryhodností 1 a mají povolené denní odesílání souhrnných emailů. Tento režim se automaticky vypne, jakmile počet registrovaných uživatelů překročí %{min_users}." bootstrap_mode_disabled: "Režim bootstrap bude deaktivován v následujících 24 hodinách." + themes: + default_description: "Výchozí" s3: regions: - us_east_1: "Východ USA (S. Virginie)" - us_west_1: "Západ USA (S. Kalifornie)" - us_west_2: "Západ USA (Oregon)" - us_gov_west_1: "AWS GovCloud (USA)" - eu_west_1: "EU (Irsko)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapur)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Bombaj)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Soul)" - sa_east_1: "Jižní Amerika (Sao Paulo)" + ap_south_1: "Asia Pacific (Bombaj)" + ap_southeast_1: "Asia Pacific (Singapur)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "Čína (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irsko)" + eu_west_2: "EU (Londýn)" + sa_east_1: "Jižní Amerika (Sao Paulo)" + us_east_1: "Východ USA (S. Virginie)" + us_gov_west_1: "AWS GovCloud (USA)" + us_west_1: "Západ USA (S. Kalifornie)" + us_west_2: "Západ USA (Oregon)" edit: 'upravit název a kategorii příspěvku' not_implemented: "Tato funkce ještě nebyla naprogramována, omlouváme se." no_value: "Ne" @@ -351,6 +355,8 @@ cs: change_group_setting: "Změnit nastavení skupiny" add_user_to_group: "Přidat uživatele" remove_user_from_group: "Odebrat uživatele" + make_user_group_owner: "Udělej vlastníkem" + remove_user_as_group_owner: "Odstraň vlastníka" groups: logs: title: "Logy" @@ -364,9 +370,18 @@ cs: edit: title: 'Editovat skupinu' full_name: 'Celé jméno' + add_members: "Přidat členy" + delete_member_confirm: "Odstranit '%{username}' ze skupiny '%{group}'?" + name_placeholder: "Název skupiny, bez mezer, stejná pravidla jako pro uživatelská jména" add: "Přidat" + join: "Přidat se ke skupině" + leave: "Opustit skupinu" + request: "Požádat o členství ve skupině" automatic_group: Automatická skupina + closed_group: Uzavřená skupina + is_group_user: "Jsi členem této skupiny" membership: "Členství" + name: "Název" user_count: "Počet členů" bio: "O skupině" selector_placeholder: "Přidat členy" @@ -384,6 +399,10 @@ cs: posts: "Odpovědi" mentions: "Zmínění" messages: "Zprávy" + visibility_levels: + title: "Kdo vidí tuto skupinu?" + public: "Všichni" + members: "Vlastníci skupiny, členové a správci" alias_levels: title: "Kdo může do této skupiny psát zprávy a @zmiňovat ji?" nobody: "Nikdo" @@ -475,6 +494,11 @@ cs: profile: "Profil" mute: "Ignorovat" edit: "Upravit nastavení" + download_archive: + button_text: "Stáhnout vše" + confirm: "Jste si jistí, že chcete stáhnout všechny své příspěvky?" + success: "Stahování zahájeno, až bude dokončeno, přijde ti zpráva." + rate_limit_error: "Příspěvky mohou být staženy jen jednou za den. Zkus to, prosím, zítra." new_private_message: "Nová zpráva" private_message: "Zpráva" private_messages: "Zprávy" @@ -572,12 +596,23 @@ cs: move_to_archive: "Archivovat" failed_to_move: "Nepodařilo se přesunout vybrané zprávy (možná vám spadlo připojení)" select_all: "Vybrat vše" + preferences_nav: + account: "Účet" + profile: "Profil" + emails: "Emaily" + notifications: "Upozornění" + categories: "Kategorie" + tags: "Tagy" + interface: "Rozhraní" + apps: "Aplikace" change_password: success: "(email odeslán)" in_progress: "(odesílám)" error: "(chyba)" action: "Odeslat email na obnovu hesla" set_password: "Nastavit heslo" + choose_new: "Zvolte si nové heslo" + choose: "Vyber si heslo" change_about: title: "Změna o mně" error: "Došlo k chybě při pokusu změnit tuto hodnotu." @@ -627,9 +662,11 @@ cs: ok: "Parádní jméno" username: title: "Uživatelské jméno" + instructions: "jedinečné, bez mezer, krátké" short_instructions: "Lidé vás mohou zmínit pomocí @{{username}}" available: "Vaše uživatelské jméno je volné" not_available: "Není k dispozici. Co třeba {{suggestion}}?" + not_available_no_suggestion: "Není k dispozici" too_short: "Uživatelské jméno je moc krátké" too_long: "Uživatelské jméno je moc dlouhé" checking: "Zjišťuji, zda je uživatelské jméno volné..." @@ -719,6 +756,8 @@ cs: expired: "Poznávka je už prošlá." rescind: "Smazat" rescinded: "Pozvánka odstraněna" + rescind_all: "Odstranit všechny pozvánky" + rescinded_all: "Všechny pozvánky odstraněny!" reinvite: "Znovu poslat pozvánku" reinvite_all: "Znovu poslat všechny pozvánky" reinvited: "Pozvánka byla opětovně odeslána." @@ -739,6 +778,7 @@ cs: same_as_username: "Vaše heslo je stejné jako Vaše uživatelské jméno." same_as_email: "Vaše heslo je stejné jako váš e-mail." ok: "Vaše heslo je v pořádku." + instructions: "alespoň %{count} znaků" summary: title: "Souhrn" stats: "Statistiky" @@ -901,6 +941,8 @@ cs: complete_email_found: "Byl nalezen účet odpovídající emailu %{email}. Za chvilku obdržíte email s instrukcemi jak přenastavit vaše heslo." complete_username_not_found: "Nebyl nalezen účet s uživatelským jménem %{username}" complete_email_not_found: "Nebyl nalezen účet s odpovídající emailu %{email}" + button_ok: "OK" + button_help: "Nápověda" login: title: "Přihlásit se" username: "Uživatel" @@ -920,6 +962,7 @@ cs: not_allowed_from_ip_address: "Z této IP adresy se nemůžete přihlásit." admin_not_allowed_from_ip_address: "Z této IP adresy se nemůžete přihlásit jako administrátor." resend_activation_email: "Klikněte sem pro zaslání aktivačního emailu." + submit_new_email: "Aktualizovat emailovou adresu" sent_activation_email_again: "Zaslali jsme vám další aktivační email na {{currentEmail}}. Může trvat několik minut, než vám dorazí. Zkontrolujte také vaši složku s nevyžádanou pošlou." to_continue: "Přihlaš se, prosím" preferences: "Pro to, aby jsi mohl měnit své uživatelské nastavení, se musíš přihlásit." @@ -947,14 +990,22 @@ cs: message: "Autentizuji přes GitHub (ujistěte se, že nemáte zablokovaná popup okna)" invites: accept_title: "Pozvání" + welcome_to: "Vítejte na %{site_name}!" invited_by: "Pozvání od:" + accept_invite: "Přijmout pozvání" success: "Váš účet byl vytvořen a nyní jste přihlášeni." + name_label: "Jméno" + password_label: "Nastavit heslo" + optional_description: "(volitelné)" + password_reset: + continue: "Pokračovat na %{site_name}" emoji_set: apple_international: "Apple/Mezinárodní" google: "Google" twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Pouze kategorie" categories_with_featured_topics: "Kategorie a vybrané příspěvky" @@ -969,6 +1020,7 @@ cs: options: "Možnosti" whisper: "šeptat" unlist: "neviditelné" + blockquote_text: "Bloková citace" add_warning: "Toto je oficiální varování." toggle_whisper: "Přepnout šeptání" toggle_unlisted: "Přepnout neviditelné" @@ -1117,6 +1169,7 @@ cs: sort_by: "Seřadil" relevance: "Relevance" latest_post: "Poslední příspěvek" + latest_topic: "Nejnovější téma" most_viewed: "Nejzobrazovanější" most_liked: "Nejoblíbenější" select_all: "Vybrat vše" @@ -1131,6 +1184,7 @@ cs: no_more_results: "Nenalezeny žádné další výsledky." searching: "Hledám ..." post_format: "#{{post_number}} od {{username}}" + results_page: "Výsledky hledání" context: user: "Vyhledat příspěvky od @{{username}}" category: "Hledat v kategorii #{{category}}" @@ -1162,6 +1216,7 @@ cs: seen: jsem četl unseen: jsem nečetl wiki: jsou wiki + all_tags: Obsahuje všechny štítky statuses: label: Kde příspěvky open: jsou otevřeny @@ -1197,8 +1252,10 @@ cs: dismiss_new: "Odbýt nová" toggle: "hromadný výběr témat" actions: "Hromadné akce" + change_category: "Nastavit kategorii" close_topics: "Zavřít téma" archive_topics: "Archivovat témata" + notification_level: "Upozornění" choose_new_category: "Zvolte novou kategorii pro témata:" selected: one: "Vybrali jste 1 téma." @@ -1317,6 +1374,7 @@ cs: go: "go" jump_bottom: "na poslední příspěvek" jump_prompt: "přejít na..." + jump_prompt_of: "%{count} příspěvků" jump_prompt_long: "Na který příspěvek chcete přejít?" jump_bottom_with_number: "Skočit na příspěvěk %{post_number}" total: celkem příspěvků @@ -1509,9 +1567,11 @@ cs: post: reply: " {{replyAvatar}} {{usernameLink}}" reply_topic: " {{link}}" + quote_reply: "Cituj" edit: "Editujete {{link}} {{replyAvatar}} {{username}}" edit_reason: "Důvod: " post_number: "příspěvek č. {{number}}" + wiki_last_edited_on: "wiki naposledy upravena" last_edited_on: "příspěvek naposledy upraven" reply_as_new_topic: "Odpovědět v propojeném tématu" continue_discussion: "Pokračující diskuze z {{postLink}}:" @@ -1562,6 +1622,8 @@ cs: via_email: "tento příspěvek byl přijat přes email" via_auto_generated_email: "tento příspěvek byl přijat přes automaticky generovaný email" whisper: "tento příspěvek je soukromé šeptání pro moderátory" + wiki: + about: "toto je wiki příspěvek" archetypes: save: 'Uložit nastavení' few_likes_left: "Díky za šíření lásky! Zbývá ti pro dnešek už jen několi \"líbí se\"." @@ -1571,6 +1633,7 @@ cs: has_liked: "tento příspěvek se mi líbí" undo_like: "už se mi to nelíbí" edit: "upravit příspěvek" + edit_action: "Upravit" edit_anonymous: "Omlouváme se, ale pro editaci tohoto příspěvku musíte být přihlášení." flag: "nahlásit příspěvek moderátorovi" delete: "smazat příspěvek" @@ -1694,6 +1757,11 @@ cs: one: "Opravdu chcete odstranit tento příspěvek?" few: "Opravdu chcete odstranit všechny tyto příspěvky?" other: "Opravdu chcete odstranit všechny tyto příspěvky?" + merge: + confirm: + one: "Opravdu chceš sloučit tyto příspěvky?" + few: "Opravdu chceš sloučit {{count}} příspěvky?" + other: "Opravdu chceš sloučit {{count}} příspěvků?" revisions: controls: first: "První revize" @@ -1703,14 +1771,26 @@ cs: hide: "Schovejte revizi" show: "Zobrazte revizi" revert: "Vrátit se k této revizi" + edit_wiki: "Upravit Wiki" + edit_post: "upravit příspěvek" comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: title: "Vykreslený příspěvek se změnami zobrazenými v textu" + button: 'HTML' side_by_side: title: "Rozdíli mezi vykreslenými příspěveky vedle sebe" + button: 'HTML' side_by_side_markdown: title: "Show the raw source diffs side-by-side" + raw_email: + displays: + text_part: + title: "Ukázat textovou část emailu" + button: 'Text' + html_part: + title: "Zobrazit html část emailu" + button: 'HTML' category: can: 'smí… ' none: '(bez kategorie)' @@ -1794,6 +1874,8 @@ cs: created: "Vytvořena" sort_ascending: 'Vzestupně' sort_descending: 'Sestupně' + subcategory_list_styles: + rows: "Řady" flagging: title: 'Děkujeme, že pomáháte udržovat komunitu zdvořilou!' action: 'Nahlásit příspěvek' @@ -2324,6 +2406,8 @@ cs: description: "Popis" go_back: "Zpět na seznam" payload_url_placeholder: "https://example.com/postreceive" + content_type: "Typ obsahu" + wildcard_event: "Pošli mi vše." active: "Aktivní" delivery_status: inactive: "Neaktivní" @@ -2434,6 +2518,9 @@ cs: color: "Barva" opacity: "Neprůhlednost" copy: "Kopírovat" + copy_to_clipboard: "Kopírovat do schránky" + copied_to_clipboard: "Zkopírováno do schránky" + theme_owner: "není editovatelné, vlastněno:" email_templates: title: "Šablona emailu" subject: "Předmět" @@ -2442,6 +2529,12 @@ cs: none_selected: "Pro začátek editace zvolte šablonu emailu." revert: "Vrátit změny" revert_confirm: "Opravdu chcete vrátit změny?" + theme: + customize_desc: "Přizpůsobit:" + edit: "Upravit" + mobile: "Mobilní verze" + preview: "Náhled" + color_scheme: "Barevné schéma" colors: title: "Barvy" long_title: "Barevná schémata" @@ -2505,6 +2598,8 @@ cs: delivery_method: "Způsob doručení" preview_digest_desc: "Zobraz náhled mailů se souhrnem posílaným neaktivním uživatelúm." refresh: "Aktualizovat" + send_digest: "Poslat" + sending_email: "Posílám email..." format: "Formát" html: "html" text: "text" @@ -2557,6 +2652,8 @@ cs: block: "blokovat" do_nothing: "nedělat nic" staff_actions: + all: "vše" + filter: "Filtr:" title: "Akce redaktorů" clear_filters: "Zobrazit vše" staff_user: "Člen redakce" @@ -2596,9 +2693,14 @@ cs: revoke_admin: "odebrat administrátorská práva" grant_moderation: "udělit moderátorská práva" revoke_moderation: "odebrat moderátorská práva" + backup_create: "vytvořit zálohu" deleted_tag: "smazaný štítek" renamed_tag: "přejmenovaný štítek" revoke_email: "Odebrat email" + activate_user: "aktivovat uživatele" + deactivate_user: "deaktivovat uživatele" + backup_download: "stáhnout zálohu" + backup_destroy: "smazat zálohu" screened_emails: title: "Filtrované emaily" description: "Při registraci nového účtu budou konzultovány následujíci adresy. Při shodě bude registrace zablokována, nebo bude provedena jiná akce." @@ -2891,6 +2993,7 @@ cs: user_preferences: "Uživatelká nastavení" tags: "Tagy" search: "Vyhledávání" + groups: "Skupiny" badges: title: Odznaky new_badge: Nový odznak diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 25e59cb583..4424690f4c 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -148,20 +148,20 @@ da: default_description: "Standard" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Irland)" - eu_west_2: "EU (London)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asien (Singapore)" - ap_southeast_2: "Asien (Sydney)" - ap_south_1: "Asien (Mumbai)" ap_northeast_1: "Asien (Tokyo)" ap_northeast_2: "Asien (Seoul)" - sa_east_1: "Sydamerika (Sao Paulo)" + ap_south_1: "Asien (Mumbai)" + ap_southeast_1: "Asien (Singapore)" + ap_southeast_2: "Asien (Sydney)" cn_north_1: "Kina (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irland)" + eu_west_2: "EU (London)" + sa_east_1: "Sydamerika (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'redigér titel og kategori for dette emne' not_implemented: "Beklager, denne feature er ikke blevet implementeret endnu." no_value: "Nej" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 1ef54b63d9..581d1a023b 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -148,20 +148,20 @@ de: default_description: "Standard" s3: regions: - us_east_1: "USA Ost (Nord-Virginia)" - us_west_1: "USA West (Nordkalifornien)" - us_west_2: "USA West (Oregon)" - us_gov_west_1: "AWS GovCloud (USA)" - eu_west_1: "EU (Irland)" - eu_west_2: "EU (London)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asien-Pazifik (Singapur)" - ap_southeast_2: "Asien-Pazifik (Sydney)" - ap_south_1: "Asien-Pazifik (Mumbai)" ap_northeast_1: "Asien-Pazifik (Tokio)" ap_northeast_2: "Asien-Pazifik (Seoul)" - sa_east_1: "Südamerika (São Paulo)" + ap_south_1: "Asien-Pazifik (Mumbai)" + ap_southeast_1: "Asien-Pazifik (Singapur)" + ap_southeast_2: "Asien-Pazifik (Sydney)" cn_north_1: "China (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irland)" + eu_west_2: "EU (London)" + sa_east_1: "Südamerika (São Paulo)" + us_east_1: "USA Ost (Nord-Virginia)" + us_gov_west_1: "AWS GovCloud (USA)" + us_west_1: "USA West (Nordkalifornien)" + us_west_2: "USA West (Oregon)" edit: 'Titel und Kategorie dieses Themas ändern' not_implemented: "Entschuldige, diese Funktion wurde noch nicht implementiert!" no_value: "Nein" @@ -1011,7 +1011,6 @@ de: twitter: "Twitter" emoji_one: "Emoji One" win10: "Windows 10" - google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "nur Kategorien" @@ -3014,6 +3013,7 @@ de: reset_bounce_score: label: "Zurücksetzen" title: "Anzahl unzustellbarer E-Mails auf 0 zurücksetzen" + visit_profile: "Besuche die Benutzer-Einstellungen, um dieses Profil zu bearbeiten" deactivate_explanation: "Ein deaktivierter Benutzer muss seine E-Mail-Adresse erneut bestätigen." suspended_explanation: "Ein gesperrter Benutzer kann sich nicht anmelden." block_explanation: "Ein geblockter Benutzer kann keine Themen erstellen oder Beiträge veröffentlichen." diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 4924686d61..620ff626c9 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -9,15 +9,15 @@ el: js: number: format: - separator: "." - delimiter: "," + separator: "," + delimiter: "." human: storage_units: format: '%n %u' units: byte: one: Byte - other: Byte + other: Bytes gb: GB kb: KB mb: MB @@ -66,8 +66,8 @@ el: almost_x_years: one: "1χ" other: "%{count}έ" - date_month: "DD ΜΜΜ" - date_year: "ΜΜΜ 'ΕΕ" + date_month: "DD MMM" + date_year: "MMM 'YY" medium: x_minutes: one: "1 λεπτό" @@ -78,7 +78,7 @@ el: x_days: one: "1 ημέρα" other: "%{count} ημέρες" - date_year: "ΜΜΜ Η, 'ΕΕ" + date_year: "MMM D, 'YY" medium_with_ago: x_minutes: one: "πριν από 1 λεπτό " @@ -104,16 +104,16 @@ el: placeholder: Επιλέξτε ημερομηνία share: topic: 'κοινοποίησε έναν σύνδεσμο προς αυτό το νήμα' - post: 'Ανάρτηση #%{postNumber}' + post: 'ανάρτηση #%{postNumber}' close: 'κλείσιμο' twitter: 'κοινοποίησε τον σύνδεσμο στο Twitter' facebook: 'κοινοποίησε τον σύνδεσμο στο Facebook' google+: 'κοινοποίησε τον σύνδεσμο στο Google+' email: 'στείλε αυτόν τον σύνδεσμο με ένα μήνυμα ηλεκτρονικού ταχυδρομείου' action_codes: - public_topic: "έκανε το θέμα δημόσιο στις ${when}" - private_topic: "έκανε το θέμα ιδιωτικό στις %{when}" - split_topic: "διαχώρισε αυτό το θέμα στις %{when}" + public_topic: "έκανε το νήμα δημόσιο στις %{when}" + private_topic: "έκανε το νήμα ιδιωτικό στις %{when}" + split_topic: "διαχώρισε αυτό το νήμα στις %{when}" invited_user: "προσκάλεσε τον χρήστη %{who} στις %{when}" invited_group: "προσκάλεσε τον χρήστη %{who} στις %{when}" removed_user: "αφαίρεσε τον χρήστη %{who} στις %{when}" @@ -137,8 +137,8 @@ el: enabled: 'καταχωρήθηκε %{when}' disabled: 'μη καταχωρημένο %{when}' banner: - enabled: 'έγινε μπάνερ %{when}. Θα εμφανίζεται στην κορυφή κάθε σελίδας μέχρι να απορριφθεί από τον χρήστη.' - disabled: 'αφαιρέθηκε μπάνερ %{when}. Δεν θα εμφανίζεται πια στην κορυφή κάθε σελίδας.' + enabled: 'το έκανε νήμα ανακοίνωσης στις %{when}. Θα εμφανίζεται στην κορυφή κάθε σελίδας μέχρι να απορριφθεί από τον χρήστη.' + disabled: 'αφαίρεσε αυτό το νήμα ανακοίνωσης %{when}. Δεν θα εμφανίζεται πλέον στην κορυφή κάθε σελίδας.' topic_admin_menu: "Διαχείριση νήματος" wizard_required: "Καλώς ήλθατε στο νέο σας Discourse! Ας αρχίσουμε με τον οδηγό εγκατάστασης ✨" emails_are_disabled: "Τα εξερχόμενα μηνύματα ηλεκτρονικού ταχυδρομείου έχουν απενεργοποιηθεί από κάποιον διαχειριστή. Δε θα σταλεί κανένα μήνυμα ηλεκτρονικού ταχυδρομείου." @@ -148,21 +148,22 @@ el: default_description: "Προεπιλογή" s3: regions: - us_east_1: "Ανατολικές ΗΠΑ (Β. Βιρτζίνια)" - us_west_1: "Δυτικές ΗΠΑ (Β. Καλιφόρνια)" - us_west_2: "Δυτικές ΗΠΑ (Όρεγκον)" - us_gov_west_1: "AWS GovCloud (ΗΠΑ)" - eu_west_1: "ΕΕ (Ιρλανδία)" - eu_west_2: "EU (Λονδίνο)" - eu_central_1: "ΕΕ (Φρανκφούρτη)" - ap_southeast_1: "Ασία-Ειρηνικός (Σιγκαπούρη)" - ap_southeast_2: "Ασία-Ειρηνικός (Σίδνεϊ)" - ap_south_1: "Ασία - Ειρηνικός (Μομβάη)" ap_northeast_1: "Ασία-Ειρηνικός (Τόκιο)" ap_northeast_2: "Ασία-Ειρηνικός (Σεούλ)" - sa_east_1: "Ασία-Ειρηνικός (Σάο Πάολο)" - cn_north_1: "Κίνα(Πεκίνο)" - edit: 'αλλαγή του τίτλου και την κατηγορίας του νήματος' + ap_south_1: "Ασία - Ειρηνικός (Μομβάη)" + ap_southeast_1: "Ασία-Ειρηνικός (Σιγκαπούρη)" + ap_southeast_2: "Ασία-Ειρηνικός (Σίδνεϊ)" + cn_north_1: "Κίνα (Πεκίνο)" + eu_central_1: "ΕΕ (Φρανκφούρτη)" + eu_west_1: "ΕΕ (Ιρλανδία)" + eu_west_2: "EU (Λονδίνο)" + sa_east_1: "Νότια Αμερική (Σάο Πάολο)" + us_east_1: "Ανατολικές ΗΠΑ (Β. Βιρτζίνια)" + us_east_2: "Ανατολικές ΗΠΑ (Οχάιο)" + us_gov_west_1: "AWS GovCloud (ΗΠΑ)" + us_west_1: "Δυτικές ΗΠΑ (Β. Καλιφόρνια)" + us_west_2: "Δυτικές ΗΠΑ (Όρεγκον)" + edit: 'αλλαγή του τίτλου και της κατηγορίας του νήματος' not_implemented: "Λυπούμαστε! Αυτή η λειτουργία δεν έχει υλοποιηθεί ακόμα." no_value: "Όχι" yes_value: "Ναι" @@ -179,11 +180,11 @@ el: links: "Σύνδεσμοι" links_lowercase: one: "Σύνδεσμος" - other: "Σύνδεσμοι" + other: "σύνδεσμοι" faq: "Συχνές ερωτήσεις" guidelines: "Οδηγίες" - privacy_policy: "Πολιτική προστασίας δεδομένων" - privacy: "Προστασία δεδομένων" + privacy_policy: "Πολιτική Απορρήτου" + privacy: "Ιδιωτικότητα" terms_of_service: "Όροι χρήσης" mobile_view: "Προβολή για κινητό" desktop_view: "Προβολή για σταθερό υπολογιστή" @@ -222,7 +223,7 @@ el: topic_count: "Νήματα" post_count: "Αναρτήσεις" user_count: "Χρήστες" - active_user_count: "Ενεργός χρήστης" + active_user_count: "Ενεργοί Χρήστες" contact: "Επικοινώνησε μαζί μας" contact_info: "Σε περίπτωση που προκύψει κάποιο κρίσιμο πρόβλημα ή κάποιο επείγον θέμα που αφορά αυτόν τον ιστότοπο, παρακαλούμε να επικοινωνήσεις μαζί μας στο %{contact_info}." bookmarked: @@ -230,14 +231,14 @@ el: clear_bookmarks: "Διαγραφή σελιδοδεικτών" help: bookmark: "Πάτα εδώ για να μπεί σελιδοδείκτης στην πρώτη ανάρτηση του νήματος." - unbookmark: "Πάτα εδώ να αφαιρεθούν όλοι οι σελιδοδείκτες από αυτό το θέμα." + unbookmark: "Πάτα εδώ να αφαιρεθούν όλοι οι σελιδοδείκτες από αυτό το νήμα." bookmarks: not_logged_in: "Λυπούμαστε, για να τοποθετήσεις έναν σελιδοδείκτη θα πρέπει να συνδεθείς πρώτα" created: "έχεις προσθέσει σελιδοδείκτη σε αυτή την ανάρτηση" not_bookmarked: "έχεις διαβάσει αυτή την ανάρτηση. Πάτησε για να προστεθεί σελιδοδείκτης" last_read: "αυτή είναι η τελευταία ανάρτηση που διάβασες. Πάτησε για να προστεθεί σελιδοδείκτης" remove: "Αφαίρεση σελιδοδείκτη" - confirm_clear: "Είστε σίγουρος ότι θέλετε να σβηστούν όλα τα αγαπημένα από αυτό το θέμα;" + confirm_clear: "Είστε σίγουρος ότι θέλετε να σβηστούν όλα τα αγαπημένα από αυτό το νήμα;" topic_count_latest: one: "{{count}} νέο ενημερωμένο νήμα" other: "{{count}} νέα ή ενημερωμένα νήματα." @@ -265,8 +266,8 @@ el: switch_to_anon: "Έναρξη κατάστασης ανωνυμίας" switch_from_anon: "Τερματισμός κατάστασης ανωνυμίας" banner: - close: "Απομάκρυνε αυτή την εικόνα" - edit: "Επεξεργάσου αυτό το banner >>" + close: "Απομάκρυνε αυτό το νήμα ανακοίνωσης" + edit: "Επεξεργάσου αυτό το νήμα ανακοίνωσης >>" choose_topic: none_found: "Δεν βρέθηκαν νήματα." title: @@ -284,9 +285,9 @@ el: view_pending: "προβολή εκκρεμών αναρτήσεων" has_pending_posts: one: "Αυτό το θέμα έχει 1 ανάρτηση που αναμένει έγκριση" - other: "Αυτό το θέμα έχει {{count}} αναρτήσεις που αναμένουν έγκριση" + other: "Αυτό το νήμα έχει {{count}} αναρτήσεις που αναμένουν έγκριση" confirm: "Αποθήκευση αλλαγών" - delete_prompt: "Είστε σίγουρος πως θέλετε να διαγράψετε τον/την %{username}? Αυτό θα διαγράψει όλες τους τις αναρτήσεις και θα μπλοκάρει την διεύθυνση ηλεκτρονικού ταχυδρομείου τους και την IP διεύθυνσή τους." + delete_prompt: "Είστε σίγουρος πως θέλετε να διαγράψετε τον/την %{username}; Αυτό θα διαγράψει όλες τους τις αναρτήσεις και θα μπλοκάρει την διεύθυνση ηλεκτρονικού ταχυδρομείου τους και την IP διεύθυνσή τους." approval: title: "Η ανάρτηση χρειάζεται έγκριση" description: "Η νέα σου ανάρτηση καταχωρήθηκε αλλά χρειάζεται να εγκριθεί από έναν συντονιστή. Παρακαλώ περιμένετε." @@ -295,7 +296,7 @@ el: other: "Έχετε {{count}} εκκρεμείς αναρτήσεις." ok: "OK" user_action: - user_posted_topic: "{{user}} ανάρτησε το νήμα " + user_posted_topic: "{{user}} ανάρτησε το νήμα " you_posted_topic: "Ανάρτησες αυτό το νήμα" user_replied_to_post: "Ο/Η {{user}} απάντησε στο {{post_number}}" you_replied_to_post: "Απάντησες στο νήμα {{post_number}}" @@ -312,36 +313,36 @@ el: filter_name: "φιλτράρισμα με βάση το όνομα χρήστη" title: "Χρήστες" likes_given: "Δόθηκαν" - likes_received: "Λήφθηκαν" + likes_received: "Λήφθησαν" topics_entered: "Προβλήθηκαν" - topics_entered_long: "Θέματα που προβλήθηκαν" + topics_entered_long: "Νήματα που προβλήθηκαν" time_read: "Ωρα ανάγνωσης" - topic_count: "Θέματα" + topic_count: "Νήματα" topic_count_long: "Νήματα που δημιουργήθηκαν" post_count: "Απαντήσεις" - post_count_long: "Απαντήσεις που δημοσιεύθηκαν" + post_count_long: "Αναρτημένες Απαντήσεις" no_results: "Δεν βρέθηκαν αποτελέσματα" days_visited: "Επισκέψεις" days_visited_long: "Μέρες επίσκεψης" posts_read: "Διαβασμένο" - posts_read_long: "Διαβασμένες αναρτήσεις" + posts_read_long: "Διαβασμένες Αναρτήσεις" total_rows: one: "1 χειριστής" - other: "%{count} χειριστές" + other: "%{count} χρήστες" group_histories: actions: change_group_setting: "Αλλαγή ρυθμίσεων ομάδας" add_user_to_group: "Προσθήκη χρήστη" remove_user_from_group: "Αφαίρεση χρήστη" make_user_group_owner: "Κάνε ιδιοκτήτη" - remove_user_as_group_owner: "Απόσυρε ιδιοκτήτη" + remove_user_as_group_owner: "Απέσυρε ιδιοκτήτη" groups: logs: title: "Αρχεία καταγραφής" when: "Όταν" action: "Δράση" - acting_user: "Δραστήριοι χρήστες" - target_user: "Χρήστης στόχος" + acting_user: "Ενεργοί χρήστες" + target_user: "Επιδιοκώμενος χρήστη" subject: "Θέμα" details: "Λεπτομέρειες" from: "Από" @@ -350,19 +351,20 @@ el: title: 'Επεξεργασία ομάδας' full_name: 'Ονοματεπώνυμο' add_members: "Προσθήκη μελών" - delete_member_confirm: "Ναι αφαιρεθεί ο/η '%{username}' από την '%{group}' ομάδα;" + delete_member_confirm: "Να αφαιρεθεί ο/η '%{username}' από την '%{group}' ομάδα;" name_placeholder: "Όνομα ομάδας, χωρίς κενά, ίδιοι κανόνες με τους κανόνες για το όνομα χρήστη" empty: - posts: "Δεν υπάρχουν δημοσιεύσεις από μέλη της ομάδας." + posts: "Δεν υπάρχουν αναρτήσεις από μέλη της ομάδας." members: "Δεν υπάρχουν μέλη σε αυτή την ομάδα." mentions: "Δεν υπάρχει καμία αναφορά για αυτή την ομάδα." messages: "Δεν υπάρχουν μηνύματα για αυτή την ομάδα." - topics: "Δεν υπάρχουν θέματα από μέλη της ομάδας." + topics: "Δεν υπάρχουν νήματα από μέλη της ομάδας." logs: "Δεν υπάρχουν αρχεία καταγραφής για αυτή την ομάδα." add: "Προσθήκη" join: "Συμμετοχή στην ομάδα" - leave: "Απομάκρυνση από την ομάδα" + leave: "Αποχώρηση από την ομάδα" request: "Αίτημα για συμμετοχή στην ομάδα" + message: "Μήνυμα" automatic_group: Αυτόματη ομάδα closed_group: Κλειστή ομάδα is_group_user: "Είσαι μέλος αυτής της ομάδας" @@ -381,11 +383,17 @@ el: other: "ομάδες" activity: "Δραστηριότητα" members: "Μέλη" - topics: "Θέματα" + topics: "Νήματα" posts: "Αναρτήσεις" mentions: "Αναφορές" messages: "Μηνύματα" notification_level: "Προκαθορισμένο επίπεδο ειδοποιήσεων για μηνύματα ομάδων" + visibility_levels: + title: "Ποιος μπορεί να δει αυτή την ομάδα;" + public: "Όλοι" + members: "Ιδιοκτήτες ομάδας, μέλη και διαχειριστές" + staff: "Ιδιοκτήτες ομάδας και προσωπικό" + owners: "Ιδιοκτήτες ομάδας και διαχειριστές" alias_levels: title: "Ποιός μπορεί να στείλει μήνυμα και να @αναφέρει αυτή την ομάδα?" nobody: "Κανένας" @@ -399,10 +407,10 @@ el: notifications: watching: title: "Παρατηρείται" - description: "Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια δημοσίευση σε κάθε μήνυμα, και η καταμέτρηση των καινούριων απαντήσεων θα εμφανίζεται." + description: "Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια ανάρτηση σε κάθε μήνυμα, και η καταμέτρηση των καινούριων απαντήσεων θα εμφανίζεται." watching_first_post: title: "Παρακολούθηση της πρώτης ανάρτησης" - description: "Θα ειδοποιήστε μόνο για την πρώτη ανάρτηση σε κάθε νέο θέμα σε αυτή την ομάδα." + description: "Θα ειδοποιήστε μόνο για την πρώτη ανάρτηση σε κάθε νέο νήμα σε αυτή την ομάδα." tracking: title: "Παρακολουθείται" description: "Θα λαμβάνεις ειδοποιήσεις εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε εσένα, και η καταμέτρηση των καινούριων απαντήσεων θα εμφανίζεται." @@ -411,14 +419,14 @@ el: description: "Θα ειδοποιηθείτε αν κάποιος αναφέρει το @name σας ή σας απαντήσει" muted: title: "Σε σιγή" - description: "Δε θα λαμβάνεις ποτέ ειδοποιήσεις για οτιδήποτε σχετικά με τις καινούριες δημοσιεύσεις της ομάδας." + description: "Δε θα λαμβάνεις ποτέ ειδοποιήσεις για οτιδήποτε σχετικά με νέα νήματα σε αυτήν την ομάδα." flair_url: "Avatar Flair εικόνα" - flair_url_placeholder: "( προαιρετικό) Εικόνα URL ή Font Awesome κατηγορία " + flair_url_placeholder: "(προαιρετικό) URL εικόνας ή Font Awesome κατηγορία " flair_bg_color: "Avatar Flair χρώμα φόντου" flair_bg_color_placeholder: "(Προαιρετικό) Τιμή χρώματος Hex" flair_color: "Avatar Flair χρώμα" flair_color_placeholder: "(Προαιρετικό) Τιμή χρώματος Hex" - flair_preview_icon: "Προεπισκόπηση εικονίσματος" + flair_preview_icon: "Προεπισκόπηση εικονιδίου" flair_preview_image: "Προεπισκόπηση εικόνας" flair_note: "Σημείωση: Το Flair θα εμφανιστεί μονο για την κύρια ομάδα του χρήστη" user_action_groups: @@ -444,32 +452,32 @@ el: title: "Επαναταξινόμησε τις κατηγορίες" title_long: "Αναδιοργάνωση της λίστας κατηγοριών" fix_order: "Σταθερές θέσεις" - fix_order_tooltip: "Δε έχουν όλες οι κατηγορίες μοναδικό νούμερο θέσης, το οποίο ίσως προκαλέσει αναπάντεχα αποτελέσματα." - save: "Αποθήκευση εντολής" + fix_order_tooltip: "Δεν έχουν όλες οι κατηγορίες μοναδικό νούμερο θέσης, το οποίο ίσως προκαλέσει αναπάντεχα αποτελέσματα." + save: "Αποθήκευση Κατάταξης" apply_all: "Εφαρμογή" position: "Θέση" posts: "Αναρτήσεις" topics: "Νήματα" - latest: "Τελευταία νήματα" - latest_by: "τελευταία ανάρτηση από τον/την" - toggle_ordering: "Αλλαγή ταξινόμησης" + latest: "Πρόσφατες" + latest_by: "πρόσφατα από" + toggle_ordering: "εναλλαγή ταξινόμησης" subcategories: "Υποκατηγορίες" topic_sentence: one: "1 θέμα" - other: "%{count} θέματα" + other: "%{count} νήματα" topic_stat_sentence: one: "%{count} νέο νήμα στα τελευταία %{unit}." other: "%{count} νέα νήματα στα τελευταία %{unit}." ip_lookup: title: Αναζήτηση διεύθυνσης IP hostname: Όνομα εξυπηρετητή - location: Διεύθυνση + location: Τοποθεσία location_not_found: (άγνωστο) organisation: Οργάνωση phone: Τηλέφωνο other_accounts: "Άλλοι λογαριασμοί με αυτή την IP διεύθυνση:" delete_other_accounts: "Διαγραφή %{count}" - username: "Όνομα χρήστη" + username: "χρηστώνυμο" trust_level: "TL" read_time: "χρόνος ανάγνωσης" topics_entered: "νήματα που είδες" @@ -484,9 +492,9 @@ el: edit: "Επεξεργασία ρυθμίσεων" download_archive: button_text: "Λήψη Όλων" - confirm: "Είσαι σίγουρος πως θέλεις να κάνεις λήψη των δημοσιεύσεων σου;" - success: "Όταν θα αρχίσει να κατεβαίνει το αρχείο, θα ειδοποιηθείται μέσω μυνήματος μόλις ολοκληρωθεί η διαδικασία." - rate_limit_error: "Μπορείς να κάνεις λήψη των δημοσιεύσεών σου μια φορά την ημέρα, προσπάθησε ξανά αύριο." + confirm: "Είσαι σίγουρος πως θέλεις να κάνεις λήψη των αναρτήσεών σου;" + success: "Ξεκίνησε η διαδικασία λήψης. Θα ειδοποιηθείτε ]μόλις ολοκληρωθεί η διαδικασία." + rate_limit_error: "Μπορείς να κάνεις λήψη των αναρτήσεών σου μια φορά την ημέρα, προσπάθησε ξανά αύριο." new_private_message: "Νέο Μήνυμα" private_message: "Μήνυμα" private_messages: "Μηνύματα" @@ -498,7 +506,7 @@ el: invited_by: "Προσκλήθηκε από τον/την" trust_level: "Επίπεδο εμπιστοσύνης" notifications: "Ειδοποιήσεις" - statistics: "Αναλυτικά στοιχεία" + statistics: "Στατιστικά" desktop_notifications: label: "Ειδοποιήσεις στην επιφάνεια εργασίας" not_supported: "Οι ειδοποιήσεις δεν υποστηρίζονται από αυτό το πρόγραμμα περιήγησης. Λυπάμαι." @@ -507,12 +515,12 @@ el: perm_denied_expl: "Απορρίψατε την άδεια για ειδοποιήσεις. Επιτρέψτε τις ειδοποιήσεις μέσω των ρυθμίσεων του φυλλομετρητή σας." disable: "Απενεργοποίηση ειδοποιήσεων" enable: "Ενεργοποίηση ειδοποιήσεων" - each_browser_note: "Σημείωση: Έχετε αλλάξει αυτή τη ρύθμιση σε κάθε φυλλομετρητή που χρησιμοποιείτε." - dismiss_notifications: "Διώξτα όλα" + each_browser_note: "Σημείωση: Θα πρέπει να αλλάξετε αυτή τη ρύθμιση σε κάθε φυλλομετρητή που χρησιμοποιείτε." + dismiss_notifications: "Απόρριψη όλων" dismiss_notifications_tooltip: "Όλες οι αδιάβαστες ειδοποιήσεις να χαρακτηριστούν διαβασμένες" first_notification: "Η πρώτη σου ειδοποίηση! Επίλεξε την για να ξεκινήσεις." - disable_jump_reply: "Να μην εστιάζεται η ανάρτηση μου αφ' ότου απαντήσω" - dynamic_favicon: "Δείξε τον αριθμό νέων/ενημερωμένων αναρτήσεων στο εικονίδιο του προγράμματος περιήγησης" + disable_jump_reply: "Να μην εστιάζεται η ανάρτηση μου αφού απαντήσω" + dynamic_favicon: "Δείξε τον αριθμό νέων/ενημερωμένων νημάτων στο εικονίδιο του προγράμματος περιήγησης" theme_default_on_all_devices: "Αυτό να γίνει το προεπιλεγμένο θέμα σε όλες μου τις συσκευές" external_links_in_new_tab: "Άνοιγε όλους τους εξωτερικούς συνδέσμους σε νέα καρτέλα" enable_quoting: "Το κείμενο που επισημαίνεται να παρατίθεται στην απάντηση " @@ -522,7 +530,7 @@ el: moderator_tooltip: "Αυτός ο χρήστης είναι συντονιστής" admin_tooltip: "Αυτός ο χρήστης είναι διαχειριστής" blocked_tooltip: "Ο χρήστης είναι μπλοκαρισμένος" - suspended_notice: "Αυτός ο χρήστης είναι αποκλεισμένος μέχρι τις {{date}}." + suspended_notice: "Αυτός ο χρήστης είναι σε αποβολή μέχρι τις {{date}}." suspended_reason: "Αιτιολογία:" github_profile: "Github" email_activity_summary: "Περίληψη ενεργειών" @@ -538,21 +546,21 @@ el: few_per_day: "Στείλε μου ένα email για κάθε νέα ανάρτηση (περίπου 2 τη μέρα)" tag_settings: "Ετικέτες" watched_tags: "Εθεάθη" - watched_tags_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτές τις ετικέτες. Θα λαμβάνεις ειδοποιήσεις για όλες τις καινούριες δημοσιεύσεις και νήματα, και η καταμέτρηση των καινούριων δημοσιεύσεων θα εμφανίζεται επίσης δίπλα στο νήμα." + watched_tags_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτές τις ετικέτες. Θα λαμβάνεις ειδοποιήσεις για όλες τις καινούριες αναρτήσεις και νήματα, και η καταμέτρηση των καινούριων αναρτήσεων θα εμφανίζεται επίσης δίπλα στο νήμα." tracked_tags: "Παρακολουθείται" - tracked_tags_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτές τις ετικέτες. Η καταμέτρηση των καινούριων δημοσιεύσεων θα εμφανίζεται δίπλα σε αυτό το νήμα." + tracked_tags_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτές τις ετικέτες. Η καταμέτρηση των καινούριων αναρτήσεων θα εμφανίζεται δίπλα σε αυτό το νήμα." muted_tags: "Σίγαση" - muted_tags_instructions: "Δε θα λαμβάνεις ειδοποιήσεις για τίποτα σχετικά με τα καινούρια νήματα, και δε θα εμφανίζονται στα τελευταία. " + muted_tags_instructions: "Δε θα λαμβάνεις ειδοποιήσεις για τίποτα σχετικά με νέα νήματα με αυτές τις ετικέτες και δε θα εμφανίζονται στα τελευταία. " watched_categories: "Παρατηρείται" - watched_categories_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Θα λαμβάνεις ειδοποιήσεις για όλες τις καινούριες δημοσιεύσεις και νήματα, και η καταμέτρηση των δημοσιεύσεων θα εμφανίζεται επίσης δίπλα στο νήμα." + watched_categories_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Θα λαμβάνεις ειδοποιήσεις για όλες τις καινούριες αναρτήσεις και νήματα, και η καταμέτρηση των αναρτήσεων θα εμφανίζεται επίσης δίπλα στο νήμα." tracked_categories: "Παρακολουθείται" - tracked_categories_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Η καταμέτρηση των καινούριων δημοσιεύσεων θα εμφανίζεται δίπλα στο νήμα." - watched_first_post_categories: "Παρακολούθηση πρώτης δημοσίευσης" - watched_first_post_categories_instructions: "Θα ειδοποιηθείς για την πρώτη δημοσίευση του κάθε καινούριου θέματος αυτών των κατηγοριών." - watched_first_post_tags: "Παρακολούθηση πρώτης δημοσίευσης" - watched_first_post_tags_instructions: "Θα ειδοποιηθείς για την πρώτη δημοσίευση του κάθε καινούριου θέματος με αυτές τις ετικέτες. " + tracked_categories_instructions: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Η καταμέτρηση των καινούριων αναρτήσεων θα εμφανίζεται δίπλα στο νήμα." + watched_first_post_categories: "Παρακολούθηση πρώτης ανάρτησης" + watched_first_post_categories_instructions: "Θα ειδοποιηθείς για την πρώτη ανάρτηση σε κάθε νέο νήμα αυτών των κατηγοριών." + watched_first_post_tags: "Παρακολούθηση πρώτης ανάρτησης" + watched_first_post_tags_instructions: "Θα ειδοποιηθείς για την πρώτη ανάρτηση σε κάθε νέο νήμα με αυτές τις ετικέτες. " muted_categories: "Σε σίγαση" - muted_categories_instructions: "Δε θα λαμβάνεις ειδοποιήσεις για τίποτα σχετικά με τις καινούριες δημοσιεύσεις σε αυτές τις κατηγορίες, και δε θα εμφανίζονται στα τελευταία." + muted_categories_instructions: "Δε θα λαμβάνεις ειδοποιήσεις για τίποτα σχετικά με τα νέα νήματα σε αυτές τις κατηγορίες, και δε θα εμφανίζονται στα τελευταία." no_category_access: "Ως συντονιστής έχεις περιορισμένη πρόσβαση στην κατηγορία, η αποθήκευση είναι απενεργοποιημένη." delete_account: "Διαγραφή του λογαριασμού μου " delete_account_confirm: "Είσαι σίγουρος πως θέλεις να διαγράψεις μόνιμα τον λογαριασμό σου; Αυτή η πράξη είναι μη αναστρέψιμη." @@ -560,9 +568,9 @@ el: delete_yourself_not_allowed: "Δεν μπορείς να διαγράψεις τον λογαριασμό σου αυτή την στιγμή. Επικοινώνησε με έναν διαχειριστή για να τον διαγράψει αυτός για σένα." unread_message_count: "Μηνύματα" admin_delete: "Διαγραφή" - users: "χειριστές" + users: "Χρήστες" muted_users: "Σε σίγαση" - muted_users_instructions: "Αποσιώπησε όλες τις ειδοποιήσεις από αυτούς τους χειριστές" + muted_users_instructions: "Αποσιώπησε όλες τις ειδοποιήσεις από αυτούς τους χρήστες." muted_topics_link: "Δείξε τα νήματα που είναι σε σίγαση" watched_topics_link: "Δείξε τα νήματα που παρακολουθούνται" tracked_topics_link: "Δείξτε τα νήματα που παρακολουθούνται" @@ -595,7 +603,7 @@ el: emails: "Emails" notifications: "Ειδοποιήσεις" categories: "Κατηγορίες" - tags: "Εττικέτες" + tags: "Ετικέτες" interface: "Διεπαφή" apps: "Εφαρμογές" change_password: @@ -611,7 +619,7 @@ el: error: "Προέκυψε σφάλμα στην αλλαγή της αξίας." change_username: title: "Αλλαγή χρηστώνυμου" - confirm: "Εάν αλλάξεις το όνομα χρήστη σου, όλες οι προηγούμενες παραθέσεις των δημοσιεύσεων σου και οι αναφορές στο @όνομά σου θα χαθούν. Είσαι απόλυτα βέβαιος πως θέλεις;" + confirm: "Εάν αλλάξεις το όνομα χρήστη σου, όλες οι προηγούμενες παραθέσεις των αναρτήσεων σου και οι αναφορές στο @όνομά σου θα χαθούν. Είσαι απόλυτα βέβαιος πως θέλεις;" taken: "Λυπούμαστε, αυτό το χρηστώνυμο χρησιμοποιείται ήδη." invalid: "Αυτό το χρηστώνυμο δεν είναι έγκυρο. Θα πρέπει να αποτελείται μόνο από αριθμούς και γράμματα" change_email: @@ -639,7 +647,7 @@ el: instructions: "Οι εικόνες στο φόντο θα κεντραρίζονται και το προκαθορισμένο πλάτος τους είναι 590px." email: title: "Διεύθυνση ηλεκτρονικού ταχυδρομείου" - instructions: "Να μην εμφανίζεται ποτέ δημόσια" + instructions: "δεν εμφανίζεται ποτέ δημόσια" ok: "Για επιβεβαίωση θα σου στείλουμε ένα μήνυμα ηλεκτρονικού ταχυδρομείου" invalid: "Παρακαλώ δώσε μία έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου" authenticated: "Η διεύθυνση ηλεκτρονικού ταχυδρομείου σου ταυτοποιήθηκε από τον πάροχο {{provider}}" @@ -676,7 +684,7 @@ el: last_seen: "Ειδώθηκε" created: "Μέλος από" log_out: "Αποσύνδεση" - location: "Διεύθυνση" + location: "Τοποθεσία" card_badge: title: "Παράσημο καρτέλας χρήστη" website: "Ιστοσελίδα" @@ -684,12 +692,12 @@ el: like_notification_frequency: title: "Ειδοποίησε με όταν έχω \"μου αρέσει\"" always: "Πάντα" - first_time_and_daily: "Πρώτη φορά που η δημοσίευση έχει \"μου αρέσει\" και καθημερινά" - first_time: "Πρώτη φορά που η δημοσίευση έχει \"μου αρέσει\"" + first_time_and_daily: "Πρώτη φορά που η ανάρτηση έχει \"μου αρέσει\" και καθημερινά" + first_time: "Πρώτη φορά που η ανάρτηση έχει \"μου αρέσει\"" never: "Ποτέ" email_previous_replies: title: "Συμπερίλαβε προηγούμενες απαντήσεις στο κάτω μέρος των ηλεκτρονικών μηνυμάτων" - unless_emailed: "Εάν δεν έχει σταλεί προηγουμένως" + unless_emailed: "εάν δεν έχει σταλεί προηγουμένως" always: "πάντα" never: "ποτέ" email_digests: @@ -697,11 +705,11 @@ el: every_30_minutes: "κάθε 30 λεπτά" every_hour: "ωριαία" daily: "καθημερινά" - every_three_days: "Κάθε τρεις μέρες" + every_three_days: "κάθε τρεις μέρες" weekly: "εβδομαδιαία" - every_two_weeks: "Κάθε τρεις μέρες" + every_two_weeks: "κάθε δύο εβδομάδες" include_tl0_in_digests: "Συμπεριλάμβανε περιεχόμενο από νέους χρήστες σε περιληπτικά μηνύματα" - email_in_reply_to: "Συμπερίλαβε ένα απόσπασμα της απάντησης για να το δημοσιεύσεις σέ ηλεκτρονικό μήνυμα" + email_in_reply_to: "Συμπεριέλαβε ένα απόσπασμα της απάντησης για να το στείλεις με ηλεκτρονικό μήνυμα" email_direct: "Στείλε μου ένα μήνυμα ηλεκτρονικού ταχυδρομείου όταν κάποιος παραθέσει ανάρτησή μου, απαντήσει σε ανάρτησή μου, αναφέρει το @username μου ή με προσκαλεί σε ένα νήμα." email_private_messages: "Στείλε μου ένα μήνυμα ηλεκτρονικού ταχυδρομείου όταν κάποιος που στείλει προσωπικό μήνυμα." email_always: "Στείλε μου ειδοποιήσεις μέσω ηλεκτρονικού μηνύματος όταν είμαι ενεργός στην ιστοσελίδα" @@ -711,10 +719,10 @@ el: label: "Τα νήματα να θεωρούνται νέα όταν" not_viewed: "Δεν τα έχω δει αυτά ακόμη" last_here: "δημιουργήθηκαν από την τελευταία επίσκεψή μου" - after_1_day: "δημιουργήθηκε την τελευταία ημέρα" - after_2_days: "δημιουργήθηκε τις 2 τελευταίες ημέρες" + after_1_day: "δημιουργήθηκαν την τελευταία ημέρα" + after_2_days: "δημιουργήθηκαν τις 2 τελευταίες ημέρες" after_1_week: "δημιουργήθηκαν την τελευταία εβδομάδα" - after_2_weeks: "δημιουργήθηκε τις 2 τελευταίες εβδομάδες" + after_2_weeks: "δημιουργήθηκαν τις 2 τελευταίες εβδομάδες" auto_track_topics: "Τα νήματα που βλέπω να παρακολουθούνται αυτόματα " auto_track_options: never: "ποτέ" @@ -726,7 +734,7 @@ el: after_4_minutes: "μετά από 4 λεπτά" after_5_minutes: "μετά από 5 λεπτά" after_10_minutes: "μετά από 10 λεπτά" - notification_level_when_replying: "Όταν δημοσιοποιώ ένα νήμα, τοποθέτησε αυτό το νήμα σε" + notification_level_when_replying: "Όταν αναρτώ σε ένα νήμα, τοποθέτησε αυτό το νήμα σε" invited: search: "γράψε για να αναζητήσεις προσκλήσεις..." title: "Προσκλήσεις" @@ -735,7 +743,7 @@ el: none: "Δεν υπάρχουν προσκλήσεις σε εκκρεμότητα να σου δείξω." truncated: one: "Δείχνοντας την πρώτη πρόσκληση." - other: "Δείχνοντας την πρώτη {{count}} προσκλήσεις." + other: "Προβάλονται οι πρώτες {{count}} προσκλήσεις." redeemed: "Προσκλήσεις που έγιναν αποδεκτές" redeemed_tab: "Εξοφλήθηκε" redeemed_tab_with_count: "Εξοφλήθηκε ({{count}})" @@ -744,10 +752,13 @@ el: pending_tab: "Σε εκκρεμότητα" pending_tab_with_count: "Σε εκκρεμότητα ({{count}})" topics_entered: "Νήματα που ειδώθηκαν" - posts_read_count: "Διαβασμένες αναρτήσεις" + posts_read_count: "Διαβασμένες Αναρτήσεις" expired: "Αυτή η πρόσκληση έχει λήξει." rescind: "Αναίρεση πρόσκλησης" rescinded: "Η πρόσκληση αναιρέθηκε" + rescind_all: "Αναίρεσε όλες τις προσκλήσεις" + rescinded_all: "Όλες οι προσκλήσεις αναιρέθηκαν!" + rescind_all_confirm: "Θέλετε σίγουρα να αναιρέσετε όλες τις προσκλήσεις;" reinvite: "Επαναποστολή πρόσκλησης" reinvite_all: "Αποστολή ξανά όλων των προσκλήσεων" reinvite_all_confirm: "Σίγουρα θέλετε να στείλετε ξανά όλες τις προσκλήσεις;" @@ -757,9 +768,9 @@ el: days_visited: "Μέρες επίσκεψης" account_age_days: "Ηλικία λογαριασμού σε μέρες" create: "Αποστολή πρόσκλησης" - generate_link: "Αντιγραφή Πρόσκληση Σύνδεσμος" - link_generated: "Η πρόσκληση του συνδέσμου έγινε επιτυχώς!" - valid_for: "Η πρόσκληση συνδέσμου είναι έγκυρη μόνο για τη διεύθυνση ηλεκτρονικού μηνύματος: %{email}" + generate_link: "Αντιγραφή Συνδέσμου Πρόσκλησης" + link_generated: "Η δημιουργία του συνδέσμου πρόσκλησης έγινε επιτυχώς!" + valid_for: "Ο σύνδεσμος πρόσκλησης είναι έγκυρος μόνο για αυτή τη διεύθυνση ηλεκτρονικού μηνύματος: %{email}" bulk_invite: none: "Δεν έχετε προσκαλέσει κανέναν ακόμα. Στείλτε ατομικές προσκλήσεις, ή καλέστε πολλά άτομα με τη μία μεταφορτώνοντας ένα αρχείο CSV ." text: "Ομαδική πρόσκληση από αρχείο" @@ -782,28 +793,28 @@ el: other: "τα θέματα δημιουργήθηκαν" post_count: one: "οι δημοσιεύσεις" - other: "οι δημοσιεύσεις δημιουργήθηκαν" + other: "αναρτήσεις που δημιουργήθηκαν" likes_given: one: " δεδομένο" - other: " δεδομένο" + other: " δόθηκαν" likes_received: one: " ελήφθη" - other: " ελήφθη" + other: " ελήφθησαν" days_visited: one: "ημέρα που επισκέφτηκες" other: "ημέρες που επισκέφτηκες" posts_read: one: "η δημοσίευση διαβάστηκε" - other: "οι δημοσιεύσεις διαβάστηκαν" + other: "αναρτήσεις που διαβάστηκαν" bookmark_count: one: "σελιδοδείκτης" other: "σελιδοδείκτες" top_replies: "Κορυφαίες απαντήσεις" no_replies: "Καμία απάντηση ακόμα." more_replies: "Περισσότερες Απαντήσεις" - top_topics: "Κορυφαία Θέματα" - no_topics: "Κανένα θέμα ακόμα." - more_topics: "Περισσότερα Θέματα" + top_topics: "Κορυφαία Νήματα" + no_topics: "Κανένα νήμα ακόμα." + more_topics: "Περισσότερα Νήματα" top_badges: "Κορυφαία Παράσημα" no_badges: "Κανένα παράσημο ακόμα." more_badges: "Περισσότερα Παράσημα" @@ -828,11 +839,11 @@ el: stream: posted_by: "Αναρτήθηκε από" sent_by: "Στάλθηκε από" - private_message: "Μήνυμα" + private_message: "μήνυμα" the_topic: "το νήμα" loading: "Φόρτωση... " errors: - prev_page: "στο ανέβασμα" + prev_page: "κατά το φόρτωμα" reasons: network: "Σφάλμα δικτύου" server: "Σφάλμα διακομιστή" @@ -858,12 +869,12 @@ el: enabled: "Αυτή η ιστοσελίδα είναι σε λειτουργία μόνο ανάγνωσης. Παρακαλώ συνέχισε να κάνεις περιήγηση, όμως το να απαντάς να πατάς \"μου αρέσει\" και κάποιες άλλες λειτουργίες δεν είναι διαθέσιμες τώρα." login_disabled: "Η δυνατότητα σύνδεσης έχει απενεργοποιηθεί για όσο το φόρουμ είναι σε κατάσταση μόνο ανάγνωσης." logout_disabled: "Η αποσύνδεση δεν είναι διαθέσιμη ενώ η ιστοσελίδα είναι σε λειτουργία μόνο ανάγνωσης." - too_few_topics_and_posts_notice: "Ας αρχίσουμε αυτή την συζήτηση! Υπάρχουν %{currentTopics} / %{requiredTopics} νήματα και %{currentPosts} / %{requiredPosts} δημοσιεύεις. Οι νέοι επισκέπτες χρειάζονται κάποιες συνομιλίες για να τις διαβάσουν και να απαντήσουν σε αυτές." - too_few_topics_notice: "Ας αρχίσουμε αυτή την συζήτηση! Υπάρχουν %{currentTopics} / %{requiredTopics} νήματα και %{currentPosts} / %{requiredPosts} δημοσιεύεις. Οι νέοι επισκέπτες χρειάζονται κάποιες συνομιλίες για να τις διαβάσουν και να απαντήσουν σε αυτές." - too_few_posts_notice: "Ας ξεκινήσουμε αυτή τη συζήτηση! Υπάρχουν προσωρινά %{currentPosts} / %{requiredPosts} δημοσιεύσεις. Οι καινούριοι επισκέπτες χρειάζοντε να διαβάσουν και να απαντήσουν σε μερικές συζητήσεις." + too_few_topics_and_posts_notice: "Ας αρχίσουμε αυτή την συζήτηση! Υπάρχουν %{currentTopics} / %{requiredTopics} νήματα και %{currentPosts} / %{requiredPosts} αναρτήσεις. Οι νέοι επισκέπτες χρειάζονται κάποιες συνομιλίες για να τις διαβάσουν και να απαντήσουν σε αυτές." + too_few_topics_notice: "Ας αρχίσουμε αυτή την συζήτηση! Υπάρχουν %{currentTopics} / %{requiredTopics} νήματα. Οι νέοι επισκέπτες χρειάζονται κάποιες συνομιλίες για να τις διαβάσουν και να απαντήσουν σε αυτές." + too_few_posts_notice: "Ας ξεκινήσουμε αυτή τη συζήτηση! Υπάρχουν %{currentPosts} / %{requiredPosts} αναρτήσεις. Οι νέοι επισκέπτες χρειάζονται κάποιες συνομιλίες για να τις διαβάσουν και να απαντήσουν σε αυτές." logs_error_rate_notice: reached: "%{relativeAge}%{rate} έχει φτάσει το ορισμένο όριο της ιστοσελίδας των %{siteSettingRate}." - exceeded: "&{relativeAge} - %{rate} υπερβαίνει το όριο των ρυθμίσεων της ιστοσελίδας %{siteSettingRate}. " + exceeded: "%{relativeAge} - %{rate} υπερβαίνει το όριο των ρυθμίσεων της ιστοσελίδας %{siteSettingRate}. " rate: one: "1 σφάλματα/%{duration}" other: "%{count} σφάλματα/%{duration}" @@ -889,14 +900,14 @@ el: sign_up: "Εγγραφή" hide_session: "Υπενθύμιση αύριο" hide_forever: "όχι ευχαριστώ" - hidden_for_session: "Εντάξει, θα σε ρωτήσω αύριο. Μπορείς πάντα να χρησιμοποιείς τη \"Σύνδεση\" και για να δημιουργήσεις ένα λογαριασμό." - intro: "Γεια σου! :heart_eyes: Φαίνεται να απολαμβάνεις τη συζήτηση, όμως δεν έχεις εγγραφεί σε ένα λογαριασμό." - value_prop: "Όταν δημιουργείς έναν λογαριασμό, θυμόμαστε ακριβώς τι έχεις διαβάσει, έτσι ώστε πάντα να επιστρέφεις όπου είχες μείνει. Επίσης, λαμβάνεις ειδοποιήσεις, εδώ και μέσω ηλεκτρονικού μηνύματος, κάθε φορά που δημιουργούνται καινούριες δημοσιεύσεις. Και μπορείς να πατήσεις \"μου αρέσει\" στις δημοσιεύσεις για να μοιραστείς την αγάπη. :heartbeat:" + hidden_for_session: "Εντάξει, θα σε ρωτήσω αύριο. Μπορείς πάντα να χρησιμοποιείς τη \"Σύνδεση\" για να δημιουργήσεις ένα λογαριασμό." + intro: "Γεια σου! :heart_eyes: Φαίνεται να απολαμβάνεις τη συζήτηση, όμως δεν έχεις δημιουργήσει ακόμα δικό σου λογαριασμό." + value_prop: "Όταν δημιουργείς έναν λογαριασμό, θυμόμαστε ακριβώς τι έχεις διαβάσει, έτσι ώστε πάντα να επιστρέφεις όπου είχες μείνει. Επίσης, λαμβάνεις ειδοποιήσεις, εδώ και μέσω ηλεκτρονικού μηνύματος, κάθε φορά που δημιουργούνται καινούριες αναρτήσεις. Επίσης μπορείς να πατήσεις \"μου αρέσει\" στις αναρτήσεις για να μοιραστείς την αγάπη. :heartbeat:" summary: - enabled_description: "Βλέπεις μια περίληψη αυτού του νήματος: οι πιο ενδιαφέρουσες αναρτήσεις όπως αυτές καθορίστηκαν από την κοινότητα." + enabled_description: "Βλέπεις μια περίληψη αυτού του νήματος: οι πιο ενδιαφέρουσες αναρτήσεις, όπως αυτές καθορίστηκαν από την κοινότητα." description: "Υπάρχουν {{replyCount}} απαντήσεις." description_time: "Υπάρχουν {{replyCount}} απαντήσεις με εκτιμώμενο χρόνο ανάγνωσης {{readingTime}} λεπτών." - enable: 'Κάνε μια σύνοψη αυτού του νήματος' + enable: 'Σύνοψη αυτού του νήματος' disable: 'Εμφάνιση όλων των αναρτήσεων' deleted_filter: enabled_description: "Αυτό το νήμα περιέχει σβησμένες αναρτήσεις, οι οποίες αποκρύπτονται." @@ -916,7 +927,7 @@ el: trust_level: 'Επίπεδο εμπιστοσύνης' search_hint: 'χρηστώνυμο, διεύθυνση ηλεκτρονικού ταχυδρομείου ή IP διεύθυνση' create_account: - disclaimer: "Κάνοντας εγγραφή συμφωνείς με την πολιτική προστασίας προσωπικών δεδομένων και με τους όρους υπηρεσίας." + disclaimer: "Κάνοντας εγγραφή συμφωνείς με την πολιτική προστασίας προσωπικών δεδομένων και με τους όρους χρήσης." title: "Δημιουργία νέου λογαριασμού" failed: "Κάτι πήγε στραβά. Ίσως αυτή η διεύθυνση ηλεκτρονικού ταχυδρομείου να είναι ήδη δηλωμένη. Δοκίμασε το σύνδεσμο «ξέχασα τον κωδικό μου»." forgot_password: @@ -943,7 +954,7 @@ el: rate_limit: "Παρακαλώ περιμένετε προτού προσπαθήσετε ξανά να συνδεθείτε." blank_username_or_password: "Παρακαλώ δώσε την διεύθυνση ηλεκτρονικού ταχυδρομείου σου ή το χρηστώνυμό σου και τον κωδικό πρόσβασής σου." reset_password: 'Επαναφορά κωδικού πρόσβασης' - logging_in: "Συνδέεσε..." + logging_in: "Σύνδεση..." or: "Ή" authenticating: "Ταυτοποιώ..." awaiting_activation: "Ο λογαριασμός σας αναμένει ενεργοποίηση, χρησιμοποιήστε τον σύνδεσμο ξεχασμένου συνθηματικού για δημιουργία νέου email ενεργοποίησης." @@ -985,9 +996,9 @@ el: message: "Ταυτοποίηση μέσω του Github (βεβαιώσου πως δεν έχει ενεργοποιηθεί το μπλοκάρισμα των αναδυόμενων παραθύρων)" invites: accept_title: "Πρόσκληση" - welcome_to: "Καλώς ήλθατε στο %{site_name}!" + welcome_to: "Καλώς ήλθατε στην %{site_name}!" invited_by: "Προσκληθήκατε από:" - social_login_available: "Θα μπορούσατε επίσεις να συνδεθείτε μέσω οποιοδήποτε μέσoυ κοινωνικής δικτύωσης χρησημοποιώντας αυτή την διεύθυνση ηλεκτρονικού ταχυδρομείου." + social_login_available: "Μπορείτε επίσης να συνδεθείτε μέσω λογαριασμών κοινωνικής δικτύωσης χρησημοποιώντας αυτή την διεύθυνση ηλεκτρονικού ταχυδρομείου." your_email: "Η διεύθυνση ηλεκτρονικού ταχυδρομείου του λογαριασμού σας είναι %{email}." accept_invite: "Αποδοχή πρόσκλησης" success: "Ο λογαριασμός σας έχει δημιουργηθεί και έχετε συνδεθεί. " @@ -1000,16 +1011,35 @@ el: apple_international: "Apple/Διεθνής" google: "Google" twitter: "Twitter" - emoji_one: "Emoji ένα" - win10: "Κερδισαι10" + emoji_one: "Emoji One" + win10: "Win10" + google_classic: "Google Classic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Μόνο Κατηγορίες" - categories_with_featured_topics: "Κατηγορίες με Προτεινόμενα Θέματα" - categories_and_latest_topics: "Κατηγορίες και Τελευταία Θέματα" + categories_with_featured_topics: "Κατηγορίες με Προτεινόμενα Νήματα" + categories_and_latest_topics: "Κατηγορίες και Τελευταία Νήματα" shortcut_modifier_key: shift: ' Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Αναζήτηση για emoji + people: Άνθρωποι + nature: Φύση + food: Τροφή + activity: Δραστηριότητα + travel: Ταξίδια + objects: Αντικείμενα + celebration: Γιορτή + custom: Προσαρμοσμένα emojis + recent: Πρόσφατα χρησιμοποιημένα + default_tone: Χωρίς απόχρωση επιδερμίδας + light_tone: Λευκή απόχρωση επιδερμίδας + medium_light_tone: Ανοιχτόχρωμη απόχρωση επιδερμίδας + medium_tone: Καυκάσια απόχρωση επιδερμίδας + medium_dark_tone: Μελαμψή απόχρωση επιδερμίδας + dark_tone: Σκούρη απόχρωση επιδερμίδας composer: emoji: "Emoji :)" more_emoji: "περισσότερα..." @@ -1018,8 +1048,8 @@ el: unlist: "μη καταχωρημένος" blockquote_text: "Blockquote" add_warning: "Αυτή είναι μια επίσημη προειδοποίηση." - toggle_whisper: "Ανταλλαγή ψιθύρων" - toggle_unlisted: "Μεταβάλλω ακαταχώριστα" + toggle_whisper: "Εναλλαγή ψιθύρων" + toggle_unlisted: "Εναλλαγή ακαταχώρητων" posting_not_on_topic: "Σε ποιο νήμα θέλεις να απαντήσεις;" saving_draft_tip: "αποθήκευση..." saved_draft_tip: "αποθηκεύτηκε" @@ -1028,7 +1058,7 @@ el: drafts_offline: "τοπικά πρόχειρα" group_mentioned: one: "Αναφέροντας {{group}}, πρόκειται να λάβει ειδοποίηση 1 άτομο - είσαι βέβαιος;" - other: "Αναφέροντας το {{group}}, πρόκειται να ειδοποίησεις {{count}} άνθρωποι - είσαι σίγουρος;" + other: "Αναφέροντας το {{group}}, πρόκειται να ειδοποίησεις {{count}} μέλη - είσαι σίγουρος;" cannot_see_mention: category: "Ανέφερες το {{username}} αλλά δε θα λάβουν ειδοποίηση, επειδή δεν έχουν πρόσβαση σε αυτή την κατηγορία. Θα πρέπει να τους προσθέσεις σε μια ομάδα που έχει πρόσβαση σε αυτή την κατηγορία." private: "Ανέφερες {{username}} αλλά δεν θα λάβουν ειδοποίηση, επειδή δεν μπορούν να δουν αυτό το προσωπικό μήνυμα. Πρέπει να τους προσκαλέσεις σε αυτό το ιδιωτικό μήνυμα." @@ -1039,14 +1069,14 @@ el: title_too_long: "Ο τίτλος δεν μπορεί να έχει πάνω από {{max}} χαρακτήρες" post_missing: "Μία ανάρτηση δεν μπορεί να είναι κενή" post_length: "Κάθε ανάρτηση πρέπει να περιέχει τουλάχιστον {{min}} χαρακτήρες" - try_like: 'Έχεις δοκιμάσει το κουμπί ;' + try_like: 'Έχεις δοκιμάσει το κουμπί ;' category_missing: "Πρέπει να διαλέξεις μια κατηγορία" save_edit: "Αποθήκευση Επεξεργασίας" reply_original: "Απάντηση στο αρχικό νήμα" reply_here: "Απάντησε εδώ" reply: "Απάντηση" cancel: "Ακύρωση" - create_topic: "Δημιουργία Θέματος" + create_topic: "Δημιουργία Νήματος" create_pm: "Μήνυμα" title: "Ή πάτα Ctrl+Enter" users_placeholder: "Προσθήκη χρήστη" @@ -1064,10 +1094,10 @@ el: show_preview: 'εμφάνιση της προεπισκόπησης »' hide_preview: '« απόκρυψη της προεπισκόπησης' quote_post_title: "Παράθεση ολόκληρης την ανάρτησης" - bold_label: "Β" + bold_label: "B" bold_title: "Έντονα" bold_text: "Έντονη γραφή" - italic_label: "Ι" + italic_label: "I" italic_title: "Έμφαση" italic_text: "κείμενο σε έμφαση" link_title: "Σύνδεσμος" @@ -1085,7 +1115,7 @@ el: olist_title: "Αριθμημένη λίστα" ulist_title: "Κουκίδες" list_item: "Στοιχείο Λίστας" - heading_label: "Η" + heading_label: "H" heading_title: "Επικεφαλίδα" heading_text: "Επικεφαλίδα" hr_title: "Οριζόντια γραμμή" @@ -1097,7 +1127,7 @@ el: yourself_confirm: title: "Ξεχάσατε να προσθέσετε αποδέκτες;" body: "Αυτή τη στιγμή το ηλεκτρονικό μήνυμα στέλνεται μόνο σε εσάς!" - admin_options_title: "Προαιρετικές ρυθμίσεις προσωπικού για αυτό το θέμα" + admin_options_title: "Προαιρετικές ρυθμίσεις προσωπικού για αυτό το νήμα" notifications: title: "ειδοποιήσεις για αναφορές στο @name, απαντήσεις στις αναρτήσεις σου και στα νήματά σου, προσωπικά μηνύματα, κλπ." none: "Αυτή τη στιγμή δεν είναι δυνατόν να φορτωθούν οι ειδοποιήσεις." @@ -1105,7 +1135,7 @@ el: more: "Εμφάνιση παλιότερων ειδοποιήσεων" total_flagged: "σύνολο επισημασμένων αναρτήσεων" mentioned: "

    {{username}} {{description}}

    " - group_mentioned: "

    {{username}} {{description}}

    " + group_mentioned: "

    {{username}} {{description}}

    " quoted: "

    {{username}} {{description}}

    " replied: "

    {{username}} {{description}}

    " posted: "

    {{username}} {{description}}

    " @@ -1114,7 +1144,7 @@ el: liked_2: "

    {{username}}, {{username2}}{{description}}

    " liked_many: one: "

    {{username}}, {{username2}} και 1 άλλος {{description}}

    " - other: "

    {{username}}, {{username2}} και {{count}}άλλους {{description}}

    " + other: "

    {{username}}, {{username2}} και {{count}} ακόμα {{description}}

    " private_message: "

    {{username}} {{description}}

    " invited_to_private_message: "

    {{username}} {{description}}

    " invited_to_topic: "

    {{username}} {{description}}

    " @@ -1123,7 +1153,7 @@ el: linked: "

    {{username}} {{description}}

    " granted_badge: "

    Κέρδισες το παράσημο '{{description}}'

    " topic_reminder: "

    {{username}} {{description}}

    " - watching_first_post: "

    Νέο θέμα {{description}}

    " + watching_first_post: "

    Νέο νήμα {{description}}

    " group_message_summary: one: "

    {{count}} μηνύματα στα {{group_name}} εισερχόμενά σου

    " other: "

    {{count}} μηνύματα στα {{group_name}} εισερχόμενά σου

    " @@ -1131,67 +1161,67 @@ el: mentioned: "Αναφέρθηκε από" quoted: "Απόσπασμα από " replied: "Απαντήθηκε" - posted: "Δημοσίευση από" - edited: "Επεξεργασία της δημοσίευσης από" - liked: "Η δημοσίευσή σου έχει \"μου αρέσει\"" + posted: "Ανάρτηση από" + edited: "Επεξεργασία της ανάρτησης από" + liked: "Η ανάρτησή σου έχει \"μου αρέσει\"" private_message: "Προσωπικό μήνυμα από" invited_to_private_message: "Πρόσκληση σε προσωπικό μήνυμα από" - invited_to_topic: "Πρόσκληση σε θέμα από" + invited_to_topic: "Προσκλήθηκε στο νήμα από" invitee_accepted: "Η πρόσκληση έγινε δεκτή από" - moved_post: "Η δημοσίευση σου μετακινήθηκε από" - linked: "Σύνδεση στη δημοσίευσή σου" - granted_badge: "Χορηγήθηκε σήμα" + moved_post: "Η ανάρτησή σου μετακινήθηκε από" + linked: "Σύνδεση στη ανάρτησή σου" + granted_badge: "Χορηγήθηκε παράσημο" group_message_summary: "Μηνύματα στα εισερχόμενα ομάδας" popup: - mentioned: 'Ο/Η {{username}} σε ανέφερε στο "{{topic}}" - {{site_title}}' - group_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}}' + mentioned: '{{username}} σε ανέφερε στο "{{topic}}" - {{site_title}}' + group_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: "Προσθήκη εικόνας ή αρχείου" from_my_computer: "Από τη συσκευή μου" from_the_web: "Από το διαδίκτυο" remote_tip: "υπερσύνδεσμος προς μια εικόνα" - remote_tip_with_attachments: "σύνδεσμος σε μια εικόνα ή ένα φάκελο {{authorized_extensions}}" + remote_tip_with_attachments: "σύνδεσμος σε εικόνα ή φάκελο {{authorized_extensions}}" local_tip: "επιλογή εικόνων από τη συσκευή σου" local_tip_with_attachments: "επίλεξε εικόνες ή φακέλους από τη συσκευή σου {{authorized_extensions}}" hint: "(μπορείς να τα σύρεις με το ποντίκι στην περιοχή για να τα ανεβάσεις)" hint_for_supported_browsers: "μπορείς επίσης να μεταφέρεις ή να επικόλλησεις εικόνες από τον συντάκτη" uploading: "Ανεβαίνει" - select_file: "Επιλογή Φακέλου" + select_file: "Επιλογή Αρχείου" image_link: "υπερσύνδεσμος στον οποίο θα δείχνει η εικόνα σου" search: sort_by: "Ταξινόμηση κατά" - relevance: "συνάφεια" - latest_post: "Τελευταίες Δημοσιεύσεις" + relevance: "Συνάφεια" + latest_post: "Τελευταίες Αναρτήσεις" latest_topic: "Πρόσφατο Νήμα" most_viewed: "Περισσότερες εμφανίσεις" most_liked: "Περισσότερα \"μου αρέσει\"" select_all: "Επιλογή όλων" clear_all: "Καθαρισμός όλων" - too_short: "Ο όρος στην αναζήτησή σου είναι πολύ μικρός." + too_short: "Ο όρος αναζήτησής σου είναι πολύ μικρός." result_count: - one: "1 αποτέλεσμα για \"{{term}}\"" - other: "{{count}} αποτελέσματα για \"{{term}}\"" + one: "1 αποτέλεσμα για {{term}}" + other: "{{count}} αποτελέσματα για {{term}}" title: "ψάξε σε νήματα, αναρτήσεις, χρήστες ή κατηγορίες" no_results: "Δε βρέθηκε κάτι." - no_more_results: "Δε βρέθηκαν αποτελέσματα" + no_more_results: "Δε βρέθηκαν άλλα αποτελέσματα" searching: "Ψάχνω ..." post_format: "#{{post_number}} από {{username}}" results_page: "Αποτελέσματα Αναζήτησης" context: user: "Ψάξε στις αναρτήσεις του χρήστη @{{username}}" - category: "Αναζήτηση στην #{{category}} κατηγορία" + category: "Αναζήτηση στην κατηγορία #{{category}} " topic: "Ψάξε σε αυτό το νήμα" private_messages: "Αναζήτηση στα μηνύματα" advanced: title: Προηγμένη Αναζήτηση posted_by: - label: Δημοσιεύθηκε από + label: Αναρτήθηκε από in_category: label: Σε κατηγορία in_group: @@ -1199,16 +1229,16 @@ el: with_badge: label: Με σήμα with_tags: - label: Με ετικέτες + label: Με Ετικέτες filters: - label: Να επιστρέψεις μόνο νήματα/δημοσιεύσεις που... + label: Να επιστρέψεις μόνο νήματα/αναρτήσεις που... likes: Μου αρέσει - posted: Δημοσίευσα σε + posted: ανάρτησα σε watching: Παρακολουθώ - tracking: Παρακολουθώ + tracking: Ακολουθώ private: είναι στα μηνύματα μου bookmarks: Έχω βάλει σελιδοδείκτη - first: είναι η πρώτη πρώτη δημοσίευση + first: είναι η πρώτη πρώτη ανάρτηση pinned: καρφιτσώθηκαν unpinned: δεν καρφιτσώθηκαν seen: Διάβασα @@ -1216,17 +1246,17 @@ el: wiki: ' είναι βίκι' all_tags: Πειρέχει όλες τις ετικέτες statuses: - label: όπου θέματα + label: Όπου τα νήματα open: είναι ανοιχτά closed: είναι κλειστά archived: είναι αρχειοθετημένα noreplies: έχουν μηδέν απαντήσεις - single_user: περιέχει ένα μόνο χρήστη + single_user: περιέχουν ένα μόνο χρήστη post: count: - label: Ελάχιστη καταμέτρηση δημοσιεύσεων + label: Ελάχιστος αριθμός αναρτήσεων time: - label: Δημοσιεύθηκε + label: Αναρτήθηκε before: πριν after: μετά hamburger_menu: "πήγαινε σε άλλη καταχώρηση νήματος ή κατηγορία" @@ -1239,33 +1269,36 @@ el: bulk: select_all: "Επιλογή όλων" clear_all: "Καθαρισμός όλων" - unlist_topics: "Μη καταχωρημένα θέματα" - reset_read: "Επαναφορά διαβασμένων" - delete: "Διαγραφή νημάτων" + unlist_topics: "Απέκρυψε Νήματα" + relist_topics: "Εμφάνισε Νήματα" + reset_read: "Μηδενισμός Διαβασμένων" + delete: "Διαγραφή Νημάτων" dismiss: "Απόρριψη" dismiss_read: "Απόρριψη όλων των μη αναγνωσμένων" dismiss_button: "Απόρριψη..." - dismiss_tooltip: "Απόρριψη μόνο των καινούριων δημοσιεύσεων ή διακοπή των νημάτων που παρακολουθούνται" - also_dismiss_topics: "Διακοπή παρακολούθησης αυτών των νημάτων ώστε να μην εμφανιστούν ξανά ως μη αναγνωσμένα σε εμένα" + dismiss_tooltip: "Απόρριψη μόνο των νέων αναρτήσεων ή διακοπή των νημάτων που ακολουθούνται" + also_dismiss_topics: "Διακοπή ακολούθησης αυτών των νημάτων ώστε να μην εμφανιστούν ξανά ως μη αναγνωσμένα σε εμένα" dismiss_new: "Αγνόησε τα νέα" - toggle: "μαζική επιλογή νημάτων" + toggle: "εναλλαγή μαζικής επιλογής νημάτων" actions: "Μαζικές ενέργειες" - close_topics: "Κλείσιμο νημάτων" - archive_topics: "Αρχειοθέτηση νημάτων" + change_category: "Θέσε Κατηγορία" + close_topics: "Κλείσιμο Νημάτων" + archive_topics: "Αρχειοθέτηση Νημάτων" + notification_level: "Ειδοποιήσεις" choose_new_category: "Διάλεξε τη νέα κατηγορία για τα νήματα:" selected: one: "Έχεις διαλέξει 1 νήμα." other: "Έχεις διαλέξει {{count}} νήματα." - change_tags: "Αντικατάσταση ετικετών " - append_tags: "Προσάρτηση ετικετών " - choose_new_tags: "Επίλεξε καινούριες ετικέτες από αυτά τα θέματα:" + change_tags: "Αντικατάσταση Ετικετών " + append_tags: "Προσάρτηση Ετικετών " + choose_new_tags: "Επίλεξε καινούριες ετικέτες για αυτά τα νήματα:" choose_append_tags: "Επιλογή νέων ετικετών για την προσάρτηση τους σε αυτά τα νήματα" - changed_tags: "Οι ετικέτες αυτών των θεμάτων έχουν αλλάξει." + changed_tags: "Οι ετικέτες αυτών των νημάτων έχουν αλλάξει." none: unread: "Έχεις διαβάσει όλα τα νήματα." new: "Δεν υπάρχουν νέα νήματα." read: "Δεν έχεις διαβάσει κανένα νήμα ακόμη." - posted: "Δεν έχεις αναρτήσει τίποτε ακόμη." + posted: "Δεν έχεις αναρτήσει σε κάποιο νήμα ακόμη." latest: "Δεν υπάρχουν νεότερα νήματα. Αυτό είναι λυπηρό." hot: "Δεν υπάρχουν δημοφιλή νήματα." bookmarks: "Δεν έχεις βάλει σελιδοδείκτη σε κανένα νήμα." @@ -1273,18 +1306,18 @@ el: top: "Δεν υπάρχουν κορυφαία νήματα." search: "Δεν υπάρχουν αποτελέσματα σε αυτή την αναζήτηση." educate: - new: '

    Τα νέα σου θέματα εμφανίζονται εδώ.

    Από προεπιλογή, τα θέματα θεωρούνται καινούρια και θα δείχνουν ένα καινούριο δείκτη εαν έχουν δημιουργηθεί τις τελευταίες 2 μέρες.

    Επισκέψου τις προτιμήσεις σου για να το αλλάξεις αυτό.

    ' - unread: '

    Τα αδιάβαστα νήματά σου εμφανίζονται εδώ.

    Από προεπιλογή, τα νήματα θεωρούνται αδιάβαστα και θα δείχνουν αδιάβαστα 1 έαν:

    • Δημιούργησες το νήμα
    • Απάντησεσ στο νήμα
    • Διάβασεσ το νήμα για περισσότερα από 4 λεπτά

    Ή εάν έχεις ορίσει το νήμα να παρακολουθήται μέσω έλεγχο ειδοποιήσης στο κάτω μέρος του κάθε νήματος.

    Επισκέψου τις προτιμήσεις σου για να το αλλάξεις αυτό.

    ' + new: '

    Τα νέα σου νήματα εμφανίζονται εδώ.

    Από προεπιλογή, τα νήματα συζητήσεων θεωρούνται καινούρια και θα δείχνουν ένα καινούριο δείκτη εαν έχουν δημιουργηθεί τις τελευταίες 2 μέρες.

    Επισκέψου τις προτιμήσεις σου για να το αλλάξεις αυτό.

    ' + unread: '

    Τα αδιάβαστα νήματά σου εμφανίζονται εδώ.

    Από προεπιλογή, τα νήματα συζητήσεων θεωρούνται αδιάβαστα και θα δείχνουν αδιάβαστα 1 έαν:

    • Δημιούργησες το νήμα
    • Απάντησες στο νήμα
    • Διάβασες το νήμα για περισσότερα από 4 λεπτά

    Ή εάν έχεις ορίσει το νήμα να παρακολουθείται μέσω της επιλογής ειδοποιήσεων στο κάτω μέρος του κάθε νήματος.

    Επισκέψου τις προτιμήσεις σου για να το αλλάξεις αυτό.

    ' bottom: - latest: "Αυτά ήταν τα πρόσφατα νήματα." - hot: "Αυτά ήταν όλα τα δημοφιλή νήματα." - posted: "Αυτά ήταν όλα τα νήματα." - read: "Αυτά ήταν όλα τα διαβασμένα νήματα." - new: "Αυτά ήταν όλα τα νέα νήματα." - unread: "Αυτά ήταν όλα τα αδιάβαστα νήματα." - category: "Αυτά ήταν όλα τα νήματα στην κατηγορία {{category}}." - top: "Αυτά ήταν όλα τα κορυφαία νήματα." - bookmarks: "Αυτά ήταν όλα τα νήματα με σελιδοδείκτη." + latest: "Δεν υπάρχουν άλλα πρόσφατα νήματα." + hot: "Δεν υπάρχουν άλλα δημοφιλή νήματα." + posted: "Δεν υπάρχουν άλλα νήματα." + read: "Δεν υπάρχουν άλλα διαβασμένα νήματα." + new: "Δεν υπάρχουν άλλα νέα νήματα." + unread: "Δεν υπάρχουν άλλα αδιάβαστα νήματα." + category: "Δεν υπάρχουν άλλα νήματα στην κατηγορία {{category}}." + top: "Δεν υπάρχουν άλλα κορυφαία νήματα." + bookmarks: "Δεν υπάρχουν άλλα νήματα με σελιδοδείκτη." search: "Δεν υπάρχουν άλλα αποτελέσματα σε αυτή την αναζήτηση." topic: unsubscribe: @@ -1292,7 +1325,7 @@ el: change_notification_state: "Η τρέχουσα κατάσταση των ειδοποιήσεων σου είναι" filter_to: one: "1 δημοσίευση σε νήματα" - other: "{{count}} δημοσιεύσεις σε νήματα" + other: "{{count}} αναρτήσεις σε νήματα" create: 'Νέο νήμα' create_long: 'Δημιουργία νέου νήματος' private_message: 'Ξεκίνα ένα προσωπικό μήνυμα' @@ -1340,30 +1373,70 @@ el: toggle_information: "Εμφάνιση ή απόκρυψη λεπτομερειών νήματος" read_more_in_category: "Θέλεις να διαβάσεις περισσότερα; Βρες άλλα νήματα στο {{catLink}} ή {{latestLink}}." read_more: "Θέλεις να διαβασεις περισσότερα; {{catLink}} ή {{latestLink}}." - read_more_MF: "Έχεις { UNREAD, plural, =0 {} one { is 1 αδιάβαστο } other { are # αδιάβαστα } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {έχεις } other{}} 1 νέο νήμα} other { {BOTH, select, true{and } false {έχεις } other{}} # νέα νήματα} } ακόμη, ή {CATEGORY, select, true {περιηγήσου σε άλλα νήματα {catLink}} false {{latestLink}} other {}}" + read_more_MF: "Έχεις { UNREAD, plural, =0 {} one { μόνο 1 αδιάβαστο } other { # αδιάβαστα } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {έχεις } other{}} 1 νέο νήμα} other { {BOTH, select, true{and } false {έχεις } other{}} # νέα νήματα} } ακόμη, ή {CATEGORY, select, true {περιηγήσου σε άλλα νήματα {catLink}} false {{latestLink}} other {}}" browse_all_categories: Περιήγηση σε όλες τις κατηγορίες view_latest_topics: δες τα πρόσφατα νήματα suggest_create_topic: Γιατί δεν φτιάχνεις ένα νέο νήμα; jump_reply_up: μετάβαση στην απάντηση που προηγείται jump_reply_down: μετάβαση στην απάντηση που ακολουθεί deleted: "Το νήμα έχει διαγραφεί " + topic_status_update: + title: "Ρύθμιση Χρονοδιακόπτη Νήματος" + save: "Ρύθμιση Χρονοδιακόπτη" + num_of_hours: "Ώρες:" + remove: "Αφαίρεση Χρονοδιακόπτη" + publish_to: "Δημοσίευση Σε:" + when: "Πότε:" + auto_update_input: + later_today: "Αργότερα σήμερα" + tomorrow: "Αύριο" + later_this_week: "Αργότερα αυτή την εβδομάδα" + this_weekend: "Αυτό το Σαββατοκύριακο" + next_week: "Την άλλη εβδομάδα" + next_month: "Τον άλλο μήνα" + pick_date_and_time: "Επέλεξε ημερομηνία και ώρα" + set_based_on_last_post: "Κλείσε ανάλογα με την τελευταία ανάρτηση" + publish_to_category: + title: "Χρονοδιάγραμμα Αναρτήσεων" + temp_open: + title: "Ανοιχτό Προσωρινά" + auto_reopen: + title: "Αυτόματο άνοιγμα νήματος" + temp_close: + title: "Κλειστό Προσωρινά" + auto_close: + title: "Αυτόματο κλείσιμο νήματος" + label: "Ώρες αυτόματου κλεισίματος νήματος:" + error: "Παρακαλώ εισάγετε μια έγκυρη αξία." + based_on_last_post: "Να μην κλείσει μέχρι η τελευταία ανάρτηση στο νήμα να είναι τόσο παλιά." + auto_delete: + title: "Αυτόματη διαγραφή νήματος" + reminder: + title: "Υπενθύμισέ Με" + status_update_notice: + auto_open: "Αυτό το νήμα θα ανοίξει αυτόματα σε %{timeLeft}." + auto_close: "Αυτό το νήμα θα κλείσει αυτόματα σε %{timeLeft}." + auto_publish_to_category: "Αυτό το νήμα θα αναρτηθεί στην κατηγορία #%{categoryName} %{timeLeft}." + auto_close_based_on_last_post: "Αυτό το νήμα θα κλείσει σε %{duration} μετά την τελευταία απάντηση." + auto_delete: "Αυτό το νήμα θα διαγραφεί αυτόματα σε %{timeLeft}." + auto_reminder: "Θα λάβεις υπενθύμιση σχετικά με αυτό το νήμα σε %{timeLeft}." auto_close_title: 'Ρυθμίσεις για το αυτόματο κλείσιμο' auto_close_immediate: one: "Η τελευταία δημοσίευση στο νήμα είναι ήδη 1 ώρα παλιό, έτσι το νήμα θα κλείσει αμέσως." - other: "Η τελευταία δημοσίευση είναι ήδη %{count} ώρες παλιό, έτσι το νήμα θα κλείσει αμέσως." + other: "Η τελευταία ανάρτηση είναι ήδη %{count} ώρες παλιά, έτσι το νήμα θα κλείσει αμέσως." timeline: back: "Πίσω" - back_description: "Πήγαινε πίσω στην τελευταία μη αναγνωσμένη δημοσιεύση" + back_description: "Πήγαινε πίσω στην τελευταία μη αναγνωσμένη ανάρτηση" replies_short: "%{current} / %{total}" progress: title: πρόοδος νήματος go_top: "αρχή" go_bottom: "τέλος" go: "πάμε" - jump_bottom: "μεταπήδηση στην τελευταία δημοσίευση" + jump_bottom: "μεταπήδηση στην τελευταία ανάρτηση" jump_prompt: "μεταπήδηση σε..." - jump_prompt_of: "των %{count} δημοσιεύσεων" - jump_prompt_long: "Σε ποια δημοσίευση θα ήθελες να μεταπηδήσεις;" + jump_prompt_of: "των %{count} αναρτήσεων" + jump_prompt_long: "Σε ποια ανάρτηση θα ήθελες να μεταπηδήσεις;" jump_bottom_with_number: "μετάβαση στην ανάρτηση %{post_number}" total: σύνολο αναρτήσεων current: τρέχουσα ανάρτηση @@ -1371,12 +1444,16 @@ el: title: άλλαξε το πόσο συχνά ειδοποιείσαι για αυτό το θέμα reasons: mailing_list_mode: "Έχεις ενεργοποιημένη τη λειτουργία λίστας αποδεκτών αλληλογραφίας, έτσι θα λαμβάνεις ενημερώσεις για τις απαντήσεις για το νήμα μέσω ηλεκτρονικών μηνυμάτων." - '3_10': 'Θα λάβεις ειδοποιήσεις, επειδή παρακολουθείς μια ετικέτα πάνω σε αυτό το θέμα.' + '3_10': 'Θα λάβεις ειδοποιήσεις, επειδή παρακολουθείς μια ετικέτα πάνω σε αυτό το νήμα.' '3_6': 'Θα λαμβάνεις ειδοποιήσεις επειδή παρακολουθείς αυτή την κατηγορία.' '3_5': 'Θα λαμβάνεις ειδοποιήσεις επειδή ξεκίνησες να παρακολουθείς αυτόματα αυτό το νήμα.' '3_2': 'Θα λαμβάνεις ειδοποιήσεις επειδή παρακολουθείς αυτό το νήμα.' '3_1': 'Θα λαμβάνεις ειδοποιήσεις επειδή δημιούργησες αυτό το νήμα.' '3': 'Θα λαμβάνεις ειδοποιήσεις επειδή παρακολουθείς αυτό το νήμα.' + '2_8': 'Θα βλέπεις έναν μετρητή νέων απαντήσεων επειδή ακολουθείς αυτήν την κατηγορία.' + '2_4': 'Θα βλέπεις έναν μετρητή νέων απαντήσεων επειδή απάντησες σε αυτό το νήμα.' + '2_2': 'Θα βλέπεις έναν μετρητή νέων απαντήσεων επειδή ακολουθείς αυτό το νήμα.' + '2': 'Θα βλέπεις έναν μετρητή νέων απαντήσεων επειδή διάβασες αυτό το νήμα.' '1_2': 'Θα λαμβάνεις ειδοποιήσεις εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε εσένα.' '1': 'Θα λαμβάνεις ειδοποιήσεις, εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε εσένα.' '0_7': 'Θα αγνοούνται όλες οι ειδοποιήσεις από αυτή την κατηγορία.' @@ -1387,7 +1464,7 @@ el: description: "Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια απάντηση σε αυτό το μήνυμα, και το πλήθος των καινούριων απαντήσεων θα εμφανίζεται." watching: title: "Παρατηρείται" - description: "Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια απάντηση σε αυτό το νήμα, και καταμέτρηση καινούριων απαντήσεων θα εμφανίζεται." + description: "Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια απάντηση σε αυτό το νήμα και ένας μετρητής νέων απαντήσεων θα εμφανίζεται." tracking_pm: title: "Παρακολουθείται" description: "Το πλήθος των καινούριων απαντήσεων θα εμφανίζεται για αυτό το μήνυμα. Θα λαμβάνεις ειδοποιήσεις εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε εσένα." @@ -1412,6 +1489,7 @@ el: open: "Νέο νήμα" close: "Κλείσε το νήμα" multi_select: "Διάλεξε αναρτήσεις..." + timed_update: "Θέσε τον χρονοδιακόπτη του νήματος..." pin: "Καρφίτσωσε το νήμα..." unpin: "Ξεκαρφίτσωσε το νήμα..." unarchive: "Επανέφερε το νήμα από το αρχείο" @@ -1419,8 +1497,8 @@ el: invisible: "Κάντο αόρατο" visible: "Κάντο ορατό" reset_read: "Επαναφορά «διαβασμένων»" - make_public: "Κάνε Δημόσιο το θέμα" - make_private: "Γράψε προσωπικό μήνυμα" + make_public: "Κάνε Δημόσιο το νήμα" + make_private: "Κάνε Προσωπικό Μήνυμα" feature: pin: "Καρφίτσωσε το νήμα" unpin: "Ξεκαρφίτσωσε το νήμα" @@ -1429,7 +1507,7 @@ el: remove_banner: "Να αφαιρεθεί η ιδιότητα του νήματος ανακοίνωσης" reply: title: 'Απάντηση' - help: 'Ξεκινήστε να συνθέτετε μια απάντηση σε αυτό το θέμα' + help: 'ξεκινήστε να συνθέτετε μια απάντηση σε αυτό το νήμα' clear_pin: title: "Ξεκαρφίτσωμα" help: "Ξεκαρφίτσωσε αυτό το νήμα ώστε να μην εμφανίζεται πια στην κορυφή της λίστας." @@ -1444,26 +1522,26 @@ el: help: 'Επισήμανε αυτό το νήμα στους συντονιστές ή στείλε ένα προσωπικό μήνυμα για αυτό.' success_message: 'Σήμανες αυτό το νήμα.' feature_topic: - title: "Το νήμα να γίνει προεξέχον" + title: "Θέσε το νήμα σε προβολή" pin: "Το νήμα αυτό να εμφανίζεται στην κορυφή της {{categoryLink}} κατηγορίας μέχρι" confirm_pin: "Υπάρχουν ήδη {{count}} καρφιτσωμένα νήματα. Τόσα πολλά καρφιτσωμένα νήματα μπορεί να είναι υπερβολικός φόρτος για νέους και ανώνυμους χρήστες. Θες στ' αλήθεια να καρφιτσώσεις ακόμη ένα νήμα σε αυτή την κατηγορία;" unpin: "Απομάκρυνε το νήμα από την κορυφή της κατηγορίας {{categoryLink}}." unpin_until: "Απομάκρυνε το νήμα από την κορυφή της {{categoryLink}} κατηγορίας ή περίμενε μέχρι %{until}." pin_note: "Οι χρήστες μπορούν να ξεκαρφιτσώσουν αυτό το νήμα ο καθένας για τον εαυτό του." - pin_validation: "Απαιτείται ημερομηνία για να καρφιτσώσεις το θέμα." + pin_validation: "Απαιτείται ημερομηνία για να καρφιτσώσεις το νήμα." not_pinned: "Δεν υπάρχουν νήματα καρφιτσωμένα σε {{categoryLink}}." already_pinned: one: "Νήματα προσωρινά καρφιτσωμένα σε {{categoryLink}}: 1" other: "Νήματα προσωρινά καρφιτσωμένα σε {{categoryLink}}: {{count}}" pin_globally: "Εμφάνισε αυτό το νήμα στην κορυφή όλων των καταχωρήσεων νημάτων μέχρι" confirm_pin_globally: "Υπάρχουν ήδη {{count}} καθολικά καρφιτσωμένα νήματα. Τόσα πολλά καρφιτσωμένα νήματα μπορεί να είναι υπερβολικός φόρτος για νέους και ανώνυμους χρήστες. Θες στ' αλήθεια να καρφιτσώσεις καθολικά ακόμη ένα νήμα;" - unpin_globally: "Αφαίρεσε αυτό το θέμα από την κορυφή όλων των λίστων θεμάτων" + unpin_globally: "Αφαίρεσε αυτό το νήμα από την κορυφή όλων των λίστων νημάτων" unpin_globally_until: "Απομάκρυνε το νήμα από την κορυφή όλων των καταχωρήσεων των νημάτων ή περίμενε μέχρι %{until}." global_pin_note: "Οι χρήστες μπορούν να ξεκαρφιτσώσουν το νήμα ο καθένας για τον εαυτό του." - not_pinned_globally: "Δεν υπάρχουν καρφιτσωμένα θέματα παγκοσμίως." + not_pinned_globally: "Δεν υπάρχουν καθολικά καρφιτσωμένα νήματα." already_pinned_globally: one: "Τα νήματα πρόσφατα καρφιτσώθηκαν παγκοσμίως:1" - other: "Τα νήματα πρόσφατα καρφιτσώθηκαν παγκοσμίως: {{count}}" + other: "Πρόσφατα καθολικά καρφιτσωμένα νήματα: {{count}}" make_banner: "Αυτό το νήμα να γίνει νήμα ανακοίνωσης που θα φαίνεται στην αρχή όλων των σελίδων." remove_banner: "Απομακρύνει το νήμα ανακοίνωσης από την αρχή όλων των σελίδων." banner_note: "Οι χρήστες μπορούν να κλείσουν την ανακοίνωση έτσι ώστε να μην εμφανίζεται σε αυτούς. Ένα μόνο νήμα μπορεί να είναι νήμα ανακοίνωσης κάθε φορά." @@ -1490,12 +1568,13 @@ el: sso_enabled: "Δώσε το χρηστώνυμο του προσώπου που θα ήθελες να προσκαλέσεις σε αυτό το νήμα." to_topic_blank: "Δώσε το χρηστώνυμο ή τη διεύθυνση ηλεκτρονικού ταχυδρομείου του προσώπου που θα ήθελες να προσκαλέσεις σε αυτό το νήμα." to_topic_email: "Έδωσες μια διεύθυνση ηλεκτρονικού ταχυδρομείου. Θα στείλουμε μια πρόσκληση που θα επιτρέπει στον παραλήπτη να απαντήσει απευθείας σε αυτό το νήμα." - to_topic_username: "Έχεις εισαγάγει όνομα χρήστη. Θα σου στείλουμε ειδοποίηση με ένα σύνδεσμο προσκαλώντας τους σε αυτό το νήμα." - to_username: "Δώσε το όνομα χρήστη του ατόμου που θα ήθελες να προσκαλέσεις. Θα σου στείλουμε ειδοποίηση με ένα σύνδεσμο προσκαλώντας τους σε αυτό το νήμα." + to_topic_username: "Έχεις εισαγάγει όνομα χρήστη. Θα του στείλουμε ειδοποίηση με ένα σύνδεσμο προσκαλώντας τον σε αυτό το νήμα." + to_username: "Δώσε το όνομα χρήστη του ατόμου που θα ήθελες να προσκαλέσεις. Θα του στείλουμε ειδοποίηση με ένα σύνδεσμο προσκαλώντας τον σε αυτό το νήμα." email_placeholder: 'name@example.com' success_email: "Στείλαμε μια πρόσκληση στον/στην {{emailOrUsername}}. Θα σε ειδοποιήσουμε όταν η πρόσκληση γίνει αποδεκτή. Στη σελίδα του προφίλ σου μπορείς να παρακολουθήσεις την εξέλιξη όλων των προσκλήσεών σου." success_username: "Προσκαλέσαμε τον χρήστη να συμμετέχει σε αυτό το νήμα." error: "Λυπούμαστε αλλά δεν μπορέσαμε να προσκαλέσουμε αυτό το πρόσωπο. Μήπως έχει ήδη προσκληθεί; (ο ρυθμός αποστολής προσκλήσεων είναι περιορισμένος)" + success_existing_email: "Ο χρήστης με email {{emailOrUsername}} υπάρχει ήδη. Προσκαλέσαμε αυτόν τον χρήστης να συμμετέχει στο νήμα." login_reply: 'Συνδέσου για να απαντήσεις' filters: n_posts: @@ -1518,9 +1597,9 @@ el: one: "Παρακαλώ επίλεξε το νήμα στο οποίο θέλεις να μεταφέρεις την ανάρτηση." other: "Παρακαλώ επίλεξε το νήμα στο οποίο θέλεις να μεταφέρεις τις {{count}} αυτές αναρτήσεις." merge_posts: - title: "Συγχώνευσε επιλεγμένες δημοσιεύσεις" - action: "συγχώνευσε επιλεγμένες δημοσιεύσεις" - error: "Προέκυψε σφάλμα κατά τη συγχώνευση των επιλεγμένων δημοσιεύσεων." + title: "Συγχώνευσε επιλεγμένες αναρτήσεις" + action: "συγχώνευσε επιλεγμένες αναρτήσεις" + error: "Προέκυψε σφάλμα κατά τη συγχώνευση των επιλεγμένων αναρτήσεων." change_owner: title: "Άλλαξε τους ιδιοκτήτες των αναρτήσεων" action: "Αλλαγή ιδιοκτήτη" @@ -1530,16 +1609,17 @@ el: instructions: one: "Παρακαλώ επίλεξε τον νέο ιδιοκτήτη της ανάρτησης του {{old_user}}." other: "Παρακαλώ επίλεξε τον νέο ιδιοκτήτη των {{count}} αναρτήσεων του {{old_user}}." - instructions_warn: "Έχε υπόψη πως δεν θα μεταφερθούν αναδρομικά όλες οι ειδοποιήσεις για αυτή την ανάρτηση στο νέο χρήστη.
    Προειδοποίηση: Τώρα κανένα δεδομένο σχετικό με την ανάρτηση δε θα μεταφερθεί στο νέο χρήστη. Χρησιμοποίησέ το με προσοχή." + instructions_warn: "Τυχόν ειδοποιήσεις σχετικές με την ανάρτηση δεν θα μεταφερθούν στον νέο χρήστη.
    Προειδοποίηση: Στην παρούσα φάση κανένα δεδομένο σχετικό με την ανάρτηση δε θα μεταφερθεί στο νέο χρήστη. Χρησιμοποίησέ το με προσοχή." change_timestamp: + title: "Αλλαγή Χρονοσήμανσης..." action: "αλλαγή χρονοσήμανσης" invalid_timestamp: "Η χρονοσήμανση δεν μπορεί να υπάρξει στο μέλλον." error: "Προέκυψε σφάλμα κατά την αλλαγή χρονοσήμανσης του νήματος." - instructions: "Παρακαλώ επίλεξε τη νέα χρονοσήμανση του νήματος. Οι δημοσιεύσεις του νήματος θα ενημερωθούν να έχουν την ίδια διαφορά ώρας." + instructions: "Παρακαλώ επίλεξε τη νέα χρονοσήμανση του νήματος. Οι αναρτήσεις του νήματος θα ενημερωθούν ώστε να έχουν την ίδια διαφορά ώρας." multi_select: select: 'επίλεξε' selected: 'επιλεγμένες ({{count}})' - select_replies: 'επίλεξε και συμπερίλαβε τις απαντήσεις' + select_replies: 'επίλεξε +replies' delete: διαγραφή επιλεγμένων cancel: ακύρωση επιλογής select_all: επιλογή όλων @@ -1550,12 +1630,12 @@ el: post: reply: " {{replyAvatar}} {{usernameLink}}" reply_topic: " {{link}}" - quote_reply: "Φράση" + quote_reply: "Παράθεση" edit: "Επεξεργασία του {{link}} {{replyAvatar}} {{username}}" edit_reason: "Αιτία:" post_number: "ανάρτηση {{number}}" wiki_last_edited_on: "το βίκι επεξεργάστηκε τελευταία φορά στις" - last_edited_on: "Η τελευταία φορά που άλλαξε η ανάρτηση ήταν " + last_edited_on: "η ανάρτηση επεξεργάστηκε τελευταία φορά στις" reply_as_new_topic: "Απάντηση με διασυνδεδεμένο νήμα" reply_as_new_private_message: "Απάντηση ως νέο μήνυμα στον ίδιο παραλήπτη" continue_discussion: "Συνέχιση της συζήτησης από το {{postLink}}:" @@ -1578,33 +1658,33 @@ el: other: "{{count}} \"Μου αρέσει\"" has_likes_title: one: "1 άτομο πάτησε \"μου αρέσει\" στη δημοσίευση" - other: "{{count}} άτομα που πάτησαν \"Μου αρέσει\" στη δημοσίευση" - has_likes_title_only_you: "Σου αρέσει αυτή η δημοσίευση" + other: "{{count}} άτομα πάτησαν \"Μου αρέσει\" στην ανάρτηση" + has_likes_title_only_you: "σου αρέσει αυτή η ανάρτηση" has_likes_title_you: one: "εσύ και 1 άλλο άτομο πάτησε \"Μου αρέσει\" στη δημοσίευση" - other: "εσύ και {{count}} άλλα άτομα πατήσατε \"Μου αρέσει\" στη δημοσίευση" + other: "εσύ και {{count}} ακόμα άτομα πατήσατε \"Μου αρέσει\" στην ανάρτηση" errors: create: "Λυπούμαστε, παρουσιάστηκε σφάλμα κατά την δημιουργία της ανάρτησης. Προσπάθησε πάλι." edit: "Λυπούμαστε, παρουσιάστηκε σφάλμα κατά την επεξεργασία της ανάρτησης. Προσπάθησε πάλι." upload: "Λυπούμαστε, παρουσιάστηκε σφάλμα κατά το ανέβασμα του αρχείου. Προσπάθησε πάλι." - file_too_large: "Λυπούμαστε, αυτός ο φάκελος είναι πολύ μεγάλος (το μέγιστο μέγεθος είναι {{max_size_kb}}kb). Γιατί δεν μεταφορτώνεις τον μεγάλο σου φάκελο σε μια υπηρεσία ανταλλαγής cloud κι έπειτα να μοιραστείς τον σύνδεσμο;" + file_too_large: "Λυπούμαστε, αυτό το αρχείο είναι πολύ μεγάλο (το μέγιστο μέγεθος είναι {{max_size_kb}}kb). Γιατί δεν μεταφορτώνεις το αρχείο σου σε μια υπηρεσία ανταλλαγής αρχείων κι έπειτα να μοιραστείς τον σύνδεσμο;" too_many_uploads: "Λυπούμαστε, μπορείς να ανεβάζεις μόνο ένα αρχείο τη φορά." - too_many_dragged_and_dropped_files: "Λυπούμαστε, μπορείς να ανεβάσεις μόνο 10 φακέλους τη φορά." - upload_not_authorized: "Λυπούμαστε, ο φάκελος που προσπαθείς να ανεβάσεις δεν επιτρέπεται (επιτρεπόμενες επεκτάσεις:{{authorized_extensions}})" - image_upload_not_allowed_for_new_user: "Λυπούμαστε, οι νέοι χρήστες δεν μπορούν να ανεβάζουν εικόνες." - attachment_upload_not_allowed_for_new_user: "Λυπούμαστε, οι νέοι χρήστες δεν μπορούν να ανεβάζουν συννημένα." - attachment_download_requires_login: "Λυπούμαστε, για να κατεβάζεις συνημμένα πρέπει πρώτα να συνδεθείς." + too_many_dragged_and_dropped_files: "Λυπούμαστε, μπορείς να ανεβάσεις μόνο 10 αρχεία τη φορά." + upload_not_authorized: "Λυπούμαστε, το αρχείο που προσπαθείς να ανεβάσεις δεν επιτρέπεται (επιτρεπόμενες επεκτάσεις:{{authorized_extensions}})" + image_upload_not_allowed_for_new_user: "Λυπούμαστε, οι νέοι χρήστες δεν μπορούν να ανεβάσουν εικόνες." + attachment_upload_not_allowed_for_new_user: "Λυπούμαστε, οι νέοι χρήστες δεν μπορούν να επισυνάψουν αρχεία." + attachment_download_requires_login: "Λυπούμαστε, για να κατεβάσεις συνημμένα αρχεία, πρέπει πρώτα να συνδεθείς." abandon: confirm: "Σίγουρα θέλεις να απορρίψεις την ανάρτησή σου;" no_value: "Όχι, κράτησέ τη" yes_value: "Ναί, απέρριψέ τη" via_email: "αυτή η ανάρτηση έγινε μέσω ηλεκτρονικού ταχυδρομείου" - via_auto_generated_email: "αυτή η δημοσίευση έφτασε μέσω ενός ηλεκτρονικού μηνύματος που δημιουργήθηκε αυτόματα" - whisper: "αυτή η δημοσίευση λέγεται εμπιστευτικά στους συντονιστές" + via_auto_generated_email: "αυτή η ανάρτηση έφτασε μέσω ενός ηλεκτρονικού μηνύματος που δημιουργήθηκε αυτόματα" + whisper: "αυτή η ανάρτηση είναι εμπιστευτική προς τους συντονιστές" wiki: - about: "αυτή η δημοσίευση είναι βίκι" + about: "αυτή η ανάρτηση είναι βίκι" archetypes: - save: 'Αποθήκευση επιλογών' + save: 'Αποθήκευση Επιλογών' few_likes_left: "Ευχαριστούμε που μοιράστηκες την αγάπη σου! Έχεις μόνο μερικά \"μου αρέσει\" ακόμα να χρησιμοποιήσεις σήμερα." controls: reply: "απάντησε σε αυτή την ανάρτηση" @@ -1612,8 +1692,9 @@ el: has_liked: "αυτή η ανάρτηση σου αρέσει" undo_like: "δεν «μου αρέσει» πια" edit: "επεξεργασία ανάρτησης" + edit_action: "Επεξεργασία" edit_anonymous: "Λυπούμαστε, αλλά για να επεξεργαστείς αυτή την ανάρτηση πρέπει πρώτα να συνδεθείς." - flag: "Ανάφερε αυτή την ανάρτηση στους συντονιστές ή στείλε ένα προσωπικό μήνυμα" + flag: "ανάφερε την ανάρτηση στους συντονιστές ή στείλε ένα προσωπικό μήνυμα σχετικά με αυτή" delete: "Σβήσε αυτή την ανάρτηση" undelete: "επανάφερε αυτή την ανάρτηση " share: "κοινοποίησε έναν σύνδεσμο προς αυτή την ανάρτηση " @@ -1624,14 +1705,14 @@ el: other: "Θέλεις επίσης να σβήσεις τις {{count}} άμεσες απαντήσεις σε αυτή την ανάρτηση;" yes_value: "Ναι, σβήσε και τις απαντήσεις" no_value: "Όχι, μόνο αυτή την ανάρτηση" - admin: "Δράσεις διαχειριστών" + admin: "ενέργειες διαχειριστή ανάρτησης" wiki: "Δημιουργία βίκι" unwiki: "Αφαίρεση βίκι" convert_to_moderator: "Πρόσθεσε χρωματισμό στους συνεργάτες" revert_to_regular: "Αφαίρεσε το χρωματισμό από τους συνεργάτες" rebake: "Ανανέωση HTML" unhide: "Επανεμφάνιση" - change_owner: "Αλλαγή ιδιοκτησίας" + change_owner: "Αλλαγή Ιδιοκτησίας" actions: flag: 'Επισήμανση' defer_flags: @@ -1642,7 +1723,7 @@ el: spam: "Αναίρεση σήμανσης" inappropriate: "Αναίρεση σήμανσης" bookmark: "Αφαίρεση σελιδοδείκτη" - like: "δεν «μου αρέσει» πια" + like: "Αναίρεση «μου αρέσει»" vote: "Αναίρεση ψήφου" people: off_topic: "μαρκαρισμένο σαν εκτός θέματος" @@ -1650,9 +1731,9 @@ el: inappropriate: "μαρκαρισμένο σαν ακατάλληλο" notify_moderators: "ειδοποιήθηκαν οι συντονιστές" notify_user: "έστειλε ένα μήνυμα" - bookmark: "του έβαλαν σελιδοδείκτη" - like: "Σου άρεσε αυτό" - vote: "Ψήφισες για αυτό" + bookmark: "του έβαλε σελιδοδείκτη" + like: "άρεσε αυτό" + vote: "ψήφισε αυτό" by_you: off_topic: "Το επισήμανες σαν εκτός θέματος" spam: "Το επισήμανες σαν ανεπιθύμητο" @@ -1677,13 +1758,13 @@ el: other: "Εσύ και άλλοι {{count}} το επισημάνατε στους συντονιστές" notify_user: one: "Εσύ και ένα ακόμη πρόσωπο στείλατε ένα μήνυμα σε αυτόν τον χρήστη" - other: "Εσύ και {{count}} άλλοι στείλατε ένα μήνυμα σε αυτόν τον χρήστη" + other: "Εσύ και άλλοι {{count}} στείλατε ένα μήνυμα σε αυτόν τον χρήστη" bookmark: one: "Εσύ και άλλος ένας τοποθετήσατε σελιδοδείκτη στην ανάρτηση" other: "Εσύ και άλλοι {{count}} τοποθετήσατε σελιδοδείκτη στην ανάρτηση" like: one: "Εσένα και ενός άλλου «σας άρεσε»" - other: "Εσένα και {{count}} άλλων «σας άρεσε»" + other: "Εσένα και άλλους {{count}} «σας άρεσε»" vote: one: "Εσύ και άλλος ένας ψηφίσατε για την ανάρτηση" other: "Εσύ και άλλοι {{count}} ψηφίσατε για την ανάρτηση" @@ -1702,7 +1783,7 @@ el: other: "{{count}} άτομα το επισημάνανε στους συντονιστές" notify_user: one: "1 πρόσωπο έστειλε ένα μήνυμα σε αυτόν τον χρήστη" - other: "{{count}} πρόσωπα έστειλαν ένα μήνυμα σε αυτόν τον χρήστη" + other: "{{count}} άτομα έστειλαν ένα μήνυμα σε αυτόν τον χρήστη" bookmark: one: "Ένα άτομο τοποθέτησε σελιδοδείκτη στην ανάρτηση" other: "{{count}} άτομα τοποθέτησαν σελιδοδείκτη στην ανάρτηση" @@ -1716,6 +1797,10 @@ el: confirm: one: "Είσαι σίγουρος ότι θέλεις να σβήσεις αυτή την ανάρτηση;" other: "Είσαι σίγουρος ότι θέλεις να σβήσεις όλες αυτές τις αναρτήσεις;" + merge: + confirm: + one: "Είσαι σίγουρος πως θέλεις να συγχωνεύσεις αυτές τις δημοσιεύσεις;" + other: "Είσαι σίγουρος πως θέλεις να συγχωνεύσεις αυτές τις {{count}} αναρτήσεις;" revisions: controls: first: "Πρώτη αναθεώρηση" @@ -1724,19 +1809,19 @@ el: last: "Τελευταία αναθεώρηση" hide: "Κρύψε την αναθεώρηση" show: "Εμφάνισε την αναθεώρηση" - revert: "Επιστροφή στην αναθεώρηση" - edit_wiki: "Επεξεργασία της Wiki" - edit_post: "Επεξεργασία δημοσίευσης" + revert: "Επιστροφή σε αυτήν την αναθεώρηση" + edit_wiki: "Επεξεργασία του Βίκι" + edit_post: "Επεξεργασία Ανάρτησης" comparing_previous_to_current_out_of_total: "{{previous}} {{current}} /{{total}}" displays: inline: - title: "Δείξε τις αλλαγές ανάμεσα στο κείμενο" + title: "Δείξε το τελικό κείμενο με τις αλλαγές και προσθηκες ενσωματωμένες σε αυτό" button: 'HTML' side_by_side: title: "Δείξε τις αλλαγές δίπλα στο κείμενο" button: 'HTML' side_by_side_markdown: - title: "Δείξε τα αρχικά κείμενα δίπλα-δίπλα για σύγκριση" + title: "Δείξε το αρχικό κείμενο δίπλα-δίπλα για σύγκριση" button: 'Ακατέργαστο' raw_email: displays: @@ -1759,22 +1844,24 @@ el: view: 'Δείξε τα νήματα αυτής της κατηγορίας' general: 'Γενικά' settings: 'Ρυθμίσεις' - topic_template: "Δείγμα νήματος" + topic_template: "Πρότυπο Νήματος" tags: "Ετικέτες" + tags_allowed_tags: "Επέτρεψε μόνο σε αυτές τις ετικέτες να χρησιμοποιηθούν σε αυτή την κατηγορία." + tags_allowed_tag_groups: "Επέτρεψε μόνο ετικέτες από αυτές τις ομάδες να χρησιμοποιηθούν σε αυτή την κατηγορία." tags_placeholder: "(Προαιρετική) λίστα επιτρεπόμενων ετικετών" - tag_groups_placeholder: "(Προαιρετική) λίστα επιτρεπόμενων ετικετών σε ομάδες" + tag_groups_placeholder: "(Προαιρετική) λίστα επιτρεπόμενων ομάδων ετικετών" topic_featured_link_allowed: "Επίτρεψε προτεινόμενους συνδέσμους σε αυτή την κατηγορία" delete: 'Διαγραφή κατηγορίας' create: 'Νέα κατηγορία' - create_long: 'Δημιουργία καινούριας κατηγορίας' + create_long: 'Δημιουργία νέας κατηγορίας' save: 'Αποθήκευση κατηγορίας' - slug: 'Μνημονικό για το URL' + slug: 'Φιλικό Όνομα Κατηγορίας' slug_placeholder: '(Προαιρετικά) με λέξεις ενωμένες με παύλα για το URL' creation_error: Παρουσιάστηκε κάποιο σφάλμα κατά την δημιουργία της κατηγορίας save_error: Παρουσιάστηκε κάποιο σφάλμα κατά την αποθήκευση της κατηγορίας. name: "Όνομα κατηγορίας" description: "Περιγραφή" - topic: "Κατηγορία νήματος" + topic: "νήμα κατηγορίας" logo: "Εικονίδιο κατηγορίας" background_image: "Εικόνα φόντου κατηγορίας" badge_colors: "Χρώματα παράσημου" @@ -1794,15 +1881,16 @@ el: email_in: "Προσαρμοσμένη διεύθυνση για εισερχόμενα μηνύματα ηλεκτρονικού ταχυδρομείου:" email_in_allow_strangers: "Αποδοχή μηνυμάτων ηλεκτρονικού ταχυδρομείου από ανώνυμους χρήστες, χωρίς λογαριασμούς" email_in_disabled: "Η δημιουργία νέων νημάτων μέσω ηλεκτρονικού ταχυδρομείου είναι απενεργοποιημένη στις ρυθμίσεις. Για να επιτραπεί η δημιουργία νέων νημάτων μέσω ηλεκτρονικού ταχυδρομείου," - email_in_disabled_click: 'ενεργοποίσε τη ρύθμιση «είσοδος με ηλ. ταχυδρομείο»' + email_in_disabled_click: 'ενεργοποίσε τη ρύθμιση «εισερχόμενα email».' suppress_from_homepage: "Αποσιώπηση αυτής της κατηγορίας από την αρχική σελίδα." - show_subcategory_list: "Προβολή υποκατηγοριών κάτω απο τα νήματα αυτής της κατηγορίας " + show_subcategory_list: "Προβολή λίστας υποκατηγοριών πάνω απο τα νήματα αυτής της κατηγορίας " num_featured_topics: "Αριθμός νημάτων που εμφανίζονται στην σελίδα κατηγοριών:" subcategory_num_featured_topics: "Αριθμός προτεινόμενων νημάτων στην σελίδα της γονικής κατηγορίας:" all_topics_wiki: "Φτιάξε καινούρια νήματα βίκι από προεπιλογή. " subcategory_list_style: "Στυλ λίστας υποκατηγορίας:" sort_order: "Ταξινόμηση λίστας νημάτων κατά:" default_view: "Προκαθορισμένη λίστα νημάτων:" + default_top_period: "Προκαθόρισμένο χρονικό διάστημα κορυφαίων νημάτων:" allow_badges_label: "Να επιτρέπεται η απονομή παράσημων σε αυτή την κατηγορία" edit_permissions: "Επεξεργασία δικαιωμάτων" add_permission: "Προσθήκη δικαιώματος" @@ -1815,13 +1903,13 @@ el: notifications: watching: title: "Παρατηρείται" - description: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια δημοσίευση, και το πλήθος των νέων απαντήσεων θα εμφανίζεται." + description: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Θα λαμβάνεις ειδοποιήσεις για κάθε καινούρια ανάρτηση και το πλήθος των νέων απαντήσεων θα εμφανίζεται." watching_first_post: - title: "Παρακολούθηση πρώτης δημοσίευσης" - description: "Θα λαμβάνεις ειδοποίηση μόνο για την πρώτη δημοσίευση των καινούριων θεμάτων σε αυτές τις κατηγορίες." + title: "Παρακολούθηση πρώτης ανάρτησης" + description: "Θα λαμβάνεις ειδοποίηση μόνο για την πρώτη ανάρτηση για κάθε νέο νήμα σε αυτές τις κατηγορίες." tracking: - title: "Παρακολουθείται" - description: "Θα παρακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Θα λαμβάνεις ειδοποιήσεις, εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε εσένα, και το πλήθος των νέων απαντήσεων θα εμφανίζεται." + title: "Ακολουθείται" + description: "Θα ακολουθείς αυτόματα όλα τα νήματα σε αυτές τις κατηγορίες. Θα λαμβάνεις ειδοποιήσεις, εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε εσένα, και το πλήθος των νέων απαντήσεων θα εμφανίζεται." regular: title: "Φυσιολογικά" description: "Θα ειδοποιείσαι εάν κάποιος αναφέρει το @όνομά σου ή απαντά σε εσένα." @@ -1831,11 +1919,11 @@ el: sort_options: default: "προεπιλογή" likes: "Αρέσει" - op_likes: "Πρωτότυπο Δημοσίευση \"Μου αρέσει\"" + op_likes: "\"Μου αρέσει\" Αρχικής Ανάρτησης" views: "Προβολές" - posts: "Δημοσιεύσεις" + posts: "Αναρτήσεις" activity: "Δραστηριότητα" - posters: "Αφίσες" + posters: "Συμμετέχοντες" category: "Κατηγορία" created: "Δημιουργήθηκε" sort_ascending: 'Αύξουσα' @@ -1852,7 +1940,7 @@ el: notify_action: 'Μήνυμα' official_warning: 'Επίσημη Προειδοποίηση' delete_spammer: "Διαγραφή του ανεπιθύμητου χρήστη" - delete_confirm_MF: "Πρόκειται να διαγράψεις {POSTS, plural, one {1 post} άλλα {# δημοσιεύσεις}} και{TOPICS,plural, one {1 topic} άλλα {# topics}} από το χρήστη, αφαίρεσε το λογαριασμό τους, μπλόκαρε τις εγγραφές από τη διεύθυνση IP {ip_address}, και πρόσθεσε τη διεύθυνση του ηλεκτρονικού ταχυδρομείου τους {email} σε μια μόνιμη καταχώρηση μπλοκαρισμένων. Είσαι βέβαιος πως αυτός ο χρήστης είναι ανεπιθύμητος;" + delete_confirm_MF: "Πρόκειται να διαγράψεις {POSTS, plural, one {1 ανάρτηση} other {# αναρτήσεις}} και{TOPICS,plural, one {1 νήμα} other {# νήματα}} από το χρήστη, να διαγράψεις τον λογαριασμό του, να απαγορεύσεις τις εγγραφές από τη διεύθυνση IP {ip_address} και να πρόσθεσεις την διεύθυνση email {email} στην λίστα ανεπιθύμητων. Είσαι βέβαιος πως αυτός ο χρήστης είναι ανεπιθύμητος;" yes_delete_spammer: "Ναι, σβήσε τον ανεπιθύμητο χρήστη" ip_address_missing: "(μη διαθέσιμο)" hidden_email_address: "(κρυφό)" @@ -1872,24 +1960,24 @@ el: other: "βάλε τουλάχιστον {{count}} χαρακτήρες" more: one: "1 να πας..." - other: "{{count}} να πας..." + other: "{{count}} ακόμα..." left: one: "1 απομένει" - other: "{{count}} απομένει" + other: "{{count}} απομένουν" flagging_topic: title: "Ευχαριστούμε για τη συνεισφρορά σου..." action: "Επισήμανση του νήματος" notify_action: "Μήνυμα" topic_map: title: "Περίληψη νήματος" - participants_title: "Συχνές δημοσιεύσεις" + participants_title: "Συχνοί Συμμετέχοντες" links_title: "Δημοφιλείς Σύνδεσμοι" - links_shown: "εμφάνισε περισσότερες συνδέσεις" + links_shown: "εμφάνισε περισσότερους συνδέσμους..." clicks: one: "1 κλικ" other: "%{count} κλικ" post_links: - about: "επέκτεινε περισσότερους συνδέσμους για αυτή τη δημοσίευση" + about: "ανάπτυξε περισσότερους συνδέσμους για αυτή την ανάρτηση" title: one: "1 περισσότερο" other: "%{count} περισσότερα" @@ -1901,28 +1989,28 @@ el: locked: help: "Αυτό το νήμα είναι πια κλειστό. Οι απαντήσεις δεν είναι πλέον δυνατές" archived: - help: "Αυτό το νήμα είναι αρχικοποιημένο. Έχει παγώσει και δεν μπορεί πλέον να τροποποιηθεί" + help: "Αυτό το νήμα είναι αρχειοθετημένο. Έχει παγώσει και δεν μπορεί πλέον να τροποποιηθεί" locked_and_archived: - help: "Αυτό το θέμα είναι κλειστό και αρχειοθετημένο. Δε δέχεται πια καινούριες απαντήσεις και δεν μπορεί να αλλάξει." + help: "Αυτό το νήμα είναι κλειστό και αρχειοθετημένο. Δε δέχεται πια καινούριες απαντήσεις και δεν μπορεί να αλλάξει." unpinned: title: "Ξεκαρφιτσωμένο" help: "Για σένα αυτό το νήμα είναι ξεκαρφιτσωμένο. Θα εμφανίζεται στην κανονική του σειρά." pinned_globally: - title: "Παντού καρφιτσωμένο" - help: "Αυτό το νήμα είναι καρφιτσωμένο παγκοσμίως. Θα εμφανίζεται στην κορυφή των τελευταίων και στην κατηγορία του" + title: "Καρφιτσωμένο Καθολικά" + help: "Αυτό το νήμα είναι καρφιτσωμένο καθολικά. Θα εμφανίζεται στην κορυφή των τελευταίων και στην κατηγορία του" pinned: title: "Καρφιτσωμένο" help: "Αυτό το νήμα είναι καρφιτσωμένο για σένα. Θα εμφανίζεται πάντα στην κορυφή της κατηγορίας του " invisible: - help: "Αυτό το νήμα είναι αόρατο. Δε θα εμφανίζεται σε καμια λίστα νημάτων και μπορεί να φάνε μόνο αν ακολουθήσεις ένα άμεσο σύνδεσμο προς αυτό." + help: "Αυτό το νήμα είναι αόρατο. Δε θα εμφανίζεται σε καμια λίστα νημάτων και μπορεί να εμφανιστεί μόνο αν ακολουθήσεις ένα άμεσο σύνδεσμο προς αυτό." posts: "Αναρτήσεις" posts_long: "υπάρχουν {{number}} αναρτήσεις σε αυτό το νήμα" posts_likes_MF: | Αυτό το νήμα έχει {count, plural, one {1 απάντηση} other {# απαντήσεις}} {ratio, select, - χαμηλό {με μεγάλη αναλογία «μου αρέσει» στις αναρτήσεις} - μέτριο {με πολύ μεγάλη αναλογία «μου αρέσει» στις αναρτήσεις} - ψηλό - {με εξαιρετικά μεγάλη αναλογία «μου αρέσει» στις αναρτήσεις} other {}} + low {με μεγάλη αναλογία «μου αρέσει» στις αναρτήσεις} + med {με πολύ μεγάλη αναλογία «μου αρέσει» στις αναρτήσεις} + high {με εξαιρετικά μεγάλη αναλογία «μου αρέσει» στις αναρτήσεις} + other {}} original_post: "Αρχική ανάρτηση" views: "Προβολές" views_lowercase: @@ -1951,7 +2039,7 @@ el: categories_list: "Λίστα κατηγοριών" filters: with_topics: "%{filter} νήματα" - with_category: "%{filter}ε νήματα στην %{category}" + with_category: "%{filter} %{category} νήματα" latest: title: "Τελευταία" title_with_count: @@ -1959,7 +2047,7 @@ el: other: "Τελευταία ({{count}})" help: "νήματα με πρόσφατες αναρτήσεις" hot: - title: "Δημοφιλές" + title: "Δημοφιλή" help: "μια επιλογή από δημοφιλή νήματα" read: title: "Διαβασμένο" @@ -1985,7 +2073,7 @@ el: one: "1 νέο" other: "{{count}} νέα" lower_title: "νέο" - title: "Καινούριο" + title: "Νέο" title_with_count: one: "Νέα (1)" other: "Νέα ({{count}})" @@ -2008,20 +2096,20 @@ el: all: title: "Από πάντα" yearly: - title: "Ανά έτος" + title: "Ετήσια" quarterly: - title: "Τριμηνιαία έκδοση" + title: "Τριμηνιαία" monthly: title: "Ανά μήνα" weekly: - title: "Ανά εβδομάδα" + title: "Εβδομαδιαία" daily: title: "Ανά μέρα" all_time: "Από πάντα" this_year: "Χρόνος" this_quarter: "Τέταρτο" this_month: "Μήνας" - this_week: "εβδομάδα" + this_week: "Εβδομάδα" today: "Σήμερα" other_periods: "δες τα κορυφαία" browser_update: 'Δυστυχώς, το πρόγραμμα περιήγησής σου είναι πολύ παλιό για να δουλέψει σε αυτό τον ιστότοπο. Παρακαλώ ενημέρωσέ το.' @@ -2032,7 +2120,7 @@ el: lightbox: download: "λήψη" keyboard_shortcuts_help: - title: 'Συντόμευση Πληκτρολογίου' + title: 'Συντομεύσεις Πληκτρολογίου' jump_to: title: 'Μεταπήδηση σε' home: 'g, h Αρχική' @@ -2046,40 +2134,40 @@ el: messages: 'g, m Μηνύματα' navigation: title: 'Πλοήγηση' - jump: '# Πήγαινε στη δημοσίευση #' + jump: '# Πήγαινε στην ανάρτηση #' back: 'u Πίσω' up_down: 'kj Μετακίνηση επιλογής ↑ ↓' - open: 'o ή Εισαγωγή Άνοιξε το επιλεγμένο νήμα' + open: 'o ή Enter Άνοιξε το επιλεγμένο νήμα' next_prev: 'shift+j/shift+k Επόμενη/Προηγούμενη ενότητα' application: title: 'Εφαρμογή' - create: 'Δημιουργία νέου νήματος' + create: 'c Δημιουργία νέου νήματος' notifications: 'n Άνοιγμα ειδοποιήσεων' hamburger_menu: '=''Ανοιξε το μενού χάμπουρκερ' user_profile_menu: 'p Άνοιγμα του μενού του χρήστη' show_incoming_updated_topics: '. Εμφάνιση ενημερωμένων νημάτων' search: '/ Αναζήτηση' help: '? Εμβάνισε τη βοήθεια πληκτρολογίου' - dismiss_new_posts: 'x, r Αγνόησε νέα νήματα/αναρτήσεεις' - dismiss_topics: 'x, t Απόρριψη νημάτων' + dismiss_new_posts: 'x, r Απόρριψη Νέων/Αναρτήσεων' + dismiss_topics: 'x, t Απόρριψη Νημάτων' log_out: 'shift+z shift+z Αποσύνδεση' actions: title: 'Δράσεις' - bookmark_topic: 'Τοποθέτηση σελιδοδείκτη στο νήμα' + bookmark_topic: 'f Εναλλαγή σελιδοδείκτη στο νήμα' pin_unpin_topic: 'shift+p Καρφίτσωμα/Ξεκαρφίτσωμα νήματος' share_topic: 'shift+s Κοινοποίηση νήματος' - share_post: 's Κοινοποίηση δημοσίευσης' + share_post: 's Κοινοποίηση ανάρτησης' reply_as_new_topic: 't Απάντηση σαν συνδεδεμένο νήμα' reply_topic: 'shift+r Απάντηση στο νήμα' - reply_post: 'r Απάντηση στη δημοσίευση' - quote_post: 'q Παράθεση δημοσίευσης' - like: 'I "Μου αρέσει" η δημοσίευση' - flag: '! Επισήμανση δημοσίευσης' - bookmark: 'b Τοποθέτηση σελιδοδείκτη στη δημοσίευση' - edit: 'e Επεξεργασία δημοσίευσης' + reply_post: 'r Απάντηση στην ανάρτηση' + quote_post: 'q Παράθεση ανάρτησης' + like: 'I "Μου αρέσει" η ανάρτηση' + flag: '! Επισήμανση ανάρτησης' + bookmark: 'b Τοποθέτηση σελιδοδείκτη στην ανάρτηση' + edit: 'e Επεξεργασία ανάρτησης' delete: 'd Διαγραφή ανάρτησης' mark_muted: 'm, m Σίγαση νήματος' - mark_regular: 'm, r Κανονικό (προεπιλογμένο) νήμα' + mark_regular: 'm, r Κανονικό (προεπιλογή) νήμα' mark_tracking: 'm, t Παρακολούθηση νήματος' mark_watching: 'm, w Παρατήρηση νήματος' print: 'ctrl+p Εκτύπωση νήματος' @@ -2090,6 +2178,8 @@ el: granted_on: "Χορηγήθηκε στις %{date}" others_count: "Άλλοι με αυτό το παράσημο (%{count})" title: Παράσημα + allow_title: "Μπορείς να χρησιμοποιήσεις αυτό το παράσημο σαν τίτλο" + multiple_grant: "Μπορείς να το κερδίσεις πολλές φορές" badge_count: one: "1 Παράσημο" other: "%{count} Παράσημα" @@ -2111,100 +2201,99 @@ el: other: name: Άλλο posting: - name: Δημοσιεύεται + name: Αναρτάται google_search: |

    Αναζήτηση στο Google

    -

    tagging: all_tags: "Όλες οι Ετικέτες" selector_all_tags: "όλες οι ετικέτες" selector_no_tags: "καμία ετικέτα" - changed: "οι ετικέτες άλλαξαν" + changed: "αλλαγμένες ετικέτες:" tags: "Ετικέτες" choose_for_topic: "επίλεξε προαιρετικές ετικέτες για αυτό το νήμα" delete_tag: "Αφαίρεση Ετικέτας" delete_confirm: "Είσαι σίγουρος πως θέλεις να αφαιρέσεις την ετικέτα;" rename_tag: "Μετονομασία Ετικέτας" rename_instructions: "Επίλεξε ένα καινούριο όνομα για την ετικέτα:" - sort_by: "Ταξινόμηση κατά" - sort_by_count: "μετρώ" + sort_by: "Ταξινόμηση κατά:" + sort_by_count: "άθροισμα" sort_by_name: "όνομα" manage_groups: "Διαχείριση Ομάδων Ετικέτας" manage_groups_description: "Καθορισμός ομάδων για την οργάνωση ετικετών" filters: without_category: "%{filter} %{tag} νήματα" - with_category: "%{filter} %{tag} θέματα στην %{category}" + with_category: "%{filter} %{tag} νήματα στην %{category}" untagged_without_category: "%{filter} νήματα χωρίς ετικέτες" untagged_with_category: "%{filter} νήματα χωρίς ετικέτες σε %{category}" notifications: watching: title: "Παρακολούθηση" - description: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτή την ετικέτα. Θα λαμβάνεις ειδοποιήσεις όλων των καινούριων δημοσιεύσεων και νημάτων, και το πλήθος των μη αναγνωσμένων και καινούριων δημοσιεύσεων θα εμφανίζεται επίσης δίπλα στο νήμα." + description: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτή την ετικέτα. Θα λαμβάνεις ειδοποιήσεις όλων των καινούριων αναρτήσεων και νημάτων και το πλήθος των μη αναγνωσμένων και καινούριων δημοσιεύσεων θα εμφανίζεται επίσης δίπλα στο νήμα." watching_first_post: - title: "Παρακολούθηση πρώτης δημοσίευσης" - description: "Θα λαμβάνεις ειδοποιήσεις μόνο για την πρώτη δημοσίευση του κάθε καινούριου νήματος με αυτή την ετικέτα." + title: "Παρακολούθηση πρώτης ανάρτησης" + description: "Θα λαμβάνεις ειδοποιήσεις για την πρώτη ανάρτηση για κάθε νέο νήμα με αυτή την ετικέτα." tracking: title: "Παρακολουθείται" - description: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτή την ετικέτα. Το πλήθος των μη αναγνωσμένων δημοσιεύσεων θα εμφανίζεται δίπλα στο νήμα." + description: "Θα παρακολουθείς αυτόματα όλα τα νήματα με αυτή την ετικέτα. Το πλήθος των μη αναγνωσμένων αναρτήσεων θα εμφανίζεται δίπλα στο νήμα." regular: title: "Κανονικός" - description: "Θα λαμβάνεις ειδοποίηση εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε αυτή τη δημοσίευση." + description: "Θα λαμβάνεις ειδοποίηση εάν κάποιος αναφέρει το @όνομά σου ή απαντήσει σε αυτή τη ανάρτηση." muted: title: "Σίγαση" description: "Δε θα λαμβάνεις ειδοποιήσεις για οτιδήποτε σχετικά με τα καινούρια νήματα με αυτή την ετικέτα, και δε θα εμφανίζονται στην καρτέλα με τα μη αναγνωσμένα." groups: - title: "Ομάδες ετικέτας" - about: "Πρόσθεσε ετικέτες στις ομάδες για να τις διαχειριστείς με περισσότερη ευκολία." + title: "Ομάδες Ετικετών" + about: "Πρόσθεσε ετικέτες σε ομάδες για να τις διαχειριστείς με περισσότερη ευκολία." new: "Καινούρια ομάδα" tags_label: "Ετικέτες σε αυτή την ομάδα:" parent_tag_label: "Μητρική Ετικέτα:" parent_tag_placeholder: "Προαιρετικό" parent_tag_description: "Οι ετικέτες από αυτή την ομάδα δεν μπορούν να χρησιμοποιηθούν αν δεν είναι παρούσα η μητρική ετικέτα." one_per_topic_label: "Περιορισμός μιας ετικέτας για κάθε νήμα από αυτή την ομάδα" - new_name: "Νέα ομαδική ετικέτα" + new_name: "Νέα Ομάδα Ετικετών" save: "Αποθήκευση" delete: "Διαγραφή" - confirm_delete: "Είσαι βέβαιος πως θέλεις να διαγράψεις αυτή την ομαδική ετικέτα;" + confirm_delete: "Είσαι βέβαιος πως θέλεις να διαγράψεις αυτή την ομάδα ετικετών;" topics: none: unread: "Δεν υπάρχουν μη αναγνωσμένα νήματα." new: "Δεν υπάρχουν νέα νήματα." read: "Δεν έχεις διαβάσει κανένα νήμα ακόμα." - posted: "Δεν έχεις δημοσιεύσει σε κανένα νήμα ακόμα." + posted: "Δεν έχεις αναρτήσει σε κανένα νήμα ακόμα." latest: "Δεν υπάρχουν τελευταία νήματα." hot: "Δεν υπάρχουν δημοφιλή νήματα." bookmarks: "Δεν υπάρχουν νήματα με σελιδοδείκτη ακόμα." top: "Δεν υπάρχουν κορυφαία νήματα." search: "Δεν υπάρχουν αποτελέσματα από την αναζήτηση." bottom: - latest: "Δεν υπάρχουν πια τελευταία νήματα." + latest: "Δεν υπάρχουν άλλα τελευταία νήματα." hot: "Δεν υπάρχουν άλλα κορυφαία μυνήματα" - posted: "Δεν υπάρχουν πια νήματα που έχουν δημοσιευθεί." - read: "Δεν υπάρχουν πια διαβασμένα νήματα." - new: "Αυτά ήταν όλα τα νέα νήματα." - unread: "Δεν υπάρχουν πια μη αναγνωσμένα νήματα." - top: "Δεν υπάρχουν πια κορυφαία νήματα." - bookmarks: "Δεν υπάρχουν πια νήματα με σελιδοδείκτη." - search: "Δεν υπάρχουν αποτελέσματα για αυτή την αναζήτηση." + posted: "Δεν υπάρχουν άλλα νήματα που έχουν δημοσιευθεί." + read: "Δεν υπάρχουν άλλα διαβασμένα νήματα." + new: "Δεν υπάρχουν άλλα νέα νήματα." + unread: "Δεν υπάρχουν άλλα μη αναγνωσμένα νήματα." + top: "Δεν υπάρχουν άλλα κορυφαία νήματα." + bookmarks: "Δεν υπάρχουν άλλα νήματα με σελιδοδείκτη." + search: "Δεν υπάρχουν άλλα αποτελέσματα για αυτή την αναζήτηση." invite: - custom_message: "Κάνε την πρόσκλησή σου λίγο πιο προσωπική γράφοντας ένα/μια" + custom_message: "Κάνε την πρόσκλησή σου λίγο πιο προσωπική γράφοντας ένα" custom_message_link: "προσαρμοσμένο μήνυμα" custom_message_placeholder: "Πρόσθεσε το προσαρμοσμένο μήνυμά σου" - custom_message_template_forum: "Γεια, θα πρέπει να λάβεις μέρος σε αυτό το φόρουμ/χώρο συζητήσεων!" + custom_message_template_forum: "Γεια, θα πρέπει να λάβεις μέρος σε αυτό το χώρο συζητήσεων!" custom_message_template_topic: "Γεια, νομίζω ότι θα απολαύσεις αυτό το νήμα!" safe_mode: enabled: "Η λειτουργία ασφαλείας είναι ενεργοποιημένη, για να εξέλθεις από τη λειτουργία ασφαλείας κλείσε το παράθυρο περιήγησης" admin_js: type_to_filter: "γράψε εδώ για φιλτράρισμα..." admin: - title: 'Διαχειριστής του Discourse' + title: 'Διαχειριστής' moderator: 'Συντονιστής' dashboard: title: "Πίνακας ελέγχου" @@ -2226,7 +2315,7 @@ el: moderators: 'Συντονιστές: ' admins: 'Διαχειριστές:' blocked: 'Αποκλεισμένοι:' - suspended: 'Αποκλεισμένα:' + suspended: 'Αποβλημένοι:' private_messages_short: "Μνμτ." private_messages_title: "Μηνύματα" mobile_title: "Κινητό" @@ -2269,37 +2358,37 @@ el: agree_flag_restore_post_title: "Επανάφερε την ανάρτηση" agree_flag: "Συμφωνώ με τη επισήμανση " agree_flag_title: "Συμφωνώ με την επισήμανση και κρατάω την ανάρτηση αμετάβλητη" - defer_flag: "Παρέπεμψέ το" + defer_flag: "Αναβολή" defer_flag_title: "Αφαίρεσε αυτή την επισήμανση. Δεν χρειάζεται να γίνει κάποια ενέργεια αυτή τη στιγμή" - delete: "Σβήστη" - delete_title: "Σβήσε την ανάρτηση στην οποία αναφέρεται αυτή η επισήμανση." - delete_post_defer_flag: "Σβήσε την ανάρτηση και αγνόησε την επισήμανση" - delete_post_defer_flag_title: "Σβήσε την ανάρτηση. Αν είναι η πρώτη στο νήμα, σβήστο και αυτό" - delete_post_agree_flag: "Σβήσε την ανάρτηση και συμφώνησε με την επισήμανση" - delete_post_agree_flag_title: "Σβήσε την ανάρτηση. Αν είναι η πρώτη, σβήστο και αυτό" - delete_flag_modal_title: "Σβήσε και..." + delete: "Διαγραφή" + delete_title: "Διέγραψε την ανάρτηση στην οποία αναφέρεται αυτή η επισήμανση." + delete_post_defer_flag: "Διέγραψε την ανάρτηση και ανέβαλε την επισήμανση" + delete_post_defer_flag_title: "Διέγραψε την ανάρτηση. Αν είναι η πρώτη στο νήμα, διέγραψε και αυτό" + delete_post_agree_flag: "Διέγραψε την ανάρτηση και συμφώνησε με την επισήμανση" + delete_post_agree_flag_title: "Διέγραψε την ανάρτηση. Αν είναι η πρώτη στο νήμα, διέγραψε και αυτό" + delete_flag_modal_title: "Διέγραψε και..." delete_spammer: "Διαγραφή ανεπιθύμητου χρήστη" delete_spammer_title: "Διέγραψε το χρήστη και όλες τις αναρτήσεις και τα νήματά του" disagree_flag_unhide_post: "Διαφωνώ (εμφάνισε την ανάρτηση)" disagree_flag_unhide_post_title: "Απόρριψε όλες τις επισημάνσεις για την ανάρτηση και κάντη και πάλι ορατή" - disagree_flag: "Απόρριψη" - disagree_flag_title: "Απόρριψε την επισήμανση ως άκυρη ή εσφαλμένη" + disagree_flag: "Διαφωνώ" + disagree_flag_title: "Αρνήσου την επισήμανση ως άκυρη ή εσφαλμένη" clear_topic_flags: "Έγινε" clear_topic_flags_title: "Το νήμα ελέγχθηκε και τα προβλήματα επιλύθηκαν. Κάνε κλίκ στο «έγινε» για να αφαιρέσεις τις επισημάνσεις." more: "(περισσότερες απαντήσεις...)" dispositions: - agreed: "συμφωνώ" - disagreed: "απορρίπτω" - deferred: "αγνοώ" - flagged_by: "Επισημάνθηκε από τον" - resolved_by: "Επιλύθηκα από τον" - took_action: "Αντέδρασε" + agreed: "συμφώνησε" + disagreed: "διαφώνησε" + deferred: "ανέβαλε" + flagged_by: "Επισημάνθηκε από" + resolved_by: "Επιλύθηκε από" + took_action: "Ενήργησε" system: "Σύστημα" error: "Κάτι πήγε λάθος" reply_message: "Απάντηση" no_results: "Δεν υπάρχουν επισημάνσεις." topic_flagged: "Αυτό το νήμα έχει επισημανθεί." - visit_topic: "Επισκέψου το νήμα για να αντιδράσεις" + visit_topic: "Επισκέψου το νήμα για να ενεργήσεις" was_edited: "Η ανάρτηση έχει υποστεί επεξεργασία μετά την πρώτη επισήμανση" previous_flags_count: "Αυτό το νήμα έχει επισημανθεί ήδη {{count}} φορές" summary: @@ -2336,7 +2425,8 @@ el: add_members: "Προσθήκη μελών" custom: "Προσαρμοσμένο" bulk_complete: "Οι χρήστες έχουν προστεθεί στην ομάδα." - bulk: "Προσθήκη ομαδικής αποστολής αλληλογραφίας σε ομάδα" + bulk_complete_users_not_added: "Αυτοί οι χρήστες δεν προστέθηκαν (βεβαιώσου ότι έχουν λογαριασμό χρήστη):" + bulk: "Μαζική Προσθήκη σε Ομάδα" bulk_paste: "Κάνε επικόλληση μια λίστα με ονόματα χρηστών η διευθύνσεις ηλεκτρονικών μηνυμάτων, ένα σε κάθε σειρά:" bulk_select: "(επέλεξε μια ομάδα)" automatic: "Αυτόματα" @@ -2351,7 +2441,7 @@ el: none_selected: "Επέλεξε ομάδα για να ξεκινήσεις" no_custom_groups: "Δημιουργία νέας προσαρμοσμένης ομάδας" api: - generate_master: "Δημιουγία κλειδιού Master API" + generate_master: "Δημιουργία Master API Key" none: "Δεν υπάρχουν ενεργά κλειδιά API αυτή την στιγμή." user: "Χρήστης" title: "API" @@ -2359,16 +2449,16 @@ el: generate: "Δημιουργία" regenerate: "Αναδημιουγία" revoke: "Ανάκληση" - confirm_regen: "Θες στ' αλήθεια να αντικαταστήσεις το κλειδί API με ένα καινούργιο;" - confirm_revoke: "Θες στ' αλήθεια να ανακαλέσεις το κλειδί API;" + confirm_regen: "Θέλεις να αντικαταστήσεις το κλειδί API με ένα καινούργιο;" + confirm_revoke: "Θέλεις σίγουρα να ανακαλέσεις το κλειδί API;" info_html: "Το κλειδί API σου δίνει τη δυνατότητα να δημιουργήσεις και να επεξεργαστείς νήματα μέσω κλήσεων JSON." all_users: "Όλοι οι χρήστες" - note_html: "Αυτό το κλειδί κράτα το κρυφό. Όλοι οι χρήστες που το κατέχουν μπορούν να δημιουργήσουν αναρτήσεις υποδυόμενοι οποιονδήποτε χρήστη χωρίς κανέναν περιορισμό." + note_html: "Κράτα αυτό το κλειδί κρυφό. Όλοι οι χρήστες που το κατέχουν μπορούν να δημιουργήσουν αναρτήσεις υποδυόμενοι οποιονδήποτε χρήστη χωρίς κανέναν περιορισμό." web_hooks: title: "Webhooks" none: "Δεν υπάρχουν Webhooks αυτή τη στιγμή." - instruction: "Τα \"Webhooks\" επιτρέπουν στην Discourse να ειδοποιεί εξωτερικές υπηρεσίες όταν ένα συμβάν λαμβάνει χώρα στον ιστότοπό σας. Όταν το \"webhook\" τεθεί σε λειτουργία θα στείλει ένα αίτημα τύπου POST στα URLs τα οποία δόθηκαν. " - detailed_instruction: "Ένα αίτημα δημοσίευσης θα σταλεί στο παρεχόμενο URL όταν συμβεί η εκδήλωση που έχει επιλεγεί." + instruction: "Τα \"Webhooks\" επιτρέπουν στην ιστοσελίδα να ειδοποιεί εξωτερικές υπηρεσίες όταν ένα γεγονός συμβεί στον ιστότοπό σας. Όταν το \"webhook\" τεθεί σε λειτουργία θα στείλει ένα αίτημα τύπου POST στα URLs τα οποία δόθηκαν. " + detailed_instruction: "Ένα POST request θα σταλεί στο παρεχόμενο URL όταν συμβεί το γεγονός που έχει επιλεγεί." new: "Νέο Webhook" create: "Δημιουργία" save: "Αποθήκευση" @@ -2376,66 +2466,67 @@ el: description: "Περιγραφή" controls: "Έλεγχοι" go_back: "Πίσω στη λίστα" - payload_url: "Φορτίο URL" + payload_url: "Payload URL" payload_url_placeholder: "https://example.com/postreceive" - warn_local_payload_url: "Φαίνεται ότι προσπαθείς να εγκαταστήσεις το webhook σε ένα τοπικό url. Η εκδήλωση που διανέμεται σε μια τοπική διεύθυνση ενδέχεται να προκαλέσει παρενέργειες ή απρόσμενες συμπεριφορές. Συνέχεια;" + warn_local_payload_url: "Φαίνεται ότι προσπαθείς να εγκαταστήσεις το webhook σε ένα τοπικό url. Το γεγονός που διανέμεται σε μια τοπική διεύθυνση ενδέχεται να προκαλέσει παρενέργειες ή απρόσμενες συμπεριφορές. Συνέχεια;" secret_invalid: "Ο μυστικός κωδικός δεν πρέπει να έχει κενούς χαρακτήρες." secret_too_short: "Ο μυστικός κωδικός πρέπει να έχει τουλάχιστον 12 χαρακτήρες." secret_placeholder: "Χρήση ενός προαιρετικού string, για τη δημιουργία υπογραφής. " - event_type_missing: "Πρέπει να εγκαταστήσεις τουλάχιστον ένα είδος εκδήλωσης." + event_type_missing: "Πρέπει να ρυθμίσεις τουλάχιστον ένα είδος γεγονότος." content_type: "Είδος περιεχομένου" secret: "Μυστικός κωδικός" + event_chooser: "Ποια γεγονότα θα ενεργοποιήσουν αυτό το webhook?" wildcard_event: "Στείλε μου τα πάντα." - individual_event: "Επίλεξε ατομικές εκδηλώσεις." - verify_certificate: "Έλεγχος TLS για το πιστοποιητικό του URL του payload. " - active: "Ενργός" - active_notice: "Θα διανέμουμε λεπτομέρεις για την εκδήλωση όταν αυτή θα λαμβάνει μέρος" - categories_filter_instructions: "Τα σχετικά webhooks θα τεθούν σε λειτουργεία μόνο εαν το συμβάν σχετίζεται με τις προσδιορισμένες κατηγορίες. Για να ενεργοποιήσετε τα webhooks για όλες της κατηγορίες, αφήστε κενό. " + individual_event: "Επίλεξε μεμονομένα γεγονότα." + verify_certificate: "Έλεγχος TLS certificate για το payload url" + active: "Ενεργός" + active_notice: "Θα στείλουμε λεπτομέρεις για το γεγονός όταν αυτό συμβεί" + categories_filter_instructions: "Τα σχετικά webhooks θα τεθούν σε λειτουργεία μόνο εαν το γεγονός σχετίζεται με τις προσδιορισμένες κατηγορίες. Για να ενεργοποιήσετε τα webhooks για όλες της κατηγορίες, αφήστε κενό. " categories_filter: "Ενεργοποιημένες Κατηγορίες" - groups_filter_instructions: "Τα σχετικά webhooks θα τεθούν σε λειτουργεία μόνο εαν το συμβάν σχετίζεται με τις προσδιορισμένες όμάδες. Για να ενεργοποιήσετε τα webhooks για όλες της ομάδες, αφήστε κενό. " + groups_filter_instructions: "Τα σχετικά webhooks θα τεθούν σε λειτουργεία μόνο εαν το γεγονός σχετίζεται με τις προσδιορισμένες όμάδες. Για να ενεργοποιήσετε τα webhooks για όλες της ομάδες, αφήστε κενό. " groups_filter: "Ενεργοποιημένες Ομάδες" delete_confirm: "Διαγραφή αυτού του webhook;" topic_event: - name: "Εκδήλωση νήματος" + name: "Γεγονός Νήματος" details: "Όταν υπάρχει ένα καινούριο νήμα, αναθεωρημένο, αλλαγμένο ή διαγραμμένο." post_event: - name: "Δημοσίευση Εκδήλωσης" + name: "Post Event" details: "Όταν υπάρχει μια καινούρια απάντηση, επεξεργασμένη, διαγραμμένη ή ανακτημένη." user_event: - name: "Εκδήλωση Χρήστη" + name: "Γεγονός Χρήστη" details: "Όταν ένας χρήστης δημιουργείται, εγκρίνεται ή ενημερώνεται." delivery_status: title: "Κατάσταση διανομής" - inactive: "Ανενεργός" + inactive: "Ανενεργό" failed: "Απέτυχε" successful: "Επιτυχής" events: - none: "Δεν υπάρχουν σχετικές εκδηλώσεις." + none: "Δεν υπάρχουν σχετικά γεγονότα." redeliver: "Διανομή ξανά" incoming: one: "Υπάρχει μία καινούρια εκδήλωση." - other: "Υπάρχουν {{count}} καινούριες εκδηλώσεις." + other: "Υπάρχουν {{count}} καινούρια γεγονότα." completed_in: one: "Ολοκληρώνεται σε 1 δευτερόλεπτο." - other: "Ολοκληρώνεται σε {{count}} δευτερόλεπτα." + other: "Ολοκληρώθηκε σε {{count}} δευτερόλεπτα." request: "Αίτημα" response: "Απάντηση" redeliver_confirm: "Είστε σιγουρος/η ότι θέλετε να παραδώσετε εκ νέου το ίδιο payload;" - headers: "Επικεφαλίδες" - payload: "Φορτίο επί πληρωμή" - body: "Σώμα" + headers: "Headers" + payload: "Payload" + body: "Body" go_list: "Πήγαινε στη λίστα" go_details: "Επεξεργασία Webhook" - go_events: "Πήγαινε στις εκδηλώσεις" + go_events: "Πήγαινε στα γεγονότα" ping: "Ping" - status: "Κωδικός κατάστασης" - event_id: "Ταυτότητα" + status: "Status Code" + event_id: "ID" timestamp: "Δημιουργήθηκε" completion: "Ώρα ολοκλήρωσης" - actions: "Δράσεις" + actions: "Ενέργειες" plugins: - title: "Πρόσθετα" - installed: "Εγκατεστημένα πρόσθετα" + title: "Plugins" + installed: "Εγκατεστημένα Plugins" name: "Όνομα" none_installed: "Δεν έχεις εγκαταστήσει πρόσθετα" version: "Έκδοση" @@ -2517,33 +2608,106 @@ el: title: "Προσαρμογή" long_title: "Προσαρμογές ιστότοπου" preview: "προεπισκόπηση " + explain_preview: "Δες την ιστοσελίδα με αυτό το θέμα ενεργό" save: "Αποθήκευση" new: "Νέο" new_style: "Νέο στυλ" import: "Εισαγωγή" delete: "Σβήσιμο" - about: "Προσάρμοσε τα αρχεία στυλ CSS και την κεφαλίδα HTML του ιστότοπου. Πρόσθεσε μια προσαρμογή για να ξεκινήσεις." + delete_confirm: "Διαγραφή αυτού του θέματος;" + about: "Προσάρμοσε τα CSS stylesheets και τα HTML headers της ιστοσελίδας. Πρόσθεσε μια προσαρμογή για να ξεκινήσεις." color: "Χρώμα" opacity: "Αδιαφάνεια" copy: "Αντιγραφή" + copy_to_clipboard: "Αντιγραφή στο Clipboard" + copied_to_clipboard: "Αντιγράφτηκε στο Clipboard" + copy_to_clipboard_error: "Σφάλμα αντιγραφής δεδομένων στο Clipboard" + theme_owner: "Μη επεξεργάσιμο, ανήκει σε:" email_templates: - title: "Τύπος ηλεκτρονικού ταχυδρομείου" + title: "Πρότυπα Ηλεκτρονικού Ταχυδρομείου" subject: "Θέμα" multiple_subjects: "Αυτό το πρότυπο ηλεκτρονικού μηνύματος έχει πολλά θέματα." body: "Σώμα" - none_selected: "Επίλεξε ένα πρότυπο ηλεκτρονικού μηνύματος να ξεκινήσεις την επεξεργασία." + none_selected: "Επίλεξε ένα πρότυπο ηλεκτρονικού μηνύματος για να ξεκινήσεις την επεξεργασία." revert: "Επαναφορά αλλαγών" revert_confirm: "Είσαι βέβαιος πως θέλεις να επαναφέρεις τις αλλαγές;" + theme: + import_theme: "Εισαγωγή Θέματος" + customize_desc: "Προσαρμογή:" + title: "Θέματα" + long_title: "Τροποποίησε τα χρώματα, CSS και HTML περιεχόμενο της ιστοσελίδας" + edit: "Επεξεργασία" + edit_confirm: "Αυτό είναι ένα απομακρυσμένο θέμα. Αν επεξεργαστείτε το CSS/HTML οι αλλαγές σας θα διαγραφούν την επόμενη φορά που θα ενημερώσετε το θέμα." + common: "Κοινό" + desktop: "Desktop" + mobile: "Mobile" + preview: "Προεπισκόπηση" + is_default: "Το θέμα είναι ενεργό εξ ορισμού" + user_selectable: "Το θέμα μπορεί να επιλεχθεί από τους χρήστες" + color_scheme: "Χρωματική Παλέτα" + color_scheme_select: "Επιλέξτε τα χρώματα που θα χρησιμοποιούνται από το θέμα" + custom_sections: "Προσαρμοσμένα τμήματα:" + theme_components: "Στοιχεία Θέματος" + uploads: "Μεταφορτώσεις" + no_uploads: "Μπορείτε να μεταφορτώσετε στοιχεία σχετικά με το θέμα σας, όπως γραμματοσειρές και εικόνες." + add_upload: "Προσθήκη Αρχείου" + upload_file_tip: "Επιλέξτε στοιχείο για μεταφόρτωση (png, woff2, etc...)" + variable_name: "SCSS var name:" + upload: "Μεταφόρτωση" + child_themes_check: "Το θέμα περιλαμβάνει άλλα υποθέματα" + css_html: "Custom CSS/HTML" + edit_css_html: "Edit CSS/HTML" + edit_css_html_help: "Δεν επεξεργαστήκατε το CSS ή το HTML" + delete_upload_confirm: "Διαγραφή αυτής της μεταφόρτωσης; (Το CSS του θέματος ενδέχεται να σταματήσει να λειτουργεί!)" + import_web_tip: "Αποθηκευτικός χώρος που περιέχει το θέμα" + import_file_tip: ".dcstyle.json αρχείο που περιέχει το θέμα" + about_theme: "Σχετικά με το Θέμα" + license: "Άδεια Χρήσης" + component_of: "Το θέμα είναι στοιχείο από:" + update_to_latest: "Ενημέρωση στην τελευταία έκδοση" + check_for_updates: "Έλεγχος για ενημερώσεις" + updating: "Ενημέρωση..." + up_to_date: "Το θέμα είναι ενημερωμένο, τελευταίος έλεγχος:" + add: "Προσθήκη" + commits_behind: + one: "Το θέμα είναι 1 έκδοση πίσω!" + other: "Το θέμα είναι {{count}} εκδόσεις πίσω!" + scss: + text: "CSS" + title: "Καταχώρησε custom CSS, γίνονται δεκτά όλα τα valid CSS και SCSS styles" + header: + text: "Header" + title: "Καταχώρησε HTML το οποίο θα εμφανίζεται πάνω από το header της ιστοσελίδας" + after_header: + text: "Μετά το Header" + title: "Καταχώρησε HTML το οποίο θα εμφανίζεται σε όλες τις σελίδες μετά το header" + footer: + text: "Footer" + title: "Καταχώρησε HTML το οποίο θα εμφανίζεται στο footer" + embedded_scss: + text: "Embedded CSS" + title: "Καταχώρησε custom CSS to deliver with embedded version of comments" + head_tag: + text: "" + title: "HTML το οποίο θα προστίθεται πριν το tag" + body_tag: + text: "" + title: "HTML το οποίο θα προστίθεται πριν το tag" colors: + select_base: + title: "Επέλεξε βασική χρωματική παλέτα" + description: "Βασική παλέτα:" title: "Χρώματα" - long_title: "Χρωματικά σύνολα" - new_name: "Νέο χρωματικό σύνολο" + edit: "Επεξεργασία Χρωματικής Παλέτας" + long_title: "Χρωματική Παλέτα" + about: "Αλλάξτε τα χρώματα που χρησιμοποιούν τα θέματά σας. Δημιουργήστε μια νέα χρωματική παλέτα για να ξεκινήσετε." + new_name: "Νέα Χρωματική Παλέτα" copy_name_prefix: "Αντίγραφο του" - delete_confirm: "Να διαγραφεί αυτό το χρωματικό σύνολο;" + delete_confirm: "Να διαγραφεί αυτή η χρωματική παλέτα;" undo: "αναίρεση" undo_title: "Να αναιρεθούν οι αλλαγές που έγιναν σε αυτό το χρώμα από την τελευταία αποθήκευσή του και έπειτα." revert: "απόρριψη" - revert_title: "Επανέφερε αυτό το χρώμα στο προκαθορισμένο χρωματικό σύνολο του Discourse." + revert_title: "Επανέφερε αυτό το χρώμα στην προκαθορισμένη χρωματική παλέτα του Discourse." primary: name: 'κύριο' description: 'Τα περισσότερα κείμενα, εικονίδια και πλαίσια.' @@ -2575,7 +2739,7 @@ el: name: 'αγάπη' description: "Το χρώμα του κουμπιού «μου αρέσει»." email: - title: "Μήνυμα Ηλεκτρονικού Ταχυδρομείου" + title: "Emails" settings: "Ρυθμίσεις" templates: "Πρότυπα" preview_digest: "Προεπισκόπηση σύνοψης" @@ -2603,7 +2767,7 @@ el: sending_email: "Αποστολή ηλεκτρονικού μηνύματος..." format: "Μορφή" html: "html" - text: "κείμενο" + text: "text" last_seen_user: "Τελευταίος χρήστης:" no_result: "Δεν βρέθηκαν αποτελέσματα για περίληψη." reply_key: "Κλειδί απάντησης" @@ -2613,12 +2777,12 @@ el: to_addresses: "Προς" cc_addresses: "Cc" subject: "Θέμα" - error: "Λάθος" + error: "Σφάλμα" none: "Δε βρέθηκε κανένα εισερχόμενο μήνυμα." modal: title: "Λεπτομέρειες εισερχόμενων μηνυμάτων" error: "Σφάλμα" - headers: "Επικεφαλίδες" + headers: "Headers" subject: "Θέμα" body: "Σώμα" rejection_message: "Μήνυμα απόρριψης" @@ -2639,22 +2803,24 @@ el: skipped_reason_placeholder: "λόγος" logs: title: "Αρχεία καταγραφής" - action: "Δράση" + action: "Ενέργεια" created_at: "Δημιουργήθηκε" - last_match_at: "Τελευταίο αποτέλεσμα" - match_count: "Αποτελέσματα" + last_match_at: "Τελευταία Αντιστοίχιση" + match_count: "Αντιστοιχίσεις" ip_address: "IP" topic_id: "Ταυτότητα νήματος" post_id: "Ταυτότητα ανάρτησης" category_id: "Ταυτότητα Κατηγορίας" - delete: 'Σβήσιμο' + delete: 'Διαγραφή' edit: 'Επεξεργασία' save: 'Αποθήκευση' screened_actions: block: "αποκλεισμός" do_nothing: "μην κάνεις τίποτα" staff_actions: - title: "Δράσεις συνεργατών" + all: "όλα" + filter: "Φίλτρο:" + title: "Ενέργειες Συνεργατών" clear_filters: "Δείξτα όλα" staff_user: "Συνεργάτης" target_user: "Χρήστη που τον αφορά" @@ -2662,29 +2828,31 @@ el: when: "Πότε" context: "Πλαίσιο αναφοράς" details: "Λεπτομέρειες" - previous_value: "Παλιότερο" + previous_value: "Προηγούμενο" new_value: "Νέο" diff: "Σύγκριση" show: "Δείξε" modal_title: "Λεπτομέρειες" no_previous: "Δεν υπάρχει προηγούμενη τιμή." - deleted: "Καμία νέα τιμή. Η εγγραφή σβήστηκε." + deleted: "Δεν υπάρχει νέα τιμή. Η εγγραφή διεγράφη." actions: delete_user: "διαγραφή χρήστη" change_trust_level: "αλλαγή επιπέδου εμπιστοσύνης" change_username: "αλλαγή χρηστώνυμου" change_site_setting: "αλλαγή ρυθμίσεων ιστότοπου" - change_site_text: "αλλαγή θέσης κειμένου" - suspend_user: "Αποκλεισμός του χρήστη" - unsuspend_user: "Αναίρεση αποκλεισμού χρήστη" - grant_badge: "Απονομή παράσημου" + change_theme: "αλλαγή θέματος" + delete_theme: "διαγραφή θέματος" + change_site_text: "αλλαγή κειμένου ιστοσελίδας" + suspend_user: "αποβολή του χρήστη" + unsuspend_user: "αναίρεση αποβολής χρήστη" + grant_badge: "απονομή παράσημου" revoke_badge: "απόσυρση παράσημου" - check_email: "Κοίτα στο ηλεκτρονικό σου ταχυδρομείο" - delete_topic: "Σβήσε το νήμα" - delete_post: "Σβήσε την ανάρτηση" + check_email: "έλεγχος email" + delete_topic: "διαγραφή νήματος" + delete_post: "διαγραφή ανάρτησης" impersonate: "υποδύσου" anonymize_user: "ανωνυμοποίηση χρήστη" - roll_up: "Τυλιγμένα IP εμπόδια" + roll_up: "roll up IP blocks" change_category_settings: "αλλαγή ρυθμίσεων κατηγορίας" delete_category: "διαγραφή κατηγορίας" create_category: "δημιουργία κατηγορίας" @@ -2694,10 +2862,10 @@ el: revoke_admin: "ανάκληση διαχειριστή" grant_moderation: "χορήγηση συντονιστή" revoke_moderation: "ανάκληση συντονιστή" - backup_create: "δημιουργία αντίγραφου" - deleted_tag: "η ετικέτα διαγράφηκε" - renamed_tag: "η ετικέτα μετονομάστηκε" - revoke_email: "Ανακάλεσε το email" + backup_create: "δημιουργία αντιγράφου" + deleted_tag: "διεγραμμένη ετικέτα" + renamed_tag: "μετονομασμένη ετικέτα" + revoke_email: "ανακάλεσε το email" lock_trust_level: "κλείδωσε το επίπεδο εμπιστοσύνης" unlock_trust_level: "ξεκλείδωσε το επίπεδο εμπιστοσύνης" activate_user: "ενεργοποίηση χρήστη" @@ -2705,19 +2873,21 @@ el: change_readonly_mode: "αλλαγή λειτουργίας \"μόνο-ανάγνωση\"" backup_download: "λήψη αντίγραφου" backup_destroy: "καταστροφή αντίγραφου" + reviewed_post: "έλεγξε ανάρτηση" + custom_staff: "προσαρμοσμένη ενέργεια προσθέτου" screened_emails: - title: "Ελεγμένες διευθύνσεις ηλεκτρονικού ταχυδρομείου" + title: "Ελεγχόμενες διευθύνσεις ηλεκτρονικού ταχυδρομείου" description: "Όταν κάποιος φτιάξει έναν λογαριασμό, οι παρακάτω διευθύνσεις ηλεκτρονικού ταχυδρομείου θα ελεγχθούν και η εγγραφή θα αποκλειστεί, ή θα συμβεί κάποια άλλη πράξη." email: "Διεύθυνση ηλεκτρονικού ταχυδρομείου" actions: allow: "Επίτρεψε" screened_urls: - title: "Ελεγμένα URL" + title: "Ελεγχόμενα URL" description: "Τα URL της λίστας χρησιμοποιήθηκαν σε αναρτήσεις από χρήστες που έχουν εντοπιστεί πως αναρτούν ανεπιθύμητα μηνύματα." url: "URL" domain: "Domain" screened_ips: - title: "Ελεγμένες IP" + title: "Ελεγχόμενες IP" description: 'Διευθύνσεις IP που παρακολουθούνται. Χρησιμοποίησε το «επίτρεψε» για να τις βάλεις στη λίστα επιτρεπόμενων.' delete_confirm: "Σίγουρα θέλεις να αφαιρέσεις τον κανόνα για τη διεύθυνση %{ip_address};" roll_up_confirm: "Σίγουρα θες να συνοψίσεις σε υποδίκτυα τις συχνά αποκλεισμένες διευθύνσεις IP;" @@ -2729,7 +2899,7 @@ el: allow_admin: "Να επιτρέπεται στο διαχειριστή" form: label: "Νέο:" - ip_address: "διεύθυνση IP" + ip_address: "IP διεύθυνση" add: "Προσθήκη" filter: "Αναζήτηση" roll_up: @@ -2737,25 +2907,49 @@ el: title: "Δημιουργεί νέες εγγραφές για τον αποκλεισμό υποδικτύων αν υπάρχουν τουλάχιστον 'min_ban_entries_for_roll_up' εγγραφές." logster: title: "Αρχεία καταγραφής σφαλμάτων" + watched_words: + title: "Εποπτευόμενες Λέξεις" + search: "αναζήτηση" + clear_filter: "Καθαρισμός" + show_words: "προβολή λέξεων" + word_count: + one: "1 λέξη" + other: "%{count} λέξεις" + actions: + block: 'Αποκλεισμός' + censor: 'Λογοκρισία' + require_approval: 'Απαιτεί Έγκριση' + flag: 'Σήμανση' + action_descriptions: + block: 'Αποτροπή της καταχώρησης αναρτήσεων που περιέχουν αυτές τις λέξεις. Ο χρήστης θα ενημερωθεί με μήνυμα λάθους όταν προσπαθήσει να καταχωρήσει την αναρτηση.' + censor: 'Επέτρεψε τις αναρτήσεις με αυτές τις λέξεις, αλλά αντικατέστησε τις λογοκριμένες λέξεις με άλλους χαρακτήρες.' + require_approval: 'Οι αναρτήσεις που περιέχουν αυτές τις λέξεις, χρειάζονται έγκριση από το προσωπικό πριν αναρτηθούν.' + flag: 'Επέτρεψε τις αναρτήσεις που περιέχουν αυτές τις λέξεις, αλλά επισήμανέ τες ως ανάρμοστες για έλεγχο από συντονιστή.' + form: + label: 'Νέα Λέξη:' + add: 'Προσθήκη' + success: 'Επιτυχία' + upload: "Μεταφόρτωση" + upload_successful: "Επιτυχημένη μεταφόρτωση. Οι λέξεις προστέθηκαν." impersonate: title: "Να υποδυθώ" help: "Χρησιμοποίησε αυτό το εργαλείο για να εντοπίσεις λάθη υποδυόμενος κάποιον άλλο χρήστη. Θα πρέπει να αποσυνδεθείς όταν τελειώσεις." not_found: "Ο χρήστης δεν μπορεί να βρεθεί" - invalid: "Συγγνώμη, δεν μπορείς να εκπροσωπήσεις αυτό τον χρήστη" + invalid: "Συγγνώμη, δεν μπορείς να υποδυθείς αυτό τον χρήστη" users: title: 'Χρήστες' create: 'Προσθήκη διαχειριστή' last_emailed: "Τελευταίο μήνυμα ηλεκτρονικού ταχυδρομείου" not_found: "Λυπούμαστε αλλά αυτό το χρηστώνυμο δεν υπάρχει στο σύστημα." id_not_found: "Λυπούμαστε αλλά αυτή η ταυτότητα χρήστη δεν υπάρχει στο σύστημα." - active: "Ενεργά" + active: "Ενεργοί" show_emails: "Δείξε τα μηνύματα ηλεκτρονικού ταχυδρομείου" nav: new: "Νέο" active: "Ενεργό" pending: "Εκκρεμεί" staff: 'Προσωπικό' - suspended: 'Αποκλεισμένοι' + suspended: 'Αποβλημένοι' blocked: 'Αποκλεισμένο' suspect: 'Ύποπτο' approved: "Εγκεκριμένο;" @@ -2778,38 +2972,38 @@ el: admins: 'Διαχειριστές' moderators: 'Συντονιστές' blocked: 'Αποκλεισμένοι χρήστες' - suspended: 'Απενεργοποιημένοι Χρήστες' + suspended: 'Αποβλημένοι Χρήστες' suspect: 'Ύποπτοι χρήστες' reject_successful: one: "Επιτυχώς απορρίφθηκε 1 χρήστης." other: "Επιτυχώς απορρίφθησαν %{count} χρήστες." reject_failures: one: "Αποτυχία απόρριψης 1 χρήστη." - other: "Αποτυχία απόρριψης %{count} χρηστών." + other: "Αποτυχία απόρριψης %{count}χρήστες." not_verified: "Μη επαληθευμένος" check_email: title: "Εμφάνισε το email αυτού του χρήστη" text: "Εμφάνιση" user: - suspend_failed: "Κάποιο λάθος εμφανίστηκε κατά την αναστολή αυτού του χρήστη {{error}}" - unsuspend_failed: "Κάτι πήγε στραβά κατά την αναίρεση του αποκλεισμού αυτού του χρήστη {{error}}" - suspend_duration: "Για πόσο χρονικό διάστημα θα γίνει αναστολή του συγκεκριμένου χρήστη;" + suspend_failed: "Κάποιο λάθος εμφανίστηκε κατά την αποβολή αυτού του χρήστη {{error}}" + unsuspend_failed: "Κάτι πήγε στραβά κατά την αναίρεση της αποβολής αυτού του χρήστη {{error}}" + suspend_duration: "Πόσο χρονικό διάστημα θα κρατήσει η αποβολή του χρήστη;" suspend_duration_units: "(ημέρες)" - suspend_reason_label: "Γιατί απενεργοποιείες; Αυτό το κείμενο θα είναι ορατό σε όλους στη σελίδα προφίλ αυτού του χρήστη και θα εμφανίζεται στο χρήστη όταν αυτός προσπαθήσει να συνδεθεί. Φρόντισε να είναι σύντομο." + suspend_reason_label: "Γιατί αποβάλεις τον χρήστη; Αυτό το κείμενο θα είναι ορατό σε όλους στη σελίδα προφίλ αυτού του χρήστη και θα εμφανίζεται στο χρήστη όταν αυτός προσπαθήσει να συνδεθεί. Φρόντισε να είναι σύντομο." suspend_reason: "Αιτία" - suspended_by: "Απενεργοποιήθηκε από τον/την" + suspended_by: "Αποβλήθηκε από τον/την" delete_all_posts: "Σβήσε όλες τις αναρτήσεις" - delete_all_posts_confirm_MF: "Είσαι έτοιμος να σβήσεις %{posts} αναρτήσεις και %{topics} νήματα. Επιβεβαίωση;" - suspend: "Απενεργοποίηση" - unsuspend: "Αναίρεση απενεργοποίησης" - suspended: "Απενεργοποιημένος;" + delete_all_posts_confirm_MF: "Πρόκειται να διαγράψεις {POSTS, plural, one {1 ανάρτηση} other {# αναρτήσεις}} and {TOPICS, plural, one {1 νήμα} other {# νήματα}}. Είσαι σίγουρος;" + suspend: "Αποβολή" + unsuspend: "Αναίρεση αποβολής" + suspended: "Αποβλημένος;" moderator: "Συντονιστής;" admin: "Διαχειριστής;" blocked: "Αποκλείστηκε; " - staged: "Σταδιακή?" + staged: "Στάδιο Μετάβασης;" show_admin_profile: "Διαχείριση" refresh_browsers: "Να ανανεωθεί αναγκαστικά η σελίδα" - refresh_browsers_message: "Το μήνυμα στάλθηκε σε όλους τους πελάτες!" + refresh_browsers_message: "Το μήνυμα στάλθηκε σε όλους τους χρήστες!" show_public_profile: "Εμφάνισε το Δημόσιο Προφίλ" impersonate: 'Να υποδυθώ' action_logs: "Καταγραφές ενεργειών" @@ -2818,8 +3012,9 @@ el: logged_out: "Ο χρήστης αποσυνδέθηκε από όλες τις συσκευές" revoke_admin: 'Ανάκληση δικαιωμάτων διαχειριστή' grant_admin: 'Απόδοση δικαιωμάτων διαχειριστή' + grant_admin_confirm: "Έχουμε στείλει ένα email για να επιβεβαιώσουμε τον νέο διαχειριστή. Παρακαλούμε ανοίξτε το και ακολουθήστε τις οδηγίες." revoke_moderation: 'Ανάκληση δικαιωμάτων συντονιστή' - grant_moderation: 'Ανάκληση δικαιωμάτων συντονιστή' + grant_moderation: 'Απόδοση δικαιωμάτων συντονιστή' unblock: 'Αναίρεση αποκλεισμού' block: 'Αποκλεισμός' reputation: Φήμη @@ -2868,24 +3063,25 @@ el: activate_failed: "Παρουσιάστηκε πρόβλημα κατά την ενεργοποίηση του χρήστη." deactivate_account: "Απενεργοποίηση του λογαριασμού" deactivate_failed: "Παρουσιάστηκε πρόβλημα κατά την απενεργοποίηση του χρήστη." - unblock_failed: 'Παρουσιάστηκε πρόβλημα κατά την ενεργοποίηση του χρήστη.' - block_failed: 'Παρουσιάστηκε πρόβλημα κατά την απενεργοποίηση του χρήστη.' - block_confirm: 'Είσαι βέβαιος πως θέλεις να αποκλείσεις αυτόν τον χρήστη; Δε θα μπορεί να δημιουργήσει καινούρια νήματα ή δημοσιεύσεις.' + unblock_failed: 'Παρουσιάστηκε πρόβλημα κατά την άρση αποκλεισμού του χρήστη.' + block_failed: 'Παρουσιάστηκε πρόβλημα κατά τον αποκλεισμό του χρήστη.' + block_confirm: 'Είσαι βέβαιος πως θέλεις να αποκλείσεις αυτόν τον χρήστη; Δε θα μπορεί να δημιουργήσει καινούρια νήματα ή αναρτήσεις.' block_accept: 'Ναι, αποκλεισμός του χρήστη' - bounce_score: "Βαθμολόγηση επιστρεφόμενων" + bounce_score: "Bounce Score" reset_bounce_score: label: "Επαναφορά" title: "Επαναφορά βαθμολόγησης επιστρεφόμενων πίσω στο 0" + visit_profile: "Επισκεφθείτε την σελίδα προτιμήσεων του χρήστη για να επεξεργαστείτε το προφίλ του" deactivate_explanation: "Ένας απενεργοποιημένος χρήστης πρέπει να επιβεβαιώσει ξανά τη διεύθυνση του ηλεκτρονικού του ταχυδρομείου." - suspended_explanation: "Ένας χρήστης που έχει απενεργοποιηθεί δεν γίνεται να συνδεθεί." - block_explanation: "Ένας απενεργοποιημένος χρήστης δεν μπορεί να δημοσιεύει ή να δημιουργεί νήματα" - staged_explanation: "Ένας staged χρήστης μπορεί να κάνει αναρτήσεις μέσω ηλεκτρονικου ταχυδρομίου και σε συγκεκριμένα θέματα. " + suspended_explanation: "Ένας χρήστης που έχει αποβληθεί δεν γίνεται να συνδεθεί." + block_explanation: "Ένας απενεργοποιημένος χρήστης δεν μπορεί να αναρτήσει ή να δημιουργεί νήματα" + staged_explanation: "Ένας χρήστης σε στάδιο μετάβασης μπορεί να κάνει αναρτήσεις μέσω ηλεκτρονικου ταχυδρομίου και σε συγκεκριμένα θέματα. " bounce_score_explanation: none: "Κανένα επιστρεφόμενο μήνυμα δεν ελήφθη από αυτή τη διεύθυνση ηλεκτρονικού ταχυδρομείου." some: "Μερικά επιστρεφόμενα μηνύματα ελήφθησαν πρόσφατα από αυτή τη διεύθυνση ηλεκτρονικού ταχυδρομείου." threshold_reached: "Ελήφθησαν πολλά επιστρεφόμενα μηνύματα από αυτή τη διεύθυνση ηλεκτρονικού ταχυδρομείου." trust_level_change_failed: "Παρουσιάστηκε πρόβλημα κατά την αλλαγή του επιπέδου εμπιστοσύνης του χρήστη." - suspend_modal_title: "Απενεργοποίηση χρήστη." + suspend_modal_title: "Αποβολή Χρήστη." trust_level_2_users: "Χρήστες στο επιπέδου εμπιστοσύνης 2" trust_level_3_requirements: "Χρήστες στο επιπέδου εμπιστοσύνης 3" trust_level_locked_tip: "το επίπεδο εμπιστοσύνης κλειδώθηκε, το σύστημα δεν θα προβιβάσει ή υποβιβάσει τον χρήστη" @@ -2898,20 +3094,20 @@ el: one: "Την τελευταία μέρα:" other: "Τις τελευταίες %{count} μέρες:" value_heading: "Τιμή" - requirement_heading: "Προϋποθέσεις" + requirement_heading: "Προϋπόθεση" visits: "Επισκέψεις" days: "ημέρες" - topics_replied_to: "Τα νήματα όπου απάντησες" - topics_viewed: "Τα νήματα που είδες" + topics_replied_to: "Νήματα που απάντησε" + topics_viewed: "Νήματα που είδε" topics_viewed_all_time: "Νήματα που ιδώθηκαν (συνολικά)" posts_read: "Αναρτήσεις που διαβάστηκαν" posts_read_all_time: "Αναρτήσεις που διαβάστηκαν (συνολικά)" flagged_posts: "Αναρτήσεις που επισημάνθηκαν" flagged_by_users: "Χρήστες που επισήμαναν" likes_given: "Μου αρέσει που αποδόθηκαν" - likes_received: "«Μου αρέσει» που δεχτήκες." - likes_received_days: "Μου αρέσει που έλαβα: ξεχωριστές μέρες" - likes_received_users: "Μου αρέσει που έλαβα: ξεχωριστοί χρήστες" + likes_received: "«Μου αρέσει» που λήφθησαν" + likes_received_days: "Μου αρέσει που ελήφθησαν: ξεχωριστές μέρες" + likes_received_users: "Μου αρέσει που ελήφθησαν: ξεχωριστοί χρήστες" qualifies: "Πληροί τις προϋποθέσεις για το επίπεδο εμπιστοσύνης 3." does_not_qualify: "Δεν πληροί τις προϋποθέσεις για το επίπεδο εμπιστοσύνης 3." will_be_promoted: "Θα προβιβαστεί σύντομα." @@ -2921,11 +3117,11 @@ el: locked_will_not_be_demoted: "Το επίπεδο εμπιστοσύνης κλειδώθηκε. Δεν πρόκειται ποτέ να υποβιβαστεί. " sso: title: "Single Sign On" - external_id: "Εξωτερικό ID" + external_id: "External ID" external_username: "Χρηστώνυμο" external_name: "Όνομα" external_email: "Διεύθυνση ηλεκτρονικού ταχυδρομείου" - external_avatar_url: "URL για την εικόνα προφίλ" + external_avatar_url: "URL Εικόνας Προφίλ" user_fields: title: "Πεδία Χρήστη" help: "Προσθέστε πεδία που οι χρήστες μπορουν να συμπληρώσουν." @@ -2959,7 +3155,7 @@ el: field_types: text: 'Πεδίο κειμένου' confirm: 'Επιβεβαίωση' - dropdown: "Άνοιγμα" + dropdown: "Dropdown" site_text: description: "Μπορείς να προσαρμόσεις οποιοδήποτε κείμενο στο χώρο συζητήσεων σου. Παρακαλώ ξεκίνησε κάνοντας αναζήτηση παρακάτω:" search: "Αναζήτησε το κείμενο που θα ήθελες να επεξεργαστείς" @@ -2969,7 +3165,7 @@ el: revert_confirm: "Είσαι βέβαιος πως θέλεις να επαναφέρεις τις αλλαγές;" go_back: "Πίσω στην αναζήτηση" recommended: "Σου προτείνουμε να προσαρμόσεις το ακόλουθο κείμενο στις ανάγκες σου:" - show_overriden: 'Δείξε μόνο τα πιο σημαντικά' + show_overriden: 'Δείξε μόνο αυτά που άλλαξαν' site_settings: show_overriden: 'Δείξε μόνο αυτά που άλλαξαν' title: 'Ρυθμίσεις' @@ -2977,7 +3173,7 @@ el: none: 'κανένα' no_results: "Δεν βρέθηκαν αποτελέσματα." clear_filter: "Καθάρισμα φίλτρου" - add_url: "δώσε URL" + add_url: "προσθήκη URL" add_host: "προσθήκη host" categories: all_results: 'Όλα' @@ -2992,11 +3188,11 @@ el: onebox: "Onebox" seo: 'SEO' spam: 'Ανεπιθύμητο' - rate_limits: 'Περιορισμοί' + rate_limits: 'Όριο Συχνότητας' developer: 'Κατασκευαστής λογισμικού' embedding: "Ενσωμάτωση" legal: "Νομικά" - user_api: 'Χρήστης API' + user_api: 'User API' uncategorized: 'Άλλα' backups: "Αντίγραφα ασφαλέιας" login: "Σύνδεση" @@ -3028,10 +3224,10 @@ el: reason: Αιτία expand: Επέκταση … revoke_confirm: Είσαι σίγουρος πως θέλεις να ανακαλέσεις αυτό το παράσημο; - edit_badges: Επεξεργασία παράσημου + edit_badges: Επεξεργασία παρασήμων grant_badge: Απονομή παράσημου granted_badges: Απονεμηθέντα παράσημα - grant: Απόνεμε + grant: Απονομή no_user_badges: "Στο χρήστη %{name} δεν έχουν απονεμηθεί παράσημα." no_badges: Δεν υπάρχουν παράσημα που να μπορούν να απονεμηθούν. none_selected: "Διάλεξε ένα παράσημο για να ξεκινήσεις" @@ -3053,7 +3249,7 @@ el: post_revision: "Όταν ένας χρήστης επεξεργάζεται ή δημιουργεί μια ανάρτηση" trust_level_change: "Όταν το επίπεδο εμπιστοσύνης ενός χρήστη αλλάξει" user_change: "Όταν ένας χρήστης δημιουργείται ή υφίσταται επεξεργασία " - post_processed: "Αφού μια δημοσίευση έχει δεχτεί επεξεργασία" + post_processed: "Αφού μια ανάρτηση έχει δεχτεί επεξεργασία" preview: link_text: "Προεπισκόπηση απονεμηθέντων παράσημων" plan_text: "Προεπισκόπηση του ερωτήματος" @@ -3086,34 +3282,36 @@ el: sample: "Χρησιμοποιήστε τον ακόλουθο HTML κώδικα στον ιστότοπό σας για να ενθέσετε θέματα. Αντικαταστήστε REPLACE_ME με το κανονικό URL της σελίδας στην οποία θα ενθέσετε το θέμα. " title: "Ενσωμάτωση" host: "Επιτρεπόμενα hosts" - path_whitelist: "Μονοπάτι Λίστας Επιτρεπομένων" + class_name: "Class Name" + path_whitelist: "Path Whitelist" edit: "επεξεργασία" - category: "Δημοσίευση στην κατηγορία" + category: "Ανάρτηση στην Κατηγορία" add_host: "Προσθήκη host" - settings: "Ενσωματωμένες ρυθμίσεις" - feed_settings: "Ρυθμίσεις τροφοδοσίας" + settings: "Ρυθμίσεις Ενσωμάτωσης" + feed_settings: "Ρυθμίσεις Τροφοδοσίας" feed_description: "Με το να συμπεριλαμβάνετε ένα RSS/ATOM feed για τον ιστότοπό σας βελτιώνετε την δυνατότητα του Discourse να εισάγει τα περιεχόμενά σας. " - crawling_settings: "Ρυθμίσεις ανίχνευσης" - crawling_description: "Όταν η ομιλία δημιουργεί νήματα για τις δημοσιεύσεις σου, εάν δεν είναι κανένα από τα RSS/ATOM παρούσα θα προσπαθήσει να αναλύσει το περιεχόμενο του HTML σου. Κάποιες φορές μπορεί να αποτελεί πρόκληση το να εξάγεις το περιεχόμενο σου, έτσι σου παρέχουμε την ικανότητα να προσδιορίσεις τους νόμους CSS για να κάνεις την εξαγωγή σου ευκολότερη." + crawling_settings: "Ρυθμίσεις Crawler " + crawling_description: "Όταν η ιστοσελίδα δημιουργεί νήματα για τις αναρτήσεις σου, άν δεν υπάρχει RSS/ATOM feed θα προσπαθήσει να αναλύσει το περιεχόμενο του HTML σου. Κάποιες φορές μπορεί να αποτελεί πρόκληση το να εξάγει το περιεχόμενο σου, έτσι σου παρέχουμε την ικανότητα να προσδιορίσεις τα CSS rules για να κάνεις την εξαγωγή ευκολότερη." embed_by_username: "Όνομα χρήστη για δημιουργία νήματος" - embed_post_limit: "Μέγιστος αριθμός δημοσιεύσεων για συγχώνευση" - embed_username_key_from_feed: "Κλειδή για έλξη του ονόματος χρήστη του discource από τη τροφή" - embed_title_scrubber: "Κανονική έκφραση χρησιμοποιείται για τον τίτλο της δημοσίευσης" - embed_truncate: "Κόψιμο των ενσωματωμένων δημοσιεύσεων" - embed_whitelist_selector: "CSS διαλογέας για στοιχεία που επιτρέπονται στα συγχωνευμένα" - embed_blacklist_selector: "CSS διαλογέας για στοιχεία που έχουν απομακρυνθεί από τα συγχωνευμένα" - embed_classname_whitelist: "Επιτρεπόμενα CSS ονόματα τάξης" - feed_polling_enabled: "Εισαγωγή δημοσιεύσεων μέσω RSS/ATOM" - feed_polling_url: "Η διεύθυνση URL του RSS/ATOM τροφοδοτεί την ανιχνεύση" - save: "Αποθήκευση ενσωματωμένων αλλαγών" + embed_post_limit: "Μέγιστος αριθμός αναρτήσεων για συγχώνευση" + embed_username_key_from_feed: "Κλειδί για λήψη του ονόματος χρήστη από το feed" + embed_title_scrubber: "Regular expression που χρησιμοποιείται για την λήψη των τίτλων των αναρτήσεων" + embed_truncate: "Truncate the embedded posts" + embed_whitelist_selector: "CSS selector για στοιχεία που επιτρέπονται στα συγχωνευμένα" + embed_blacklist_selector: "CSS selector για στοιχεία που έχουν απομακρυνθεί από τα συγχωνευμένα" + embed_classname_whitelist: "Επιτρεπόμενα CSS class names" + feed_polling_enabled: "Εισαγωγή αναρτήσεων μέσω RSS/ATOM" + feed_polling_url: "URL of RSS/ATOM feed to crawl" + feed_polling_frequency_mins: "Συχνότητα του feed polling (σε λεπτά)" + save: "Αποθήκευση των ρυθμίσεων Embedding " permalink: - title: "Μόνιμοι Σύνδεσμοι" - url: "διεύθυνση URL" - topic_id: "Ταυτότητα νήματος" + title: "Permalinks" + url: "URL" + topic_id: "Topic ID" topic_title: "Νήμα" - post_id: "Ταυτότητα δημοσίευσης" - post_title: "Δημοσίευση" - category_id: "Ταυτότητα Κατηγορίας" + post_id: "Post ID" + post_title: "Ανάρτηση" + category_id: "Category ID" category_title: "Κατηγορία" external_url: "Εξωτερική διεύθυνση URL" delete_confirm: Είσαι βέβαιος πως θέλεις να διαγράψεις αυτόν τον μόνιμο σύνδεσμο; @@ -3126,17 +3324,17 @@ el: done: "Ολοκληρώθηκε" back: "Πίσω" next: "Επόμενο" - step: "%{current} του %{total}" - upload: "Μεταφόρτωση" - uploading: "Μεταφορτώνεται..." + step: "%{current} από %{total}" + upload: "Ανέβασμα" + uploading: "Ανεβαίνει..." quit: "Ίσως αργότερα" staff_count: one: "Η κοινότητά σου έχει 1 μέλος του προσωπικού." other: "Η κοινότητά σου έχει %{count} μέλη του προσωπικού." invites: - add_user: "προσθέτω" + add_user: "προσθήκη" none_added: "Δεν έχεις προσκαλέσει το προσωπικό. Είσαι βέβαιος πως θέλεις να συνεχίσεις;" roles: admin: "Διαχειριστής" - moderator: "Μεσολαβιτής" + moderator: "Συντονιστής" regular: "Τακτικός χρήστης" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1bb210eb6c..0d08df5c56 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -409,7 +409,8 @@ en: add_members: "Add Members" delete_member_confirm: "Remove '%{username}' from the '%{group}' group?" name_placeholder: "Group name, no spaces, same as username rule" - public: "Allow users to join/leave the group freely (Requires publicly visible group)" + public_admission: "Allow users to join the group freely (Requires publicly visible group)" + public_exit: "Allow users to leave the group freely" empty: posts: "There are no posts by members of this group." members: "There are no members in this group." @@ -421,6 +422,7 @@ en: join: "Join Group" leave: "Leave Group" request: "Request to Join Group" + message: "Message" automatic_group: Automatic Group closed_group: Closed Group is_group_user: "You are a member of this group" @@ -1134,6 +1136,24 @@ en: ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Search for emoji + people: People + nature: Nature + food: Food + activity: Activity + travel: Travel + objects: Objects + celebration: Celebration + custom: Custom emojis + recent: Recently used + default_tone: No skin tone + light_tone: Light skin tone + medium_light_tone: Medium light skin tone + medium_tone: Medium skin tone + medium_dark_tone: Medium dark skin tone + dark_tone: Dark skin tone + composer: emoji: "Emoji :)" more_emoji: "more..." @@ -1313,14 +1333,15 @@ en: clear_all: "Clear All" too_short: "Your search term is too short." result_count: - one: "1 result for \"{{term}}\"" - other: "{{count}} results for \"{{term}}\"" + one: "1 result for {{term}}" + other: "{{count}} results for {{term}}" title: "search topics, posts, users, or categories" no_results: "No results found." no_more_results: "No more results found." searching: "Searching ..." post_format: "#{{post_number}} by {{username}}" results_page: "Search Results" + more_results: "There are more results. Please narrow your search criteria." context: user: "Search posts by @{{username}}" @@ -1354,6 +1375,7 @@ en: seen: I've read unseen: I've not read wiki: are wiki + images: includes image(s) all_tags: Contains all tags statuses: label: Where topics @@ -1724,6 +1746,7 @@ en: success_email: "We mailed out an invitation to {{emailOrUsername}}. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites." success_username: "We've invited that user to participate in this topic." error: "Sorry, we couldn't invite that person. Perhaps they have already been invited? (Invites are rate limited)" + success_existing_email: "A user with email {{emailOrUsername}} already exists. We've invited that user to participate in this topic." login_reply: 'Log In to Reply' @@ -3129,6 +3152,31 @@ en: logster: title: "Error Logs" + watched_words: + title: "Watched Words" + search: "search" + clear_filter: "Clear" + show_words: "show words" + word_count: + one: "1 word" + other: "%{count} words" + actions: + block: 'Block' + censor: 'Censor' + require_approval: 'Require Approval' + flag: 'Flag' + action_descriptions: + block: 'Prevent posts containing these words from being posted. The user will see an error message when they try to submit their post.' + censor: 'Allow posts containing these words, but replace them with characters that hide the censored words.' + require_approval: 'Posts containing these words will require approval by staff before they can be seen.' + flag: 'Allow posts containing these words, but flag them as inappropriate so moderators can review them.' + form: + label: 'New Word:' + add: 'Add' + success: 'Success' + upload: "Upload" + upload_successful: "Upload successful. Words have been added." + impersonate: title: "Impersonate" help: "Use this tool to impersonate a user account for debugging purposes. You will have to log out once finished." diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index b06ed639d6..7df0882ba7 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -148,20 +148,20 @@ es: default_description: "Por defecto" s3: regions: - us_east_1: "EEUU Este (Virginia del Norte)" - us_west_1: "EEUU Oeste (Californa norte)" - us_west_2: "EEUU Oeste (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "UE (Irlanda)" - eu_west_2: "EU (Londres)" - eu_central_1: "UE (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapur)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Bombay)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seúl)" - sa_east_1: "Sudamérica (São Paulo)" + ap_south_1: "Asia Pacific (Bombay)" + ap_southeast_1: "Asia Pacific (Singapur)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Pekín)" + eu_central_1: "UE (Frankfurt)" + eu_west_1: "UE (Irlanda)" + eu_west_2: "EU (Londres)" + sa_east_1: "Sudamérica (São Paulo)" + us_east_1: "EEUU Este (Virginia del Norte)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "EEUU Oeste (Californa norte)" + us_west_2: "EEUU Oeste (Oregon)" edit: 'editar el título y la categoría de este tema' not_implemented: "Esta característica no ha sido implementada aún, ¡lo sentimos!" no_value: "No" @@ -819,8 +819,8 @@ es: more_badges: "Más distintivos" top_links: "Top enlaces" no_links: "No hay enlaces aún." - most_liked_by: "Los que dieron más me gusta" - most_liked_users: "Más gustado" + most_liked_by: "De quienes recibió más me gusta" + most_liked_users: "A quienes dio más me gusta" most_replied_to_users: "A quienes más respondió" no_likes: "No hay me gusta aún." associated_accounts: "Inicios de sesión" @@ -1012,7 +1012,6 @@ es: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" - google_classic: "Google Classic" facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Solo categorías" diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 722887e363..aa3872e639 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -141,19 +141,19 @@ et: bootstrap_mode_disabled: "Käivitusrežiim lülitub välja järgmise 24 tunni jooksul." s3: regions: - us_east_1: "US Ida (N. Virginia)" - us_west_1: "US Lääs (N. California)" - us_west_2: "US Lääs (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EL (Iirimaa)" - eu_central_1: "EL (Frankfurt)" - ap_southeast_1: "Aasia ja Vaikne ookean (Singapur)" - ap_southeast_2: "Aasia ja Vaikne ookean (Sydney)" - ap_south_1: "Aasia ja Vaikse Ookeani (Mumbai)" ap_northeast_1: "Aasia ja Vaikne ookean (Tokyo)" ap_northeast_2: "Aasia ja Vaikne ookean (Seoul)" - sa_east_1: "Lõuna-Ameerika (Sao Paulo)" + ap_south_1: "Aasia ja Vaikse Ookeani (Mumbai)" + ap_southeast_1: "Aasia ja Vaikne ookean (Singapur)" + ap_southeast_2: "Aasia ja Vaikne ookean (Sydney)" cn_north_1: "Hiina (Beijing)" + eu_central_1: "EL (Frankfurt)" + eu_west_1: "EL (Iirimaa)" + sa_east_1: "Lõuna-Ameerika (Sao Paulo)" + us_east_1: "US Ida (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US Lääs (N. California)" + us_west_2: "US Lääs (Oregon)" edit: 'muuda teema pealkirja ja foorumit' not_implemented: "Seda omadust pole veel rakendatud, vabandame!" no_value: "Ei" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 9fe342d8da..059d88b002 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -95,7 +95,7 @@ fa_IR: action_codes: public_topic: "این موضوع در %{when} عمومی شده" private_topic: "این موضوع در %{when} خصوصی شده" - split_topic: "این موضوع را در %{when} جدا کنید " + split_topic: "این موضوع %{when} جدا شد " invited_user: "%{who} در %{when} دعوت شده" invited_group: "%{who} در %{when} دعوت شده" removed_user: "%{who} در %{when} حذف شد" @@ -130,20 +130,20 @@ fa_IR: default_description: "پیش‌فرض" s3: regions: - us_east_1: "شرق ایالات متحده (ویرجینیا شمالی)" - us_west_1: "غرب ایالات متحده (کالیفرنیا شمالی)" - us_west_2: "غرب ایالات متحده (اورگان)" - us_gov_west_1: "AWS GovCloud (ایالات متحده)" - eu_west_1: "اروپا (ایرلند)" - eu_west_2: "اروپا (لندن)" - eu_central_1: "اروپا (فرانکفورت)" - ap_southeast_1: "آسیا و اقیانوسیه (سنگاپور)" - ap_southeast_2: "آسیا و اقیانوسیه (سیدنی)" - ap_south_1: "آسیا و اقیانوسیه (بمبئی)" ap_northeast_1: "آسیا و اقیانوسیه (توکیو)" ap_northeast_2: "آسیا و اقیانوسیه (سئول)" - sa_east_1: "آمریکای جنوبی (سائوپائولو)" + ap_south_1: "آسیا و اقیانوسیه (بمبئی)" + ap_southeast_1: "آسیا و اقیانوسیه (سنگاپور)" + ap_southeast_2: "آسیا و اقیانوسیه (سیدنی)" cn_north_1: "چین (پکن)" + eu_central_1: "اروپا (فرانکفورت)" + eu_west_1: "اروپا (ایرلند)" + eu_west_2: "اروپا (لندن)" + sa_east_1: "آمریکای جنوبی (سائوپائولو)" + us_east_1: "شرق ایالات متحده (ویرجینیا شمالی)" + us_gov_west_1: "AWS GovCloud (ایالات متحده)" + us_west_1: "غرب ایالات متحده (کالیفرنیا شمالی)" + us_west_2: "غرب ایالات متحده (اورگان)" edit: 'موضوع و دسته‌ی این موضوع را ویرایش کنید' not_implemented: "آن ویژگی هنوز به کار گرفته نشده، متأسفیم!" no_value: "خیر" @@ -1084,7 +1084,7 @@ fa_IR: invitee_accepted: "

    {{username}} accepted your invitation

    " moved_post: "

    {{username}} moved {{description}}

    " linked: "

    {{username}} {{description}}

    " - granted_badge: "

    '{{description}}' را بدست آورده

    " + granted_badge: "

    نشان '{{description}}' را بدست آوردید

    " topic_reminder: "

    {{username}} {{description}}

    " watching_first_post: "

    موضوع جدید {{description}}یک پیام خوانده نشده وجود دارد } other { # پیام خوانده نشده وجود دارد } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} 1 موضوع جدید } other { {BOTH, select, true{and } false {are } other{}} # موضوع جدید } } remaining, or {CATEGORY, select, true {نمایش سایر موضوعات دسته‌بندی {catLink}} false {{latestLink}} other {}}" + read_more_MF: "{ UNREAD, plural, =0 {} one { یک پیام خوانده نشده } other { # پیام خوانده نشده } } { NEW, plural, =0 {} one { {BOTH, select, true{و } false { } other{}} 1 موضوع جدید } other { {BOTH, select, true{و } false { } other{}} # موضوع جدید } } وجود دارد, یا {CATEGORY, select, true {نمایش سایر موضوعات دسته‌بندی {catLink}} false {{latestLink}} other {}}" browse_all_categories: جستوجوی همه‌ی دسته‌‌بندی‌ها view_latest_topics: مشاهده آخرین موضوع suggest_create_topic: چرا یک موضوع ایجاد نمی‌کنید؟ @@ -1376,7 +1376,7 @@ fa_IR: '2_2': 'به دلیل پیگیری این موضوع، تعداد پاسخ‌های جدید را خواهید دید.' '2': 'شما به دلیل خواندن این موضوع. تعداد پاسخ‌های جدید آن را خواهید دید.' '1_2': 'در صورتی که فردی با @نام به شما اشاره کند یا پاسخی دهد به شما اطلاع داده خواهد شد.' - '1': 'در صورتی که فردی با @نام به شما اشاره کند یا به پاسخی دهد به شما اطلاع داده خواهد شد.' + '1': 'در صورتی که فردی با @نام به شما اشاره کند یا پاسخی دهد، به شما اطلاع داده خواهد شد.' '0_7': 'شما تمام اعلان‌های این دسته‌بندی را نادیده گرفته اید' '0_2': 'شما تمام اعلان‌های این موضوع را نادیده گرفته اید' '0': 'شما تمام اعلان‌های این موضوع را نادیده گرفته اید' @@ -1882,10 +1882,10 @@ fa_IR: posts: "نوشته‌ها" posts_long: "این موضوع {{number}} نوشته دارد" posts_likes_MF: | - این موضوع {count, plural, one {1 پاسخ} other {# پاسخ}} با {ratio, select, - low {تعداد پسند به نوشته بالایی دارد} - med {تعداد پسند به نوشته بسیار بالایی دارد} - high {تعداد پسند به نوشته بشدت بالایی دارد} + این موضوع دارای {count, plural, one {1 پاسخ} other {# پاسخ}} است {ratio, select, + low {و مورد پسند دیگران واقع شده است} + med {و بسیار مورد پسند دیگران واقع شده است} + high {و به شدت مورد پسند دیگران واقع شده است} other {}} original_post: "نوشته اصلی" views: "نمایش‌ها" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index bef9a585b9..04bc7fa721 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -148,20 +148,21 @@ fi: default_description: "Oletus" s3: regions: - us_east_1: "itäinen USA (Pohjois-Virginia)" - us_west_1: "Läntinen USA (Pohjois-Kalifornia)" - us_west_2: "Läntinen USA (Oregon)" - us_gov_west_1: "AWS GovCloud (USA)" - eu_west_1: "EU (Irlanti)" - eu_west_2: "EU (Lontoo)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Aasia ja Tyynimeri (Singapore)" - ap_southeast_2: "Aasia ja Tyynimeri (Sydney)" - ap_south_1: "Aasia ja Tyynimeri (Mumbai)" ap_northeast_1: "Aasia ja Tyynimeri (Tokio)" ap_northeast_2: "Aasia ja Tyynimeri (Soul)" - sa_east_1: "Etelä-Amerikka (Sao Paulo)" + ap_south_1: "Aasia ja Tyynimeri (Mumbai)" + ap_southeast_1: "Aasia ja Tyynimeri (Singapore)" + ap_southeast_2: "Aasia ja Tyynimeri (Sydney)" cn_north_1: "Kiina (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irlanti)" + eu_west_2: "EU (Lontoo)" + sa_east_1: "Etelä-Amerikka (Sao Paulo)" + us_east_1: "itäinen USA (Pohjois-Virginia)" + us_east_2: "Itäinen USA (Ohio)" + us_gov_west_1: "AWS GovCloud (USA)" + us_west_1: "Läntinen USA (Pohjois-Kalifornia)" + us_west_2: "Läntinen USA (Oregon)" edit: 'muokkaa ketjun otsikkoa ja aluetta' not_implemented: "Tätä toimintoa ei ole vielä toteutettu, pahoittelut!" no_value: "Ei" @@ -206,8 +207,8 @@ fi: one: "{{count}} merkki" other: "{{count}} merkkiä" suggested_topics: - title: "Ehdotetut ketjut" - pm_title: "Ehdotetut viestit" + title: "Ketjuehdotuksia" + pm_title: "Keskusteluehdotuksia" about: simple_title: "Tietoja" title: "Tietoja sivustosta %{title}" @@ -352,7 +353,8 @@ fi: add_members: "Lisää jäseniä" delete_member_confirm: "Poista '%{username}' ryhmästä '%{group}'?" name_placeholder: "Ryhmän nimi. Ei välilyöntejä, samat säännöt kuin käyttäjänimillä" - public: "Salli käyttäjien liittyä ryhmään ja poistua siitä vapaasti (ryhmän täytyy olla julkisesti näkyvillä)" + public_admission: "Salli käyttäjien liittyä ryhmään vapaasti (Ryhmän täytyy olla julkinen)" + public_exit: "Salli käyttäjien poistua ryhmästä vapaasti" empty: posts: "Ryhmän jäsenet eivät ole lähettäneet viestejä." members: "Ryhmässä ei ole jäseniä." @@ -364,6 +366,7 @@ fi: join: "Liity ryhmään" leave: "Poistu ryhmästä" request: "Hae ryhmään" + message: "Viesti" automatic_group: Automaattinen ryhmä closed_group: Suljettu ryhmä is_group_user: "Olet tämän ryhmän jäsen" @@ -1013,6 +1016,8 @@ fi: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google Classic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Vain alueet" categories_with_featured_topics: "Alueet, joiden yhteydessä ketjuja" @@ -1021,6 +1026,23 @@ fi: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Etsi emojia + people: Ihmiset + nature: Luonto + food: Ruoka + activity: Harrasteet + travel: Matkustelu + objects: Esineet + celebration: Juhlinta + custom: Mukautetut emojit + recent: Hiljattain käytetyt + default_tone: Ei ihonsävyä + light_tone: Vaalea ihonväri + medium_light_tone: Vaaleanruskea ihonväri + medium_tone: Ruskea ihonväri + medium_dark_tone: Tummanruskea ihonväri + dark_tone: Tumma ihonväri composer: emoji: "Emoji :)" more_emoji: "lisää..." @@ -2884,6 +2906,27 @@ fi: title: "Luo uusia aliverkkojen laajuisia porttikieltoja, jos kieltoja on asetettu vähintään 'min_ban_entries_for_roll_up' asetuksen määrä." logster: title: "Virhelokit" + watched_words: + title: "Tarkkaillut sanat" + search: "haku" + clear_filter: "Tyhjennä" + show_words: "näytä sanat" + actions: + block: 'Estä' + censor: 'Sensuroi' + require_approval: 'Vie hyväksyttäväksi' + flag: 'Liputa' + action_descriptions: + block: 'Estä lähettämästä viestiä, jossa esiintyy joku näistä sanoista. Käyttäjälle näytetään virheilmoitus, kun hän yrittää lähettää viestinsä.' + censor: 'Salli viesti, jossa esiintyy joku näistä sanoista, mutta korvaa ne merkeillä, jotka piilottavat sensuroidut sanat.' + require_approval: 'Viesti, jossa esiintyy joku näistä sanoista, tarvitsee henkilökunnan hyväksynnän ennen sen julkaisemista.' + flag: 'Salli viesti, jossa esiintyy joku näistä sanoista, mutta liputa se sopimattomaksi, jotta valvojat voivat tarkastella sitä.' + form: + label: 'Uusi sana:' + add: 'Lisä' + success: 'Onnistui' + upload: "Lataa" + upload_successful: "Lataus onnistui. Sanoja lisättiin." impersonate: title: "Esiinny käyttäjänä" help: "Tällä työkalulla voi esiintyä toisena käyttäjänä virheiden paikantamista varten. Sinun täytyy kirjautua ulos, kun olet valmis." diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index f094071c20..6df4471f1e 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -148,20 +148,20 @@ fr: default_description: "Par défaut" s3: regions: - us_east_1: "États-Unis est (Virginie)" - us_west_1: "États-Unis ouest (Californie)" - us_west_2: "États-Unis ouest (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "UE (Irlande)" - eu_west_2: "UE (Londres)" - eu_central_1: "UE (Francfort)" - ap_southeast_1: "Asie-Pacifique (Singapour)" - ap_southeast_2: "Asie-Pacifique (Sydney)" - ap_south_1: "Asie-Pacifique (Bombay)" ap_northeast_1: "Asie-Pacifique (Tokyo)" ap_northeast_2: "Asie-Pacifique (Séoul)" - sa_east_1: "Amérique du Sud (Sao Paulo)" + ap_south_1: "Asie-Pacifique (Bombay)" + ap_southeast_1: "Asie-Pacifique (Singapour)" + ap_southeast_2: "Asie-Pacifique (Sydney)" cn_north_1: "Chine (Pékin)" + eu_central_1: "UE (Francfort)" + eu_west_1: "UE (Irlande)" + eu_west_2: "UE (Londres)" + sa_east_1: "Amérique du Sud (Sao Paulo)" + us_east_1: "États-Unis est (Virginie)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "États-Unis ouest (Californie)" + us_west_2: "États-Unis ouest (Oregon)" edit: 'modifier le titre et la catégorie de ce sujet' not_implemented: "Cette fonctionnalité n'a pas encore été implémentée, désolé." no_value: "Non" @@ -352,6 +352,7 @@ fr: add_members: "Ajouter des membres" delete_member_confirm: "Supprimer %{username} du groupe « %{group} » ?" name_placeholder: "Nom du groupe, sans espaces, même règle que pour les pseudos" + public: "Autoriser les utilisateurs à rejoindre/quitter le groupe librement (nécessite que le groupe soit visible)" empty: posts: "Il n'y a aucun message de membres de ce groupe." members: "Il n' y a aucun membre dans ce groupe." @@ -386,6 +387,12 @@ fr: mentions: "Mentions" messages: "Messages" notification_level: "Niveau de notification par défaut pour les messages de groupe" + visibility_levels: + title: "Qui peut voir ce groupe ?" + public: "Tout le monde" + members: "Propriétaires du groupe, membres et administrateurs" + staff: "Propriétaires du groupe et responsables" + owners: "Propriétaires du groupe et administrateurs" alias_levels: title: "Qui peut envoyer un message et @notifier ce groupe ?" nobody: "Personne" @@ -1005,6 +1012,8 @@ fr: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Classique Google" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Catégories seules" categories_with_featured_topics: "Catégories et sujets sélectionnés" diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index e24bf1778d..e44e69bb6e 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -140,17 +140,17 @@ gl: emails_are_disabled: "Todos os correos electrónicos saíntes foron desactivados globalmente por un administrador. Non se enviará ningún tipo de notificación por correo electrónico." s3: regions: - us_east_1: "EE.UU. Leste (N. Virxinia)" - us_west_1: "EE.UU. Oeste (N. California)" - us_west_2: "EE.UU. Oeste (Oregón)" - us_gov_west_1: "AWS GovCloud (EE.UU)" - eu_west_1: "EU (Irlanda)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacífico (Singapur)" - ap_southeast_2: "Asia Pacífico (Sidney)" ap_northeast_1: "Asia Pacífico (Tokio)" ap_northeast_2: "Asia Pacífico (Seúl)" + ap_southeast_1: "Asia Pacífico (Singapur)" + ap_southeast_2: "Asia Pacífico (Sidney)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irlanda)" sa_east_1: "América do Sur (São Paulo)" + us_east_1: "EE.UU. Leste (N. Virxinia)" + us_gov_west_1: "AWS GovCloud (EE.UU)" + us_west_1: "EE.UU. Oeste (N. California)" + us_west_2: "EE.UU. Oeste (Oregón)" edit: 'editar o título e a categoría deste tema' not_implemented: "Sentímolo pero esta funcionalidade non se implementou aínda." no_value: "Non" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index f85c41bd71..57475c3339 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -18,10 +18,10 @@ he: byte: one: בית other: בתים - gb: GB - kb: KB - mb: MB - tb: TB + gb: ג״ב + kb: ק״ב + mb: מ״ב + tb: ט״ב short: thousands: "k{{number}}" millions: "{{number}}M" @@ -44,7 +44,7 @@ he: half_a_minute: "פחות מדקה" less_than_x_seconds: one: "פחות משנייה" - other: "פחות מ-%{count} שניות" + other: "פחות מ־%{count} שניות" x_seconds: one: "שנייה אחת" other: "%{count} שניות" @@ -62,7 +62,7 @@ he: other: "%{count} שנים" over_x_years: one: "יותר משנה" - other: "יותר מ-%{count} שנים" + other: "יותר מ־%{count} שנים" almost_x_years: one: "שנה אחת" other: "%{count} שנים" @@ -109,7 +109,7 @@ he: twitter: 'שתפו קישור זה בטוויטר' facebook: 'שתפו קישור זה בפייסבוק' google+: 'שתפו קישור זה בגוגל+' - email: 'שלח קישור בדוא"ל' + email: 'שליחת קישור בדוא״ל' action_codes: public_topic: "הפכו נושא זה לפומבי %{when}" private_topic: "הפכו נושא זה לפרטי %{when}" @@ -137,31 +137,32 @@ he: enabled: 'נכנס לרשימה %{when}' disabled: 'הוצא מהרשימה %{when}' banner: - enabled: 'באנר זה נוצר ב-%{when}. הוא יופיע בראש כל דף עד שישוחרר על ידי המשתמש/ת.' - disabled: 'באנר זה הוסר ב-%{when}. הוא לא יופיע יותר בראש כל דף.' + enabled: 'באנר זה נוצר ב־%{when}. הוא יופיע בראש כל דף עד שישוחרר על ידי המשתמש/ת.' + disabled: 'באנר זה הוסר ב־%{when}. הוא לא יופיע יותר בראש כל דף.' topic_admin_menu: "פעולות ניהול לנושא" - wizard_required: "ברוכים הבאים לסידקורס החדש שלכם! בואו נתחיל עם אשף ההתקנה ✨" - emails_are_disabled: "כל הדוא\"ל היוצא נוטרל באופן גורף על ידי מנהל אתר. שום הודעת דוא\"ל, מכל סוג שהוא, לא תשלח." + wizard_required: "ברוכים הבאים לדיסקורס החדש שלכם! בואו נתחיל עם אשף ההתקנה ✨" + emails_are_disabled: "כל הדוא״ל היוצא נוטרל באופן גורף על ידי מנהל אתר. שום הודעת דוא״ל, מכל סוג שהוא, לא תשלח." bootstrap_mode_enabled: "כדי להקל על הקמת האתר החדש שלכם, אתם במצב איתחול-ראשוני. כל המשתמשים החדשים יקבלו רמת אמון 1 ויקבלו תמצות יומי במייל. אפשרות זו תכובה אוטומטית כאשר יהיו יותר מ %{min_users} משתמשים." bootstrap_mode_disabled: "מצב איתחול-ראשוני יכובה ב 24 השעות הקרובות." themes: default_description: "ברירת מחדל" s3: regions: - us_east_1: "מזרח ארה\"ב (צפון וירג'יניה)" - us_west_1: "מערב ארה\"ב (צפון קליפורניה)" - us_west_2: "מערב ארה\"ב (ארגון)" - us_gov_west_1: "AWS GovCloud (ארה״ב)" - eu_west_1: "האיחוד האירופי (אירלנד)" - eu_west_2: "אירופה (לונדון)" - eu_central_1: "האיחוד האירופי (פרנקפורט)" - ap_southeast_1: "אסיה הפסיפית (סינגפור)" - ap_southeast_2: "אסיה הפסיפית (סידני)" - ap_south_1: "אסיה פסיפית (מומבאי)" ap_northeast_1: "אסיה הפסיפית (טוקיו)" ap_northeast_2: "אסיה הפסיפית (סיאול)" - sa_east_1: "דרום אמריקה (סאו פאולו)" + ap_south_1: "אסיה פסיפית (מומבאי)" + ap_southeast_1: "אסיה הפסיפית (סינגפור)" + ap_southeast_2: "אסיה הפסיפית (סידני)" cn_north_1: "סין (בייג׳ינג)" + eu_central_1: "האיחוד האירופי (פרנקפורט)" + eu_west_1: "האיחוד האירופי (אירלנד)" + eu_west_2: "אירופה (לונדון)" + sa_east_1: "דרום אמריקה (סאו פאולו)" + us_east_1: "מזרח ארה״ב (צפון וירג׳יניה)" + us_east_2: "ארה״ב מזרח (אוהיו)" + us_gov_west_1: "AWS GovCloud (ארה״ב)" + us_west_1: "מערב ארה\"ב (צפון קליפורניה)" + us_west_2: "מערב ארה\"ב (ארגון)" edit: 'עירכו את הכותרת והקטגוריה של נושא זה' not_implemented: "סליחה, תכונה זו עדיין לא מומשה!" no_value: "לא" @@ -352,6 +353,8 @@ he: add_members: "הוספת חברים" delete_member_confirm: "הסרת '%{username}' מהקבוצה '%{group}'?" name_placeholder: "שם קבוצה, ללא רווחים, לפי הכללים של שמות משתמשים" + public_admission: "אפשרו למשתמשים להצטרף לקבוצה בחופשיות (דורש קבוצה פומבית)" + public_exit: "אפשרו למשתמשים לעזוב את הקבוצה בחופשיות" empty: posts: "אין פוסטים של חברי קבוצה זו." members: "אין חברים בקבוצה זו." @@ -363,6 +366,7 @@ he: join: "הצטרפות לקבוצה" leave: "עזיבת קבוצה" request: "בקשה להצטרפות לקבוצה" + message: "הודעה" automatic_group: קבוצה אוטומטית closed_group: קבוצה סגורה is_group_user: "אתם חברים בקבוצה זו." @@ -386,6 +390,12 @@ he: mentions: "אזכורים" messages: "הודעות" notification_level: "ברירת מחדל של רמת התראות להודעות קבוצה" + visibility_levels: + title: "מי יכול לראות קבוצה זו?" + public: "כולם" + members: "בעלי קבוצה, חברים ואדמיניסטרטורים" + staff: "בעלי קבוצה וצוות" + owners: "בעלי קבוצה ואדמיניסטרטורים" alias_levels: title: "מי יכולים לשלוח מסרים ו@לאזכר בקבוצה זו?" nobody: "אף אחד" @@ -748,6 +758,9 @@ he: expired: "פג תוקף ההזמנה." rescind: "הסרה" rescinded: "הזמנה הוסרה" + rescind_all: "הסירו את כל ההזמנות" + rescinded_all: "כל ההזמנות הוסרו!" + rescind_all_confirm: "האם אתם בטוחים שאתם מעוניינים להסיר את כל ההזמנות?" reinvite: "משלוח חוזר של הזמנה" reinvite_all: "שלח מחדש את כל ההזמנות" reinvite_all_confirm: "האם אתם בטוחים שאתם מעוניינים לשלוח מחדש את כל ההזמנות?" @@ -930,6 +943,8 @@ he: complete_email_found: "מצאנו חשבון תואם ל%{email}. בתוך זמן קצר תקבלו אליו דוא\"ל עם הנחיות כיצד לאתחל את הסיסמה שלכם." complete_username_not_found: "שום חשבון אינו תואם לשם המשתמש %{username}" complete_email_not_found: "שום חשבון אינו תואם ל %{email}" + button_ok: "או קיי" + button_help: "עזרה" login: title: "התחברות" username: "משתמש" @@ -989,6 +1004,8 @@ he: accept_invite: "קבלת הזמנה" success: "החשבון שלכם נוצר ואתם מחוברים עכשיו." name_label: "שם" + password_label: "קביעת סיסמה" + optional_description: "(אופציונלי)" password_reset: continue: "המשיכו ל-%{site_name}" emoji_set: @@ -1005,6 +1022,20 @@ he: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: חיפוש אחר אימוג׳י + people: אנשים + nature: טבע + food: אוכל + activity: פעילות + custom: אימוג׳יז מותאמים + recent: בשימוש לאחרונה + default_tone: ללא גוון עור + light_tone: גוון עור בהיר + medium_light_tone: גוון עור בהיר בינוני + medium_tone: גוון עור בינוני + medium_dark_tone: גוון עור כהה בינוני + dark_tone: גוון עור כהה composer: emoji: "אמוג׳י :)" more_emoji: "עוד..." @@ -1168,8 +1199,8 @@ he: clear_all: "נקוי של הכל" too_short: "מילת החיפוש שלכם קצרה מידי." result_count: - one: "תוצאה אחת ל \"{{term}}\"" - other: "{{count}} תוצאות ל \"{{term}}\"" + one: "1 תוצאה עבור {{term}}" + other: "{{count}} תוצאות עבור {{term}}" title: "חיפוש נושאים, פוסטים, משתמשים או קטגוריות" no_results: "אין תוצאות." no_more_results: "לא נמצאו עוד תוצאות." @@ -1207,6 +1238,7 @@ he: seen: קראתי unseen: לא קראתי wiki: הם ויקי + all_tags: מכיל את כל התגים statuses: label: כאשר נושאים open: פתוחים @@ -1655,6 +1687,7 @@ he: has_liked: "אהבת פוסט זה" undo_like: "בטל 'אהוב'" edit: "עירכו פוסט זה" + edit_action: "עריכה" edit_anonymous: "מצטערים, אך עליכם להיות מחוברים בכדי לערוך פוסט זה." flag: "דגלו פוסט זה באופן פרטי לתשומת לב או שלחו התראה פרטית עליו" delete: "מחק פוסט זה" @@ -2867,6 +2900,24 @@ he: title: "יוצר ערכי איסור משנה חדשים, אם יש לפחות 'min_ban_entries_for_roll_up' ערכים." logster: title: "רישום תקלות" + watched_words: + search: "חיפוש" + clear_filter: "ניקוי" + show_words: "הצגת מילים" + actions: + block: 'חסימה' + censor: 'צינזור' + require_approval: 'דרישת אישור' + flag: 'דגל' + action_descriptions: + require_approval: 'פוסטים שמכילים מילים אלו ידרשו אישור על ידי הצוות לפני שניתן יהיה לראות אותם.' + flag: 'אפשרו פוסטים שמכילים אלו, אך דגלו אותם כלא-ראויים כדי שמנחים יוכלו לסקור אותם.' + form: + label: 'מילה חדשה:' + add: 'הוספה' + success: 'הצלחה' + upload: "העלאה" + upload_successful: "העלאה הצליחה. מילים נוספו." impersonate: title: "התחזות" help: "השתמשו בכלי זה כדי להתחזות לחשבון משתמש לצרכי דיבוג. עליכם להתנתק ברגע שתסיימו." diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 80bff180e5..edb2c2dc09 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -121,19 +121,19 @@ id: bootstrap_mode_disabled: "Mode bootstrap akan di non aktifkan pada 24 jam berikutnya." s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Ireland)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pasifik (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "Asia Pasifik (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "Cina (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ireland)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'Ubah judul dan kategori topik ini' not_implemented: "Maaf, fitur ini belum diimplementasikan." no_value: "Tidak" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index ec9c59bd37..4d00424ef1 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -148,20 +148,21 @@ it: default_description: "Default" s3: regions: - us_east_1: "Stati Uniti Est (Virginia del Nord)" - us_west_1: "Stati Uniti Ovest (California del Nord)" - us_west_2: "Stati Uniti Ovest (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "Europa (Irlanda)" - eu_west_2: "EU (Londra)" - eu_central_1: "Europa (Francoforte)" - ap_southeast_1: "Asia Pacifico (Singapore)" - ap_southeast_2: "Asia Pacifico (Sidney)" - ap_south_1: "Asia Pacifico (Mumbai)" ap_northeast_1: "Asia Pacifico (Tokyo)" ap_northeast_2: "Asia Pacifico (Seoul)" - sa_east_1: "America del Sud (San Paolo)" + ap_south_1: "Asia Pacifico (Mumbai)" + ap_southeast_1: "Asia Pacifico (Singapore)" + ap_southeast_2: "Asia Pacifico (Sidney)" cn_north_1: "Cina (Beijing)" + eu_central_1: "Europa (Francoforte)" + eu_west_1: "Europa (Irlanda)" + eu_west_2: "EU (Londra)" + sa_east_1: "America del Sud (San Paolo)" + us_east_1: "Stati Uniti Est (Virginia del Nord)" + us_east_2: "USA Est (Ohio)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "Stati Uniti Ovest (California del Nord)" + us_west_2: "Stati Uniti Ovest (Oregon)" edit: 'modifica titolo e categoria dell''argomento' not_implemented: "Spiacenti! Questa funzione non è stata ancora implementata." no_value: "No" @@ -352,6 +353,8 @@ it: add_members: "Aggiungi Membri" delete_member_confirm: "Rimuovere '%{username}' dal gruppo '%{group}'?" name_placeholder: "Nome del gruppo, senza spazi, stesse regole del nome utente" + public_admission: "Consenti agli utenti di unirsi al gruppo liberamente (Richiede che il gruppo sia visibile a tutti)" + public_exit: "Consenti agli utenti di lasciare il gruppo liberamente" empty: posts: "Non ci sono messaggi da membri di questo gruppo." members: "Non ci sono membri in questo gruppo." @@ -363,6 +366,7 @@ it: join: "Partecipa al Gruppo" leave: "Abbandona il Gruppo" request: "Richiedi di partecipare al gruppo" + message: "Messaggio" automatic_group: Gruppo Automatico closed_group: Gruppo Chiuso is_group_user: "Sei un membro di questo gruppo" @@ -386,6 +390,12 @@ it: mentions: "Menzioni" messages: "Messaggi" notification_level: "Livello di notifica predefinito per i messaggi di gruppo" + visibility_levels: + title: "Chi può vedere questo gruppo?" + public: "Chiunque" + members: "Proprietari del gruppo, membri e amministratori" + staff: "Proprietari del gruppo e staff" + owners: "Proprietari del gruppo e amministratori" alias_levels: title: "Chi può inviare @menzionare e inviare messaggi a questo gruppo?" nobody: "Nessuno" @@ -748,6 +758,9 @@ it: expired: "L'invito è scaduto." rescind: "Rimuovi" rescinded: "Invito revocato" + rescind_all: "Rimuovi tutti gli Inviti" + rescinded_all: "Tutti gli inviti sono stati rimossi!" + rescind_all_confirm: "Sei sicuro di voler rimuovere tutti gli inviti?" reinvite: "Rinvia Invito" reinvite_all: "Rispedisci tutti gli Inviti" reinvite_all_confirm: "Sei sicuro di voler inviare nuovamente tutti gli inviti?" @@ -761,10 +774,10 @@ it: link_generated: "Collegamento di invito generato con successo!" valid_for: "Questo collegamento di invito è valido solamente per il seguente indirizzo email: %{email}" bulk_invite: - none: "Non hai ancora invitato nessuno. Manda degli inviti individuali o invita più persone alla volta caricando un file CVS." + none: "Non hai ancora invitato nessuno. Puoi inviare degli inviti individuali o più persone alla volta caricando un file CSV." text: "Invito di Massa da File" success: "Il file è stato caricato con successo, riceverai un messaggio di notifica quando il processo sarà completato." - error: "Spiacente, il file deve essere in formato CSV." + error: "Spiacenti, il file deve essere in formato CSV." password: title: "Password" too_short: "La password è troppo breve." @@ -1002,6 +1015,8 @@ it: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google Classic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Solo Categorie" categories_with_featured_topics: "Categorie con argomenti in evidenza" @@ -1010,12 +1025,30 @@ it: shift: 'Maiusc' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Ricerca per emoji + people: Persone + nature: Natura + food: Cibo + activity: Attività + travel: Viaggio + objects: Oggetti + celebration: Celebrazione + custom: Emoji personalizzate + recent: Usate recentemente + default_tone: Nessun tono della pelle + light_tone: Tono della pelle chiaro + medium_light_tone: Tono della pelle medio chiaro + medium_tone: Tono della pelle medio + medium_dark_tone: Tono della pelle medio scuro + dark_tone: Tono della pelle scuro composer: emoji: "Emoji :)" more_emoji: "altro..." options: "Opzioni" whisper: "sussurra" unlist: "invisibile" + blockquote_text: "Citazione" add_warning: "Questo è un avvertimento ufficiale." toggle_whisper: "Attiva/Disattiva Sussurri" toggle_unlisted: "Rendi Invisibile" @@ -1121,6 +1154,7 @@ it: moved_post: "

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

    " linked: "

    {{username}} {{description}}

    " granted_badge: "

    Guadagnato '{{description}}'

    " + topic_reminder: "

    {{username}} {{description}}

    " watching_first_post: "

    Nuovo Argomento {{description}}

    " group_message_summary: one: "

    {{count}} messaggio nella tua casella {{group_name}}

    " @@ -1204,13 +1238,13 @@ it: posted: ho pubblicato in watching: sto osservando tracking: sto seguendo - private: sono messaggi scritti da me + private: messaggi privati bookmarks: ho aggiunto ai segnalibri first: sono il primissimo post pinned: sono appuntati unpinned: non sono appuntati - seen: Ho letto - unseen: Non ho letto + seen: ho letto + unseen: non ho letto wiki: sono wiki all_tags: Contiene tutte le etichette statuses: @@ -1238,6 +1272,7 @@ it: select_all: "Seleziona Tutti" clear_all: "Deseleziona Tutto" unlist_topics: "Deselezione Topics" + relist_topics: "Ripubblica Argomenti" reset_read: "Reimposta Lettura" delete: "Elimina Argomenti" dismiss: "Chiudi" @@ -1541,6 +1576,7 @@ it: success_email: "Abbiamo inviato un invito via email a {{emailOrUsername}}. Ti avvertiremo quando l'invito verrà riscattato. Controlla la sezione \"inviti\" sulla tua pagina utente per tracciarne lo stato." success_username: "Abbiamo invitato l'utente a partecipare all'argomento." error: "Spiacenti, non siamo riusciti ad invitare questa persona. E' stata per caso già invitata (gli inviti sono limitati)? " + success_existing_email: "Esiste già un utente con email {{emailOrUsername}}. Lo abbiamo invitato a partecipare a questo argomento." login_reply: 'Connettiti per Rispondere' filters: n_posts: @@ -1763,6 +1799,10 @@ it: confirm: one: "Sei sicuro di voler cancellare questo messaggio?" other: "Sei sicuro di voler cancellare tutti questi messaggi?" + merge: + confirm: + one: "Sicuro di voler unire questi messaggi?" + other: "Sicuro di voler unire questi {{count}} messaggi?" revisions: controls: first: "Prima revisione" @@ -1902,6 +1942,7 @@ it: notify_action: 'Messaggio' official_warning: 'Avvertimento Ufficiale' delete_spammer: "Cancella Spammer" + delete_confirm_MF: "Stai per cancellare {POSTS, plural, one {1 messaggio} other {# messaggi}} e {TOPICS, plural, one {1 argomento} other {# argomenti}} di questo utente, rimuovere il suo account, bloccare l'iscrizione dall'indirizzo IP{ip_address}, e aggiungere il suo indirizzo email {email} ad un elenco nero permanente. Sei veramente sicuro che questo utente sia uno spammer?" yes_delete_spammer: "Sì, cancella lo spammer" ip_address_missing: "(N/D)" hidden_email_address: "(nascosto)" @@ -2626,6 +2667,9 @@ it: updating: "Aggiornamento..." up_to_date: "Il tema è aggiornato, ultima verifica:" add: "Aggiungi" + commits_behind: + one: "Il tema è indietro di 1 aggiornamento!" + other: "Il tema è indietro di {{count}} aggiornamenti!" scss: text: "CSS" title: "Inserire il CSS personalizzato, accettiamo tutti gli stili validi di CSS e SCSSpersonalizzato" @@ -2861,6 +2905,27 @@ it: title: "Crea nuovi elenchi di indirizzi IP interdetti se ci sono almeno 'min_ban_entries_for_roll_up' elementi." logster: title: "Log Errori" + watched_words: + title: "Parole Osservate" + search: "ricerca" + clear_filter: "Pulisci" + show_words: "mostra le parole" + actions: + block: 'Blocca' + censor: 'Censura' + require_approval: 'Richiede Approvazione' + flag: 'Segnala' + action_descriptions: + block: 'Impedisci che i messaggi contenenti queste parole siano pubblicati. L''utente visualizzerà un messaggio di errore quando cercherà di pubblicare il messaggio.' + censor: 'Consenti i messaggi contenenti queste parole, ma sostituiscile con caratteri che nascondono le parole censurate.' + require_approval: 'I messaggi contenenti queste parole richiederanno l''approvazione dello staff prima di poter essere visualizzati.' + flag: 'Consenti i messaggi contenenti queste parole, ma segnalale come inappropriate in modo che i moderatori possano revisionarle.' + form: + label: 'Nuova Parola:' + add: 'Aggiungi' + success: 'Successo' + upload: "Carica" + upload_successful: "Caricamento andato a buon fine. Le parole sono state aggiunte." impersonate: title: "Impersona" help: "Usa questo strumento per impersonare un account utente ai fini del debugging. Una volta finito dovrai disconnetterti." diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index e3301c1ffb..2eec19dbe9 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -25,28 +25,28 @@ ja: thousands: "{{number}}k" millions: "{{number}}M" dates: - time: "h:mm a" + time: "a h:mm" timeline_date: "YYYY MMM" - long_no_year: "MMM D h:mm a" + long_no_year: "MMM D a h:mm" long_no_year_no_time: "MMM D" full_no_year_no_time: "MMMM Do" - long_with_year: "YYYY, MMM D h:mm a" - long_with_year_no_time: "YYYY, MMM D" - full_with_year_no_time: "MMMM Do, YYYY" - long_date_with_year: "MMM D, 'YY LT" - long_date_without_year: "MMM D, LT" - long_date_with_year_without_time: "MMM D, 'YY" + long_with_year: "YYYY MMM D a h:mm" + long_with_year_no_time: "YYYY MMM D" + full_with_year_no_time: "YYYY MMMM Do" + long_date_with_year: "'YY MMM D LT" + long_date_without_year: "MMM D LT" + long_date_with_year_without_time: "'YY MMM D" long_date_without_year_with_linebreak: "MMM D
    LT" - long_date_with_year_with_linebreak: "MMM D, 'YY
    LT" + long_date_with_year_with_linebreak: "'YY MMM D
    LT" wrap_ago: "%{date}前" tiny: - half_a_minute: "1分前" + half_a_minute: "1分未満" less_than_x_seconds: - other: "%{count}秒前" + other: "%{count}秒未満" x_seconds: other: "%{count}秒" x_minutes: - other: "%{count}分前" + other: "%{count}分" about_x_hours: other: "%{count}時間" x_days: @@ -54,11 +54,11 @@ ja: about_x_years: other: "%{count}年" over_x_years: - other: "%{count}年以上前" + other: "%{count}年以上" almost_x_years: other: "%{count}年" - date_month: "MMM D日" - date_year: "MMM 'YY" + date_month: "MMM D" + date_year: "'YY MMM" medium: x_minutes: other: "%{count}分" @@ -95,8 +95,11 @@ ja: action_codes: public_topic: "トピックを公開しました: %{when} " private_topic: "トピックを非公開にしました: %{when}" + split_topic: "%{when} このトピックは分割されました" invited_user: "%{who} から招待されました: %{when}" + invited_group: "%{when} %{who}に招待されました" removed_user: "%{who}が %{when}に削除しました" + removed_group: "%{when} %{who}に取り除かれました" autoclosed: enabled: 'クローズされました: %{when}' disabled: '%{when}にオープンしました' @@ -115,7 +118,11 @@ ja: visible: enabled: 'リストに表示: %{when}' disabled: 'リストから非表示: %{when}' + banner: + enabled: 'この広告%{when}を作成して下さい。ユーザーが拒否するまで各ページの上部に表示されます。' + disabled: 'このバナー%{when}を削除すると、各ページの最上部に表示しません。' topic_admin_menu: "トピックの管理" + wizard_required: "ディスコースへようこそ!さあセットアップウィザードから始めましょう" emails_are_disabled: "メールアドレスの送信は管理者によって無効化されています。全てのメール通知は行われません" bootstrap_mode_enabled: "かんたんにサイトを立ち上げられるようにするため、ブートストラップモードが有効になっています。新しいユーザーは全員、トラストレベル1を付与し、毎日更新がメールで届けられます。これらの機能は総ユーザー数が%{min_users}を超えた時にオフになります。" bootstrap_mode_disabled: "ブートストラップモードは24時間後に無効化されます。" @@ -123,20 +130,21 @@ ja: default_description: "デフォルト" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Ireland)" - eu_west_2: "EU(ロンドン)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "アジア(ムンバイ)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "アジア(ムンバイ)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "中国(北京)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ireland)" + eu_west_2: "EU(ロンドン)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_east_2: "US East" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'このトピックのタイトル/カテゴリを編集' not_implemented: "この機能はまだ実装されていません!" no_value: "いいえ" @@ -165,11 +173,11 @@ ja: or: "あるいは" now: "たった今" read_more: 'もっと読む' - more: "もっと見る" - less: "Less" + more: "増やす" + less: "減らす" never: "never" every_30_minutes: "30分毎" - every_hour: "1時間毎" + every_hour: "毎時" daily: "毎日" weekly: "毎週" every_two_weeks: "隔週" @@ -311,12 +319,22 @@ ja: target_user: "対象ユーザー" subject: "対象" details: "詳細" + from: "差出人" + to: "宛先" edit: title: 'グループの編集' full_name: 'フルネーム' add_members: "メンバーを追加" - delete_member_confirm: "'%{username}' を '%{group}' から削除してもよろしいですか?" + delete_member_confirm: "%{group}から%{username}を削除しますか?" name_placeholder: "グループネームはユーザー名と同様にスペース無し" + public: "自由なグループへの加入・脱退をユーザーに許可する (一般公開グループである必要があります)" + empty: + posts: "このグループのメンバーによる投稿はありません。" + members: "このグループにはメンバーがいません。" + mentions: "このグループに対するメンションはありません。" + messages: "このグループへのメッセージはありません。" + topics: "このグループのメンバーによるトピックはありません。" + logs: "このグループに関するログはありません。" add: "追加" join: "グループに参加" leave: "グループから離脱" @@ -342,6 +360,13 @@ ja: posts: "投稿" mentions: "メンション" messages: "メッセージ" + notification_level: "グループメッセージのデフォルト通知レベル" + visibility_levels: + title: "誰がこのグループを見ることができますか?" + public: "誰でも" + members: "グループのオーナー、メンバーおよび管理者" + staff: "グループオーナーとスタッフのみ" + owners: "グループオーナーと管理者のみ" alias_levels: title: "誰がこのグループにメッセージや@メンションが送れますか?" nobody: "無し" @@ -354,13 +379,24 @@ ja: none: "なし" notifications: watching: - title: "ウォッチ中" + title: "待機中" + description: "メッセージ内の新規投稿はあなただけに通知され、新規返信数が表示されます。" + watching_first_post: + title: "最初の投稿に注意" + description: "グループ内新規トピック欄における最初の投稿はあなただけに通知されます。" tracking: title: "追跡中" + description: "誰かがあなた宛に返信をすると通知があり、新規返信数が表示されます。" regular: title: "デフォルト" muted: title: "ミュート" + description: "グループ内の新規トピックについては何も通知されません。" + flair_url: "アバター画像" + flair_bg_color: "アバターの背景色" + flair_color: "アバター色" + flair_preview_icon: "アイコンプレビュー" + flair_preview_image: "画像プレビュー" user_action_groups: '1': "「いいね!」 " '2': "「いいね!」 された" @@ -384,6 +420,7 @@ ja: title: "カテゴリの並び替え" title_long: "カテゴリリストを並べ直します" fix_order: "位置を修正" + fix_order_tooltip: "全てのカテゴリーは唯一である番地を持っていないので、予期せぬ結果を引き起こすかもしれません。" save: "順番を保存" apply_all: "適用" position: "位置" @@ -393,6 +430,8 @@ ja: latest_by: "最新投稿: " toggle_ordering: "カテゴリの並び替えモードを切り替え" subcategories: "サブカテゴリ:" + topic_sentence: + other: "%{count}トピック群" topic_stat_sentence: other: "過去 %{unit} 間 %{count} 個の新着トピック。" ip_lookup: @@ -419,6 +458,9 @@ ja: edit: "プロフィールを編集" download_archive: button_text: "すべてダウンロード" + confirm: "本当にご自身の投稿をダウンロードしたいですか。" + success: "ダウンロードが始まると、処理完了時に通知されます。" + rate_limit_error: "投稿者は1日1回ダウンロードすることが可能です、明日再度お試しください。" new_private_message: "メッセージを作成" private_message: "メッセージ" private_messages: "メッセージ" @@ -442,8 +484,10 @@ ja: each_browser_note: "注意: 利用するすべてのブラウザでこの設定を変更する必要があります" dismiss_notifications: "すべて既読にする" dismiss_notifications_tooltip: "全ての未読の通知を既読にします" + first_notification: "最初の通知です! 始めるために選択してください。" disable_jump_reply: "返信した後に投稿へ移動しない" dynamic_favicon: "新規/更新トピックの件数をブラウザのアイコンに表示する" + theme_default_on_all_devices: "ご自身の全てのデバイス上で初期テーマを作成して下さい。" external_links_in_new_tab: "外部リンクをすべて別のタブで開く" enable_quoting: "選択したテキストを引用して返信する" change: "変更" @@ -463,9 +507,22 @@ ja: この設定は、アクティビティの情報機能を無効化します。
    ミュートしているトピックやカテゴリはこれらのメールには含まれません。 individual: "新しい投稿がある場合にメールで送る" + individual_no_echo: "自分自身を除いて新規投稿関連のメールを送信してください。" + few_per_day: "新規投稿に関連するメッセージを送信してください。(大体2日おき)" + tag_settings: "タグ" + watched_tags: "閲覧済み" + muted_tags: "通知しない" watched_categories: "ウォッチ中" + watched_categories_instructions: "自動的にカテゴリー内の全てのトピックを監視します。全ての新規投稿と新規トピックが通知されまた、新規投稿数がトピックの隣に表示されます。" tracked_categories: "追跡中" - muted_categories: "ミュート中" + tracked_categories_instructions: "自動的にカテゴリー内の全てのトピックを追跡します。新規投稿数がトピックの隣に表示されます。" + watched_first_post_categories: "新規投稿を監視中" + watched_first_post_categories_instructions: "カテゴリー内の新規トピックに対する新規投稿について通知されます。" + watched_first_post_tags: "新規投稿を監視中" + watched_first_post_tags_instructions: "これらのタグの新規トピック内の新規投稿は通知されます。" + muted_categories: "通知しない" + muted_categories_instructions: "カテゴリー内の新規トピックについて何も通知せず、また最新を表示しません。" + no_category_access: "モデレータとしてカテゴリーへのアクセスが制限されたので、保存できませんでした。" delete_account: "アカウントを削除する" delete_account_confirm: "アカウントを削除してもよろしいですか?削除されたアカウントは復元できません。" deleted_yourself: "あなたのアカウントは削除されました。" @@ -476,6 +533,10 @@ ja: muted_users: "ミュート" muted_users_instructions: "ユーザーからの通知をすべて行いません" muted_topics_link: "ミュートしたトピックを表示する" + watched_topics_link: "監視トピックを表示" + tracked_topics_link: "追跡トピックを表示" + api_approved: "承認されました:" + theme: "テーマ" staff_counters: flags_given: "役に立った通報" flagged_posts: "通報した投稿" @@ -506,11 +567,14 @@ ja: error: "(エラー)" action: "パスワードリセット用メールを送信する" set_password: "パスワードを設定する" + choose_new: "新規パスワードを決定" + choose: "パスワードを決定" change_about: title: "プロフィールを変更" error: "変更中にエラーが発生しました。" change_username: title: "ユーザー名を変更" + confirm: "ユーザー名を変更すると、全ての投稿内容とユーザー発言に支障が出ます。本当に実行したいですか?" taken: "このユーザー名は既に使われています。" invalid: "このユーザー名は無効です。英数字のみ利用可能です。" change_email: @@ -538,6 +602,7 @@ ja: instructions: "背景画像は、幅590pxで中央揃えになります" email: title: "メールアドレス" + instructions: "外部には公開しない" ok: "確認用メールを送信します" invalid: "正しいメールアドレスを入力してください" authenticated: "あなたのメールアドレスは {{provider}} によって認証されています" @@ -545,6 +610,7 @@ ja: other: "最後に利用されてから{{count}}分以上経過した場合にメールを送ります。" name: title: "名前" + instructions: "氏名(任意)" instructions_required: "氏名" too_short: "名前が短いです" ok: "問題ありません" @@ -580,6 +646,7 @@ ja: title: "メールの文章の下部に以前の返信を含める" always: "常に" email_digests: + title: "ここを訪れないときは、人気トピックと返信の要約を私宛にメールで送信して下さい。" every_30_minutes: "30分毎" every_hour: "1時間毎" daily: "毎日" @@ -627,16 +694,24 @@ ja: expired: "この招待の有効期限が切れました。" rescind: "削除" rescinded: "取り消された招待" + rescind_all: "全ての招待の取り消し" + rescinded_all: "全ての招待を取り消しました!" + rescind_all_confirm: "本当にすべての招待を取り消したいですか?" reinvite: "招待を再送信する" + reinvite_all: "全招待の再送信" + reinvite_all_confirm: "本当に全ての招待を再送したいですか?" reinvited: "招待を再度送りました。" + reinvited_all: "全ての招待が再送されました!" time_read: "リード時間" days_visited: "訪問日数" account_age_days: "アカウント有効日数" create: "招待を送る" generate_link: "招待リンクをコピー" + valid_for: "招待リンクはこのEメールアドレスにだけ有効です。%{email}" bulk_invite: text: "ファイルからまとめて招待をする" success: "ファイルは無事にアップロードされました。完了されましたらメッセージでお知らせをさせていただきます。" + error: "すみません、ファイルはCSV形式です。" password: title: "パスワード" too_short: "パスワードが短すぎます。" @@ -667,6 +742,7 @@ ja: top_badges: "最近ゲットしたバッジ" no_badges: "まだバッジはありません。" more_badges: "バッジをもっと見る" + no_likes: "好きなものはまだ有りません。" associated_accounts: "関連アカウント" ip_address: title: "最後のIPアドレス" @@ -728,7 +804,9 @@ ja: other: 返信 signup_cta: sign_up: "新しいアカウントを作成" + hide_session: "明日私に思い出させてください。" hide_forever: "いいえ、結構です" + hidden_for_session: "わかりました明日お尋ねします。アカウントを作っていつでもログインできます。" intro: "こんにちは! :heart_eyes: エンジョイしていますね。 ですが、サインアップしていないようです。" value_prop: "アカウントを作成した後、いま読んでいるページへ戻ります。また、新しい投稿があった場合はこことメールにてお知らせします。 いいね!を使って好きな投稿をみんなに教えましょう。 :heartbeat:" summary: @@ -745,6 +823,7 @@ ja: title: "メッセージ" invite: "他の人を招待する..." remove_allowed_user: "このメッセージから {{name}} を削除してもよろしいですか?" + remove_allowed_group: "あなたは本当にこのメッセージからnameを削除したいですか?" email: 'メール' username: 'ユーザ名' last_seen: '最終アクティビティ' @@ -753,6 +832,7 @@ ja: trust_level: 'トラストレベル' search_hint: 'ユーザ名、メールアドレスまたはIPアドレス' create_account: + disclaimer: "登録するとプライバシーポリシー利用規約に同意することになります。" title: "アカウントの作成" failed: "エラーが発生しました。既にこのメールアドレスは使用中かもしれません。「パスワードを忘れました」リンクを試してみてください" forgot_password: @@ -766,6 +846,8 @@ ja: complete_email_found: "%{email}宛にパスワード再設定メールを送信しました。" complete_username_not_found: " %{username}は見つかりませんでした" complete_email_not_found: "%{email}で登録したアカウントがありません。" + button_ok: "OK" + button_help: "ヘルプ" login: title: "ログイン" username: "ユーザ名" @@ -785,6 +867,8 @@ ja: not_allowed_from_ip_address: "このIPアドレスでログインできません" admin_not_allowed_from_ip_address: "そのIPアドレスからは管理者としてログインできません" resend_activation_email: "再度アクティベーションメールを送信するにはここをクリックしてください。" + resend_title: "アクティベーションメールの再送" + change_email: "メールアドレスの変更" sent_activation_email_again: "{{currentEmail}} にアクティベーションメールを再送信しました。メールが届くまで数分掛かることがあります。 (メールが届かない場合は、迷惑メールフォルダの中をご確認ください)。" to_continue: "ログインしてください" preferences: "ユーザ設定を変更するには、ログインする必要があります。" @@ -809,6 +893,19 @@ ja: github: title: "with GitHub" message: "Github による認証 (ポップアップがブロックされていないことを確認してください)" + invites: + accept_title: "招待" + welcome_to: "%{site_name}へようこそ!" + invited_by: "あなたは招待されました:" + accept_invite: "招待に応じる" + success: "あなたのアカウントは作成されて、ログインされました。" + name_label: "氏名" + password_label: "パスワードを設定" + optional_description: "(任意)" + emoji_set: + google: "Google" + twitter: "Twitter" + facebook_messenger: "Facebookメッセンジャー" category_page_style: categories_only: "カテゴリのみ" shortcut_modifier_key: @@ -883,6 +980,8 @@ ja: modal_ok: "OK" modal_cancel: "キャンセル" cant_send_pm: "%{username}へメッセージを送ることはできません。" + yourself_confirm: + body: "たった今このメッセージはあなただけに送られました。" admin_options_title: "このトピックの詳細設定" notifications: title: "@ユーザ名 のメンション、投稿やトピックへの返信、メッセージなどの通知" @@ -937,6 +1036,7 @@ ja: local_tip: "ローカルからアップロードする画像を選択" local_tip_with_attachments: "デバイスから画像/ファイルを選択する {{authorized_extensions}}" hint: "(アップロードする画像をエディタにドラッグ&ドロップすることもできます)" + hint_for_supported_browsers: "ドラッグ&ドロップあるいはエディター内に画像を貼り付けてください。" uploading: "アップロード中" select_file: "ファイル選択" image_link: "イメージのリンク先" @@ -944,20 +1044,34 @@ ja: sort_by: "並べ替え" relevance: "一番関連しているもの" latest_post: "最近の投稿" + latest_topic: "最近のトピック" most_viewed: "最も閲覧されている順" most_liked: "「いいね!」されている順" select_all: "すべて選択する" clear_all: "すべてクリア" + too_short: "検索文字が短すぎます。" title: "トピック、投稿、ユーザ、カテゴリを探す" no_results: "何も見つかりませんでした。" no_more_results: "検索結果は以上です。" searching: "検索中..." post_format: "#{{post_number}} {{username}}から" + results_page: "検索結果" context: user: "@{{username}}の投稿を検索" category: "#{{category}} から検索する" topic: "このトピックを探す" private_messages: "メッセージ検索" + advanced: + posted_by: + label: 投稿者 + in_category: + label: カテゴリー内 + in_group: + label: グループ内 + filters: + posted: 投稿済 + bookmarks: ブックマーク済 + seen: 既読 hamburger_menu: "他のトピック一覧やカテゴリを見る" new_item: "新着" go_back: '戻る' @@ -1052,6 +1166,9 @@ ja: jump_reply_up: 以前の返信へジャンプ jump_reply_down: 以後の返信へジャンプ deleted: "トピックは削除されました" + auto_update_input: + next_week: "来週" + next_month: "来月" auto_close_title: 'オートクローズの設定' timeline: back_description: "未読の最終投稿へ戻る" @@ -1202,7 +1319,10 @@ ja: other: "この {{count}} つの投稿の新しいオーナーを選択してください。(前のオーナー:{{old_user}})" instructions_warn: "この投稿についての通知が新しいユーザにさかのぼって移動されることはないので注意してください。
    警告: 投稿の関連データの所有権は、新しいユーザに転送されません。 注意して使用してください。" change_timestamp: + title: "タイムスタンプを変更してください。" action: "タイムスタンプを変更" + invalid_timestamp: "タイムスタンプは未来時刻にすることが出来ません。" + error: "トピックのタイムスタンプ変更にエラーがありました。" multi_select: select: '選択' selected: '選択中 ({{count}})' @@ -1216,6 +1336,7 @@ ja: post: reply: " {{replyAvatar}} {{usernameLink}}" reply_topic: " {{link}}" + quote_reply: "引用" edit: "編集中 {{link}} {{replyAvatar}} {{username}}" edit_reason: "理由: " post_number: "投稿{{number}}" @@ -1238,11 +1359,16 @@ ja: has_likes_title: other: "{{count}}人のユーザがこの返信に「いいね!」しました" has_likes_title_only_you: "あなたがいいねした投稿" + has_likes_title_you: + other: "あなたと{{count}} 人の人がこの投稿に「いいね!」しました。" errors: create: "申し訳ありませんが、投稿中にエラーが発生しました。もう一度やり直してください。" edit: "申し訳ありませんが、投稿の編集中にエラーが発生しました。もう一度やり直してください。" upload: "申し訳ありません、ファイルのアップロード中にエラーが発生しました。再度お試しください。" + file_too_large: "すみません、ファイルが大きすぎます。(最大サイズは{{max_size_kb}}kbです。)サイズの大きいファイルを共有クラウドサービスにアップロードせず、リンクを共有する方法があります。" too_many_uploads: "申し訳ありませんが、複数のファイルは同時にアップロードできません。" + too_many_dragged_and_dropped_files: "すみません、一度に10ファイルだけアップロードすることが出来ます。" + upload_not_authorized: "すみません、アップロードしようとしているファイルは許可されていません。 (許可された拡張子は: {{authorized_extensions}}です。)" image_upload_not_allowed_for_new_user: "申し訳ありませんが、新規ユーザは画像のアップロードができません。" attachment_upload_not_allowed_for_new_user: "申し訳ありませんが、新規ユーザはファイルの添付ができません。" attachment_download_requires_login: "ファイルをダウンロードするには、ログインする必要があります" @@ -1294,6 +1420,8 @@ ja: off_topic: "関係無い話題として通報" spam: "スパムとして通報" inappropriate: "不適切な発言として通報" + notify_user: "メッセージを送信しました。" + bookmark: "これをブックマークしました。" by_you: off_topic: "関係のない話題として通報しました" spam: "スパム報告として通報しました" @@ -1340,6 +1468,9 @@ ja: delete: confirm: other: "これらの投稿を削除してもよろしいですか?" + merge: + confirm: + other: "これらの{{count}}の投稿を統合したいですか?" revisions: controls: first: "最初のリビジョン" @@ -1616,6 +1747,7 @@ ja: mark_watching: 'm, w トピックをウォッチする' badges: granted_on: "%{date}にゲット!" + others_count: "その他のバッジ所有者(%{count})" title: バッジ badge_count: other: "%{count}個のバッジ" @@ -2199,7 +2331,7 @@ ja: title: "トラストレベル3の条件" value_heading: "値" requirement_heading: "条件" - visits: "訪問" + visits: "アクセス" days: "日" topics_replied_to: "返信したトピック" topics_viewed: "閲覧したトピックの数" @@ -2377,6 +2509,12 @@ ja: edit: "編集" category: "カテゴリへ投稿" settings: "埋め込みの設定" + embed_whitelist_selector: "埋め込みで許可される要素のCSSセレクタ" + embed_blacklist_selector: "埋め込みから削除された要素のCSSセレクタ" + embed_classname_whitelist: "許可されたCSSクラス名" + feed_polling_enabled: "RSS / ATOM経由で投稿をインポートする" + feed_polling_url: "クロールするRSS / ATOMフィードのURL" + feed_polling_frequency_mins: "フィードの更新間隔 (分)" save: "埋め込みの設定を保存" permalink: title: "パーマリンク" @@ -2393,3 +2531,21 @@ ja: label: "新規:" add: "追加" filter: "検索(URL または 外部URL)" + wizard_js: + wizard: + done: "完了" + back: "戻る" + next: "次へ" + step: "%{current} / %{total}" + upload: "アップロード" + uploading: "アップロード中..." + quit: "後で" + staff_count: + other: "コミュニティには%{count}名のスタッフがいます。" + invites: + add_user: "追加" + none_added: "あなたはスタッフを招待していません。 続行してもよろしいですか?" + roles: + admin: "管理者" + moderator: "モデレータ" + regular: "正規ユーザー" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index e1252bb25e..50ec18efed 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -95,7 +95,7 @@ ko: action_codes: public_topic: "이 토픽을 %{when} 에 공개" private_topic: "이 토픽을 %{when} 에 비공개" - split_topic: "이 주제를 ${when}에 분리" + split_topic: "이 토픽을 %{when}에 분리" invited_user: "%{who}이(가) %{when}에 초대됨" invited_group: "%{who} 이(가) %{when} 에 초대됨" removed_user: "%{who}이(가) %{when}에 삭제됨" @@ -121,7 +121,7 @@ ko: banner: enabled: 'made this a banner %{when}. It will appear at the top of every page until it is dismissed by the user.' disabled: '이 배너를 제거했습니다 %{when}. 더 이상 모든 페이지의 상단에 표시되지 않습니다.' - topic_admin_menu: "주제 관리자 기능" + topic_admin_menu: "토픽 관리" wizard_required: "새로운 Discourse에 오신것을 환영합니다! 설치 마법사 로 시작해봅시다✨" emails_are_disabled: "관리자가 이메일 송신을 전체 비활성화 했습니다. 어떤 종류의 이메일 알림도 보내지지 않습니다." bootstrap_mode_enabled: "쉬운 시작을 위해 부트스트랩 모드로 구동 되었습니다. 모든 새로운 사용자에게 신뢰 수준 1이 부여되고 매일 이메일 다이제스트가 보내집니다. 이 기능은 총 사용자 수가 %{min_users}를 초과 할 때 자동으로 꺼집니다." @@ -130,21 +130,22 @@ ko: default_description: "기본값" s3: regions: - us_east_1: "미국 동부 (N. 버지니아)" - us_west_1: "미국 서부 (N. 캘리포니아)" - us_west_2: "미국 서부 (오레곤)" - us_gov_west_1: "AWS GovCloud(US)" - eu_west_1: "유럽연합 (아일랜드)" - eu_west_2: "EU (런던)" - eu_central_1: "유럽연합 (프랑크푸르트)" - ap_southeast_1: "아시아 태평양 (싱가폴)" - ap_southeast_2: "아시아 태평양 (시드니)" - ap_south_1: "아시아 태평양 (뭄바이)" ap_northeast_1: "아시아 태평양 (토쿄)" ap_northeast_2: "아시아 태평양 (서울)" - sa_east_1: "남 아메리카 (상파울로)" + ap_south_1: "아시아 태평양 (뭄바이)" + ap_southeast_1: "아시아 태평양 (싱가폴)" + ap_southeast_2: "아시아 태평양 (시드니)" cn_north_1: "중국 (북경)" - edit: '이 주제의 제목과 카테고리 편집' + eu_central_1: "유럽연합 (프랑크푸르트)" + eu_west_1: "유럽연합 (아일랜드)" + eu_west_2: "EU (런던)" + sa_east_1: "남 아메리카 (상파울로)" + us_east_1: "미국 동부 (N. 버지니아)" + us_east_2: "미국 동부 (오하이오)" + us_gov_west_1: "AWS GovCloud(US)" + us_west_1: "미국 서부 (N. 캘리포니아)" + us_west_2: "미국 서부 (오레곤)" + edit: '이 토픽의 제목과 카테고리 편집' not_implemented: "죄송합니다. 아직 사용할 수 없는 기능입니다." no_value: "아니오" yes_value: "예" @@ -186,7 +187,7 @@ ko: character_count: other: "{{count}} 자" suggested_topics: - title: "추천 주제" + title: "추천 토픽" pm_title: "추천 메세지" about: simple_title: "About" @@ -199,7 +200,7 @@ ko: last_7_days: "지난 7일" last_30_days: "최근 30일" like_count: "좋아요" - topic_count: "주제" + topic_count: "토픽" post_count: "게시글" user_count: "사용자" active_user_count: "활성화된 사용자" @@ -209,21 +210,21 @@ ko: title: "북마크" clear_bookmarks: "북마크 제거" help: - bookmark: "북마크하려면 이 주제의 첫번째 게시글을 클릭하세요" - unbookmark: "북마크를 제거하려면 이 주제의 첫 번째 게시글을 클릭하세요" + bookmark: "클릭하면 이 토픽의 첫번째 포스트가 북마크됩니다" + unbookmark: "클릭하면 이 토픽에 속한 모든 북마크가 제거됩니다" bookmarks: not_logged_in: "죄송합니다. 게시물을 즐겨찾기에 추가하려면 로그인을 해야 합니다." created: "이 게시글을 북마크 하였습니다." not_bookmarked: "이 게시물을 읽으셨습니다. 즐겨찾기에 추가하려면 클릭하세요." last_read: "마지막으로 읽으신 게시물입니다. 즐겨찾기에 추가하려면 클릭하세요." remove: "북마크 삭제" - confirm_clear: "정말 이 주제의 모든 북마크를 제거하시겠습니까?" + confirm_clear: "정말로 이 토픽의 모든 북마크를 제거할까요?" topic_count_latest: - other: "{{count}} 새 주제 혹은 업데이트된 주제" + other: "{{count}}개의 새 토픽이나 업데이트된 토픽" topic_count_unread: - other: "{{count}} 읽지 않은 주제" + other: "{{count}}개의 읽지 않은 토픽" topic_count_new: - other: "{{count}}개의 새로운 주제" + other: "{{count}}개의 새로운 토픽" click_to_show: "보려면 클릭하세요." preview: "미리보기" cancel: "취소" @@ -245,12 +246,12 @@ ko: close: "배너 닫기" edit: "이 배너 수정 >>" choose_topic: - none_found: "주제를 찾을 수 없습니다." + none_found: "토픽을 찾을 수 없습니다." title: - search: "이름, url, ID로 주제 검색" - placeholder: "여기에 주제 제목을 입력하세요" + search: "이름, url, ID로 토픽 검색" + placeholder: "토픽 제목을 입력하세요" queue: - topic: "주제:" + topic: "토픽:" approve: '승인' reject: '거절' delete_user: '사용자 삭제' @@ -260,7 +261,7 @@ ko: cancel: "취소" view_pending: "대기중인 게시글" has_pending_posts: - other: "이 주제에는 {{count}}개의 승인 대기중인 게시글이 있습니다." + other: "이 토픽에는 {{count}}개의 승인 대기중인 포스트가 있습니다" confirm: "변경사항 저장" delete_prompt: "정말로 %{username}; 회원을 삭제하시겠습니까? 게시글이 모두 삭제되고 IP와 이메일이 차단됩니다." approval: @@ -270,12 +271,12 @@ ko: other: "대기중인 게시글이 {{count}}개 있습니다." ok: "확인" user_action: - user_posted_topic: "{{user}}님이 주제를 게시함" - you_posted_topic: "주제를 게시함" + user_posted_topic: "{{user}}님이 토픽을 게시했습니다" + you_posted_topic: "회원님토픽을 게시했습니다" user_replied_to_post: "{{user}}님이 {{post_number}} 게시글에 답글 올림" you_replied_to_post: "{{post_number}} 게시글에 답글 올림" - user_replied_to_topic: "{{user}}님이 주제에 답글 올림" - you_replied_to_topic: "주제에 답글 올림" + user_replied_to_topic: "{{user}}님이 토픽에 답글을 올렸습니다" + you_replied_to_topic: "회원님토픽에 댓글을 달았습니다" user_mentioned_user: "{{user}}님이 {{another_user}}를 멘션함" user_mentioned_you: "{{user}}님이 를 멘션함" you_mentioned_user: "{{another_user}}님을 멘션함" @@ -289,10 +290,10 @@ ko: likes_given: "제공한" likes_received: "받은" topics_entered: "읽음" - topics_entered_long: "읽은 주제" + topics_entered_long: "읽은 토픽 수" time_read: "읽은 시간" - topic_count: "주제" - topic_count_long: "생성된 주제" + topic_count: "토픽" + topic_count_long: "생성된 토픽" post_count: "답글" post_count_long: "답글" no_results: "결과가 없습니다." @@ -316,7 +317,7 @@ ko: action: "액션" acting_user: "활동하는 사용자" target_user: "대상 사용자" - subject: "주제" + subject: "제목" details: "자세한 내용" from: "부터" to: "까지" @@ -332,7 +333,7 @@ ko: members: "이 그룹에는 구성원이 없습니다." mentions: "이 그룹에서는 언급이 없습니다." messages: "이 그룹에 대한 메시지는 없습니다." - topics: "이 그룹의 구성원이 작성한 주제글이 없습니다." + topics: "이 그룹의 멤버가 작성한 토픽이 없습니다." logs: "이 그룹에 대한 기록이 없습니다." add: "추가" join: "그룹 참여하기" @@ -355,7 +356,7 @@ ko: other: "그룹" activity: "활동" members: "멤버" - topics: "주제" + topics: "토픽" posts: "게시글" mentions: "멘션" messages: "메시지" @@ -405,7 +406,7 @@ ko: '1': "선사한 '좋아요'" '2': "받은 '좋아요'" '3': "북마크" - '4': "주제" + '4': "토픽" '5': "답글" '6': "응답" '7': "멘션" @@ -435,9 +436,9 @@ ko: toggle_ordering: "정렬 컨트롤 토글" subcategories: "하위 카테고리" topic_sentence: - other: "%{count}개의 주제" + other: "%{count}개의 토픽" topic_stat_sentence: - other: "지난 %{unit} 동안 %{count}개의 새로운 주제가 있습니다." + other: "지난 %{unit} 동안 %{count}개의 새로운 토픽이 생겼습니다." ip_lookup: title: IP 주소 Lookup hostname: Hostname @@ -520,7 +521,7 @@ ko: tracked_tags: "추적하기" tracked_tags_instructions: "이 태그가 붙은 모든 토픽을 추적하도록 자동설정됩니다. 새로운 게시글의 수가 토픽 옆에 표시됩니다." muted_tags: "알람 끄기" - muted_tags_instructions: "이 태그의 새 주제에 대해 어떠한 알림도 받을 수 없으며, 최근의 주제도 나타나지 않습니다." + muted_tags_instructions: "이 태그가 달린 토픽에 대해 알림을 받지 않으며, 최근 토픽란에도 나타나지 않습니다." watched_categories: "지켜보기" watched_categories_instructions: "이 카테고리의 모든 토픽을 주시하도록 자동 설정됩니다. 새로운 게시글이나 토픽에 대하여 알림을 받게 되며, 토픽 옆에 새로운 게시글의 수가 표시됩니다." tracked_categories: "추적하기" @@ -530,7 +531,7 @@ ko: watched_first_post_tags: "첫번째 글 보기" watched_first_post_tags_instructions: "이 태그가 달린 토픽이 생길 때마다 알림을 받습니다." muted_categories: "알림 끄기" - muted_categories_instructions: "이 카테고리 내의 새 주제에 대해 어떠한 알림도 받을 수 없으며, 최근의 주제도 나타나지 않습니다." + muted_categories_instructions: "이 카테고리 내의 새 토픽에 대해 알림을 받지 않으며, 최근 토픽란에도 나타나지 않습니다." no_category_access: "운영자는 이 카테고리 접근에 제약을 받습니다. 저장이 해제 됩니다." delete_account: "내 계정 삭제" delete_account_confirm: "정말로 계정을 삭제할까요? 이 작업은 되돌릴 수 없습니다." @@ -541,10 +542,10 @@ ko: users: "회원" muted_users: "알람 끄기" muted_users_instructions: "이 회원이 보낸 알림 모두 숨김" - muted_topics_link: "알림을 끈 주제 보기" + muted_topics_link: "알림을 끈 토픽 보기" watched_topics_link: "읽은 토픽 보기" tracked_topics_link: "관심있는 토픽 보기" - automatically_unpin_topics: "글 끝에 다다르면 자동으로 주제 고정을 해제합니다." + automatically_unpin_topics: "글 끝에 다다르면 자동으로 토픽 고정을 해제합니다." apps: "앱" revoke_access: "접근권한 회수" undo_revoke_access: "접근권환 회수 취소" @@ -719,7 +720,7 @@ ko: pending: "초대를 보류합니다." pending_tab: "보류" pending_tab_with_count: "지연 ({{count}})" - topics_entered: "읽은 주제" + topics_entered: "읽은 토픽" posts_read_count: "글 읽기" expired: "이 초대장의 기한이 만료되었습니다." rescind: "삭제" @@ -773,7 +774,7 @@ ko: top_replies: "인기 댓글" no_replies: "아직 답글이 없습니다." more_replies: "답글 더 보기" - top_topics: "인기 주제" + top_topics: "인기 토픽" no_topics: "아직 주제가 없습니다." more_topics: "주제 더 보기" top_badges: "인기 배지" @@ -972,7 +973,7 @@ ko: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" - google_classic: "Google Classic" + google_classic: "Google 클래식" facebook_messenger: "Facebook 메신저" category_page_style: categories_only: "카테고리만" @@ -982,6 +983,15 @@ ko: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + people: 인물 + nature: 자연 + food: 음식 + activity: 활동 + travel: 여행 + objects: 사물 + celebration: 축하 + custom: 커스텀 emoji composer: emoji: "이모지 :)" more_emoji: "더보기..." @@ -1303,7 +1313,7 @@ ko: toggle_information: "주제의 세부 정보를 토글합니다." read_more_in_category: "더 읽을거리가 필요하신가요? {{catLink}} 또는 {{latestLink}}를 살펴보세요." read_more: "{{catLink}} 또는 {{latestLink}}에서 더 많은 토픽들을 찾으실 수 있습니다" - read_more_MF: "이 카테고리에 { UNREAD, plural, =0 {} one { is 1개의 안 읽은 } other { are #개의 안 읽은 } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} 1개의 새로운 주제가} other { {BOTH, select, true{and } false {are } other{}} # 새로운 주제가} } 남아 있고, {CATEGORY, select, true {{catLink} 주제도 확인해보세요.} false {{latestLink}} other {}}" + read_more_MF: "이 카테고리에 { UNREAD, plural, =0 {} one { is 1개의 안 읽은 } other { #개의 안 읽은 } } { NEW, plural, =0 {} one { {BOTH, select, true{ 그리고 } false {} other{}} 1개의 새로운 토픽이} other { {BOTH, select, true{ 그리고 } false {} other{}} # 새로운 토픽이} } 남아 있습니다. {CATEGORY, select카테고리, true {{catLink} 카테고리의 다른 토픽도 확인해보세요.} false {{latestLink}} 다른 토픽도 확인해 보세요 {}}" browse_all_categories: 모든 카테고리 보기 view_latest_topics: 최신 주제 보기 suggest_create_topic: 새 주제를 작성 해 보실래요? @@ -1501,6 +1511,7 @@ ko: success_email: "{{emailOrUsername}}로 초대장을 발송했습니다. 초대를 수락하면 알려 드리겠습니다. 초대상태를 확인하려면 사용자 페이지에서 '초대장' 탭을 선택하세요." success_username: "사용자가 이 주제에 참여할 수 있도록 초대했습니다." error: "그 사람을 초대할 수 없습니다. 혹시 이미 초대하진 않았나요? (Invites are rate limited)" + success_existing_email: "해당 email을 사용하고 있는 사용자 {{emailOrUsername}} 가 이미 존재합니다. 그 사용자를 이 토픽에 참여하도록 초청했습니다." login_reply: '로그인하고 답글 쓰기' filters: n_posts: @@ -1895,12 +1906,6 @@ ko: help: "이 주제는 목록에서 제외됩니다. 주제 목록에 표시되지 않으며 링크를 통해서만 접근 할 수 있습니다." posts: "글" posts_long: "이 주제의 글 수는 {{number}}개 입니다." - posts_likes_MF: | - This topic has {count, plural, one {1 reply} other {# replies}} {ratio, select, - low {with a high like to post ratio} - med {with a very high like to post ratio} - high {with an extremely high like to post ratio} - other {}} original_post: "원본 글" views: "조회수" views_lowercase: @@ -2169,7 +2174,7 @@ ko: admin_js: type_to_filter: "필터를 입력하세요" admin: - title: 'Discourse 운영' + title: 'Discourse 관리자' moderator: '운영자' dashboard: title: "대시보드" @@ -2253,8 +2258,8 @@ ko: clear_topic_flags_title: "주제 조사를 끝냈고 이슈를 해결했습니다. 신고를 지우기 위해 완료를 클릭하세요" more: "(더 많은 답글...)" dispositions: - agreed: "agreed" - disagreed: "disagreed" + agreed: "동의" + disagreed: "반대" deferred: "유예 중인" flagged_by: "신고 한 사람" resolved_by: "해결 by" @@ -2303,7 +2308,7 @@ ko: automatic: "자동화" automatic_membership_email_domains: "이 목록의 있는 항목과 사용자들이 등록한 이메일 도메인이 일치할때 이 그룹에 포함" automatic_membership_retroactive: "이미 등록된 사용자에게 같은 이메일 도메인 규칙 적용하기" - default_title: "Default title for all users in this group" + default_title: "이 그룹의 모든 사용자를 위한 기본 제목" primary_group: "Automatically set as primary group" group_owners: 소유자 add_owners: 소유자 추가하기 @@ -2322,7 +2327,7 @@ ko: revoke: "폐지" confirm_regen: "API 키를 새로 발급 받으시겠습니까?" confirm_revoke: "API 키를 폐지하겠습니까?" - info_html: "당신의 API 키는 JSON콜을 이용하여 주제를 생성하거나 수정할 수 있습니다." + info_html: "당신의 API 키는 JSON호출을 이용하여 토픽을 생성하거나 수정할 수 있습니다." all_users: "전체 유저" note_html: "이 키의 보안에 특별히 주의하세요. 이 키를 아는 모든 사용자는 다른 사용자의 이름으로 글을 작성할 수 있습니다." web_hooks: @@ -2596,7 +2601,7 @@ ko: description: "사이트 헤더에 텍스트와 아이콘" highlight: name: '하이라이트' - description: '페이지 내에 강조된 글 및 주제 등의 배경색' + description: '포스트나 토픽과 같은 페이지 내에서 강조된 요소의 배경색' danger: name: '위험' description: '글 삭제 등에 사용되는 강조색' @@ -2676,7 +2681,7 @@ ko: last_match_at: "마지막 방문" match_count: "방문" ip_address: "IP" - topic_id: "주제 ID" + topic_id: "토픽 ID" post_id: "글 ID" category_id: "카테고리 ID" delete: '삭제' @@ -2716,7 +2721,7 @@ ko: grant_badge: "배지 부여" revoke_badge: "배지 회수" check_email: "이메일 확인" - delete_topic: "주제 삭제" + delete_topic: "토픽 삭제" delete_post: "글 삭제" impersonate: "대역" anonymize_user: "anonymize user" @@ -2862,10 +2867,10 @@ ko: activity: 활동 like_count: 준/받은 '좋아요' last_100_days: '지난 100일간' - private_topics_count: 비공개 주제 수 + private_topics_count: 비공개 토픽 posts_read_count: 글 읽은 수 post_count: 글 수 - topics_entered: 읽은 주제 수 + topics_entered: 읽은 토픽 수 flags_given_count: 작성한 신고 flags_received_count: 받은 신고 warnings_received_count: 받은 경고 @@ -2902,7 +2907,7 @@ ko: deactivate_failed: "사용자 비활성에 문제가 있습니다." unblock_failed: '사용자 언블락에 문제가 있습니다.' block_failed: '사용자 블락에 문제가 있습니다.' - block_confirm: '정말로 이 사용자를 차단하겠습니까? 차단된 사용자는 어떠한 주제나나 글도 작성할 수 없습니다.' + block_confirm: '정말로 이 사용자를 차단하겠습니까? 차단된 사용자는 토픽, 포스트 작성이 금지됩니다.' block_accept: '네, 이 사용자를 차단합니다.' bounce_score: "반송 스코어" reset_bounce_score: @@ -2911,7 +2916,7 @@ ko: visit_profile: "사용자 환경설정 페이지로 가서 프로필 수정하기" deactivate_explanation: "비활성화 사용자는 이메일 인증을 다시 받아야합니다." suspended_explanation: "접근 금지된 유저는 로그인 할 수 없습니다." - block_explanation: "차단된 사용자는 글을 작성하거나 주제를 작성할 수 없습니다." + block_explanation: "차단된 사용자는 글을 작성하거나 토픽을 작성할 수 없습니다." staged_explanation: "격리조치된 회원은 특정 토픽에 한해서 이메일로만 글을 쓸 수 있습니다." bounce_score_explanation: none: "최근 해당 이메일로부터 반송메일을 받은 내역이 없습니다." @@ -2933,9 +2938,9 @@ ko: requirement_heading: "자격요건" visits: "방문횟수" days: "일" - topics_replied_to: "댓글 달은 주제 개수" - topics_viewed: "열어본 주제 개수" - topics_viewed_all_time: "열어본 주제 개수 (전체 기간)" + topics_replied_to: "댓글을 단 토픽 수" + topics_viewed: "열어본 토픽 수" + topics_viewed_all_time: "열어본 토픽 수 (전체 기간)" posts_read: "읽은 글 갯수" posts_read_all_time: "읽은 글 갯수 (전체 기간)" flagged_posts: "신고당한 글 갯수" @@ -3052,7 +3057,7 @@ ko: modal_title: 배지 그룹으로 나누기 granted_by: 배지 부여자 granted_at: 배지 수여일 - reason_help: (주제 또는 댓글로 가는 링크) + reason_help: (토픽이나 포스트로 가는 링크) save: 저장 delete: 삭제 delete_confirm: 정말로 이 배지를 삭제할까요? @@ -3114,7 +3119,7 @@ ko: embedding: get_started: "다른 웹사이트에 Discourse를 임베드하려면 호스트 추가부터 하세요" confirm_delete: "정말로 host를 삭제할까요?" - sample: "Discourse 주제를 웨사이트에 삽입(embed)하기 위해 다음 HTML코드를 이용하세요. REPLACE_ME 부분을 당신이 삽입하려는 웹사이트의 정식URL로 교체 하면 됩니다." + sample: "Discourse 토픽을 웹사이트에 삽입(embed)하기 위해 다음 HTML코드를 이용하세요. REPLACE_ME 부분을 당신이 삽입하려는 웹사이트의 정식URL로 교체 하면 됩니다." title: "삽입(Embedding)" host: "허용 Host" class_name: "클래스 이름" @@ -3127,7 +3132,7 @@ ko: feed_description: "당신 사이트의 RSS/ATOM 피드를 알려주시면 Discourse가 그 사이트 컨텐트를 더 잘 가져올 수 있습니다." crawling_settings: "크롤러 설정" crawling_description: "When Discourse creates topics for your posts, if no RSS/ATOM feed is present it will attempt to parse your content out of your HTML. Sometimes it can be challenging to extract your content, so we provide the ability to specify CSS rules to make extraction easier." - embed_by_username: "주제 생성 시 사용할 회원 이름" + embed_by_username: "토픽을 만들 때 사용할 아이디" embed_post_limit: "삽입(embed)할 글 최대갯수" embed_username_key_from_feed: "피드에서 discourse usename을 꺼내오기 위한 키(key)" embed_title_scrubber: "게시글의 제목을 긁어오기 위해 정규표현식이 사용되었습니다" @@ -3142,8 +3147,8 @@ ko: permalink: title: "고유링크" url: "URL" - topic_id: "주제 ID" - topic_title: "주제" + topic_id: "토픽 ID" + topic_title: "토픽" post_id: "글 ID" post_title: "글" category_id: "카테고리 ID" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index a9a95233dd..6a900ca6fd 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -148,20 +148,21 @@ nb_NO: default_description: "Forvalg" s3: regions: - us_east_1: "USA øst (N. Virginia)" - us_west_1: "USA vest (N. California)" - us_west_2: "USA vest (Oregon)" - us_gov_west_1: "AWS GovCloud (USA)" - eu_west_1: "EU (Irland)" - eu_west_2: "EU (London)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia/Stillehavsregionen (Singapore)" - ap_southeast_2: "Asia/Stillehavsregionen (Sydney)" - ap_south_1: "Asia/Stillehavsregionen (Mumbai)" ap_northeast_1: "Asia/Stillehavsregionen (Tokyo)" ap_northeast_2: "Asia/Stillehavsregionen (Seoul)" - sa_east_1: "Asia/Stillehavsregionen (Sao Paulo)" + ap_south_1: "Asia/Stillehavsregionen (Mumbai)" + ap_southeast_1: "Asia/Stillehavsregionen (Singapore)" + ap_southeast_2: "Asia/Stillehavsregionen (Sydney)" cn_north_1: "Kina (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irland)" + eu_west_2: "EU (London)" + sa_east_1: "Asia/Stillehavsregionen (Sao Paulo)" + us_east_1: "USA øst (N. Virginia)" + us_east_2: "USA øst (Ohio)" + us_gov_west_1: "AWS GovCloud (USA)" + us_west_1: "USA vest (N. California)" + us_west_2: "USA vest (Oregon)" edit: 'rediger tittelen og kategorien til dette emnet' not_implemented: "Beklager, denne funksjonen har ikke blitt implementert enda." no_value: "Nei" @@ -352,6 +353,8 @@ nb_NO: add_members: "Legg til medlemmer" delete_member_confirm: "Fjern '%{username}' fra gruppen '%{group}'?" name_placeholder: "Gruppenavn, ingen mellomrom, samme regler som for brukernavn" + public_admission: "Tillat brukere å ta del i gruppen fritt (krever offentlig synlig gruppe)" + public_exit: "Tillat brukere å fritt forlate gruppen" empty: posts: "Det er ingen innlegg av medlemmer i denne gruppen." members: "Det er ingen medlemmer av denne gruppen." @@ -363,6 +366,7 @@ nb_NO: join: "Bli medlem av gruppe" leave: "Forlat gruppe" request: "Be om gruppemedlemskap" + message: "Melding" automatic_group: Automatisk gruppe closed_group: Lukket gruppe is_group_user: "Du er medlem av denne gruppen" @@ -386,6 +390,12 @@ nb_NO: mentions: "Omtalelser" messages: "Meldinger" notification_level: "Forvalgt merknadsnivå for gruppemeldinger" + visibility_levels: + title: "Hvem kan se denne gruppen?" + public: "Alle" + members: "Gruppeeiere, medlemmer og administratorer" + staff: "Gruppemedlemmer og personale" + owners: "Gruppeeiere og administratorer" alias_levels: title: "Hvem kan sende meldinger til og @nevne denne gruppen?" nobody: "Ingen" @@ -1005,6 +1015,8 @@ nb_NO: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Klassisk Google" + facebook_messenger: "Facebook Meldingstjeneste" category_page_style: categories_only: "Kun kategorier" categories_with_featured_topics: "Kategorier med framhevede emner" @@ -1013,6 +1025,23 @@ nb_NO: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Søk etter emoji + people: Folk + nature: Natur + food: Mat + activity: Aktivitet + travel: Reise + objects: Objekter + celebration: Feiring + custom: Egendefinerte emoji-er + recent: Nylig brukt + default_tone: Ingen hudtone + light_tone: Lys hudtone + medium_light_tone: Middels lys hudtone + medium_tone: Middels hudtone + medium_dark_tone: Middels mørk hudtone + dark_tone: Mørk hudtone composer: emoji: "Emoji :)" more_emoji: "mer…" @@ -1178,8 +1207,8 @@ nb_NO: clear_all: "Fjern Alle" too_short: "Din søketekst er for kort." result_count: - one: "Ett resultat for \"{{term}}\"" - other: "{{count}} resultater for \"{{term}}\"" + one: "1 resultat for {{term}}" + other: "{{count}} resultater for {{term}}" title: "søk etter emner, innlegg, brukere eller kategorier" no_results: "Ingen resultater funnet." no_more_results: "Ingen flere resultater funnet." @@ -1547,6 +1576,7 @@ nb_NO: success_email: "Vi har sendt ut en invitasjon til {{emailOrUsername}}. Vi varsler deg når invitasjonen er godtatt. Sjekk invitiasjonsfanen på brukersiden din for å beholde oversikten over invitasjonene dine." success_username: "Vi har invitert brukeren til å delta i dette emnet." error: "Beklager, vi kunne ikke invitere den brukeren. De har muligens allerede blitt invitert?" + success_existing_email: "En bruker med e-posten {{emailOrUsername}} finnes allerede. Vi har invitert den brukeren til å ta del i emnet." login_reply: 'Logg Inn for å svare' filters: n_posts: @@ -2879,6 +2909,30 @@ nb_NO: title: "Lager nye blokkeringsoppføringer for subnett hvis det er minst 'min_ban_entries_for_roll_up' oppføringer." logster: title: "Feillogg" + watched_words: + title: "Ord som holdes oppsyn med" + search: "søk" + clear_filter: "Tøm" + show_words: "vis ord" + word_count: + one: "ett ord" + other: "%{count} ord" + actions: + block: 'Blokker' + censor: 'Sensurer' + require_approval: 'Krev godkjenning' + flag: 'Flagg' + action_descriptions: + block: 'Forhindre innlegg som inneholder disse ordene fra å bli postet. Brukeren vil se en feilmelding når vedkommende prøver å sende inn innlegget sitt.' + censor: 'Tillat innlegg som inneholder disse ordene, men erstatt dem med bokstaver som skjuler de sensurerte ordene.' + require_approval: 'Det kreves at innlegg som inneholder disse ordene godkjennes av staben før de kan ses.' + flag: 'Tillat innlegg som inneholder disse ordene, men flagg dem som upassende slik at moderatorer kan se over dem.' + form: + label: 'Nytt ord:' + add: 'Legg til' + success: 'Suksess' + upload: "Last opp" + upload_successful: "Opplastet. Ord lagt til." impersonate: title: "Fremstå som" help: "Bruk dette verktøyet for å fremstå som en annen bruker for feilsøking. Du må logge ut når du er ferdig." diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index c705af7c16..1d99a5b67a 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -28,14 +28,14 @@ nl: dates: time: "HH:mm" timeline_date: "MMM YYYY" - long_no_year: "D MMM [om] HH:mm" + long_no_year: "D MMM HH:mm" long_no_year_no_time: "D MMM" full_no_year_no_time: "D MMMM" - long_with_year: "D MMM YYYY [om] HH:mm" + long_with_year: "D MMM YYYY HH:mm" long_with_year_no_time: "D MMM YYYY" full_with_year_no_time: "D MMMM YYYY" - long_date_with_year: "D MMM 'YY [om] HH:mm" - long_date_without_year: "D MMM [om] HH:mm" + long_date_with_year: "D MMM 'YY HH:mm" + long_date_without_year: "D MMM HH:mm" long_date_with_year_without_time: "D MMM 'YY" long_date_without_year_with_linebreak: "D MMM
    HH:mm" long_date_with_year_with_linebreak: "D MMM 'YY
    HH:mm" @@ -140,36 +140,37 @@ nl: enabled: 'heeft deze banner gemaakt op %{when}. De banner verschijnt bovenaan elke pagina, totdat de gebruiker deze verbergt.' disabled: 'heeft deze banner verwijderd op %{when}. De banner zal niet meer bovenaan elke pagina verschijnen.' topic_admin_menu: "beheeracties voor topic" - wizard_required: "Welkom bij uw nieuwe Discourse! Laten we beginnen met de configuratiewizard ✨" + wizard_required: "Welkom bij je nieuwe Discourse! Laten we beginnen met de configuratiewizard ✨" emails_are_disabled: "Alle uitgaande e-mail is uitgeschakeld door een beheerder. Er zal geen enkele e-mailmelding worden verstuurd." - bootstrap_mode_enabled: "Om het lanceren van uw website makkelijker te maken, bevindt u zich in bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend, en dagelijkse e-mailsamenvattingen zijn voor hen ingeschakeld. Dit wordt automatisch uitgeschakeld wanneer het totale gebruikersaantal %{min_users} gebruikers overschrijdt." + bootstrap_mode_enabled: "Om het lanceren van je website makkelijker te maken, bevind je je in bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend en dagelijkse e-mailsamenvattingen zijn voor hen ingeschakeld. Dit wordt automatisch uitgeschakeld wanneer het totale gebruikersaantal %{min_users} gebruikers overschrijdt." bootstrap_mode_disabled: "De bootstrapmodus zal in de komende 24 uur worden uitgeschakeld." themes: default_description: "Standaard" s3: regions: - us_east_1: "VS Oost (N. Virginia)" - us_west_1: "VS West (N. Californië)" - us_west_2: "VS West (Oregon)" - us_gov_west_1: "AWS GovCloud (VS)" - eu_west_1: "EU (Ierland)" - eu_west_2: "EU (Londen)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Azië Pacifisch (Singapore)" - ap_southeast_2: "Azië Pacifisch (Sydney)" - ap_south_1: "Azië Pacifisch (Bombay)" ap_northeast_1: "Azië Pacifisch (Tokio)" ap_northeast_2: "Azië Pacifisch (Seoel)" - sa_east_1: "Zuid-Amerika (Sao Paulo)" + ap_south_1: "Azië Pacifisch (Bombay)" + ap_southeast_1: "Azië Pacifisch (Singapore)" + ap_southeast_2: "Azië Pacifisch (Sydney)" cn_north_1: "China (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ierland)" + eu_west_2: "EU (Londen)" + sa_east_1: "Zuid-Amerika (Sao Paulo)" + us_east_1: "VS Oost (N. Virginia)" + us_east_2: "VS Oost (Ohio)" + us_gov_west_1: "AWS GovCloud (VS)" + us_west_1: "VS West (N. Californië)" + us_west_2: "VS West (Oregon)" edit: 'de titel en categorie van dit topic bewerken' - not_implemented: "Die functie is nog niet geïmplementeerd, sorry!" + not_implemented: "Deze functie is helaas nog niet beschikbaar, sorry!" no_value: "Nee" yes_value: "Ja" generic_error: "Sorry, er is iets fout gegaan." generic_error_with_reason: "Er is iets fout gegaan: %{error}" sign_up: "Registreren" - log_in: "Aanmelden" + log_in: "Inloggen" age: "Leeftijd" joined: "Lid sinds" admin_title: "Beheer" @@ -184,10 +185,10 @@ nl: guidelines: "Richtlijnen" privacy_policy: "Privacybeleid" privacy: "Privacy" - terms_of_service: "Servicevoorwaarden" + terms_of_service: "Algemene Voorwaarden" mobile_view: "Mobiele weergave" desktop_view: "Desktopweergave" - you: "U" + you: "Jij" or: "of" now: "zojuist" read_more: 'meer info' @@ -232,12 +233,12 @@ nl: bookmark: "Klik om een bladwijzer voor het eerste bericht van dit topic te maken" unbookmark: "Klik om alle bladwijzers in dit topic te verwijderen" bookmarks: - not_logged_in: "sorry, u moet aangemeld zijn om bladwijzers voor berichten te maken" - created: "u hebt een bladwijzer voor dit bericht gemaakt" - not_bookmarked: "u hebt dit bericht gelezen; klik om er een bladwijzer voor te maken" - last_read: "dit is het laatste bericht dat u hebt gelezen; klik om er een bladwijzer voor te maken" + not_logged_in: "sorry, je moet ingelogd zijn om berichten aan je favorieten toe te kunnen voegen" + created: "je hebt een bladwijzer voor dit bericht gemaakt" + not_bookmarked: "je hebt dit bericht gelezen; klik om er een bladwijzer voor te maken" + last_read: "dit is het laatste bericht dat je hebt gelezen; klik om er een bladwijzer voor te maken" remove: "Bladwijzer verwijderen" - confirm_clear: "Weet u zeker dat u alle bladwijzers van dit topic wilt verwijderen?" + confirm_clear: "Weet je zeker dat je alle bladwijzers van dit topic wilt verwijderen?" topic_count_latest: one: "{{count}} nieuwe of aangepaste discussie." other: "{{count}} nieuwe of bijgewerkte topics." @@ -249,7 +250,7 @@ nl: other: "{{count}} nieuwe topics." click_to_show: "Klik om te bekijken." preview: "voorbeeld" - cancel: "annuleren" + cancel: "annuleer" save: "Wijzigingen opslaan" saving: "Opslaan..." saved: "Opgeslagen!" @@ -352,6 +353,7 @@ nl: add_members: "Leden toevoegen" delete_member_confirm: "'%{username}' uit de groep '%{group}' verwijderen?" name_placeholder: "Groepsnaam, geen spaties, hetzelfde als gebruikersnaamregel" + public: "Gebruikers toestaan om bij groepen aan te melden of deze te verlaten (Groep moet hiervoor publiekelijk zichtbaar zijn)" empty: posts: "Er zijn geen berichten van leden van deze groep." members: "Er zijn geen leden in deze groep." @@ -386,6 +388,12 @@ nl: mentions: "Vermeldingen" messages: "Berichten" notification_level: "Standaard meldingsniveau voor groepsberichten" + visibility_levels: + title: "Voor wie is deze groep zichtbaar?" + public: "Iedereen" + members: "Groepseigenaars, -beheerders en -leden" + staff: "Groepseigenaars en -personeel" + owners: "Groepeigenaars en -beheerders" alias_levels: title: "Wie kan deze groep een bericht sturen en taggen?" nobody: "Niemand" @@ -483,6 +491,7 @@ nl: mute: "Negeren" edit: "Voorkeuren bewerken" download_archive: + button_text: "Alles downloaden" confirm: "Weet u zeker dat u uw berichten wilt downloaden?" success: "Downloaden is gestart; u ontvangt een melding zodra het proces is voltooid." rate_limit_error: "Berichten kunnen maar één keer per dag worden gedownload; probeer het morgen opnieuw." @@ -552,6 +561,7 @@ nl: watched_first_post_tags_instructions: "U ontvangt een melding bij het eerste bericht in elk nieuw topic met deze tags." muted_categories: "Genegeerd" muted_categories_instructions: "U ontvangt geen melding over nieuwe topics en berichten in deze categorieën, en ze verschijnen niet in Nieuwste." + no_category_access: "Als een beheerder heeft u beperkte toegang tot categorieën, opslaan is uitgeschakeld." delete_account: "Mijn account verwijderen" delete_account_confirm: "Weet u zeker dat u uw account definitief wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt!" deleted_yourself: "Uw account is met succes verwijderd." @@ -746,6 +756,9 @@ nl: expired: "Deze uitnodiging is verlopen." rescind: "Verwijderen" rescinded: "Uitnodiging verwijderd" + rescind_all: "Verwijder alle uitnodigingen" + rescinded_all: "Alle uitnodigingen verwijderd!" + rescind_all_confirm: "Weet u zeker dat u alle uitnodigingen wilt verwijderen?" reinvite: "Uitnodiging opnieuw versturen" reinvite_all: "Alle uitnodigingen opnieuw versturen" reinvite_all_confirm: "Weet u zeker dat u alle uitnodigingen opnieuw wilt versturen?" @@ -928,6 +941,8 @@ nl: complete_email_found: "We hebben een account gevonden met het e-mailadres %{email}. U zou spoedig een e-mail moeten ontvangen met instructies om uw wachtwoord opnieuw in te stellen." complete_username_not_found: "Geen account met de gebruikersnaam %{username} gevonden" complete_email_not_found: "Geen account met het e-mailadres %{email} gevonden" + button_ok: "OK" + button_help: "Help" login: title: "Aanmelden" username: "Gebruiker" @@ -986,6 +1001,9 @@ nl: your_email: "Het e-mailadres van uw account is %{email}." accept_invite: "Uitnodiging accepteren" success: "Uw account is gemaakt en u bent nu aangemeld." + name_label: "Naam" + password_label: "Wachtwoord instellen" + optional_description: "(optioneel)" password_reset: continue: "Doorgaan naar %{site_name}" emoji_set: @@ -994,6 +1012,8 @@ nl: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google Klassiek" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Alleen categorieën" categories_with_featured_topics: "Categorieën met aanbevolen topics" @@ -1002,12 +1022,24 @@ nl: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Zoek naar emoji + people: Mensen + nature: Natuur + food: Voeding + activity: Activiteit + travel: Reizen + objects: Objecten + celebration: Viering + custom: Eigen emojis + recent: Recent gebruikt composer: emoji: "Emoji :)" more_emoji: "meer..." options: "Opties" whisper: "fluisteren" unlist: "onzichtbaar" + blockquote_text: "Citaat" add_warning: "Dit is een officiële waarschuwing." toggle_whisper: "Fluistermodus in-/uitschakelen" toggle_unlisted: "Onzichtbaar in-/uitschakelen" @@ -1205,6 +1237,7 @@ nl: seen: die ik heb gelezen unseen: die ik niet heb gelezen wiki: die een wiki zijn + all_tags: Bevat alle tags statuses: label: Waarin topics open: open zijn @@ -1534,6 +1567,7 @@ nl: success_email: "Er is een uitnodiging naar {{emailOrUsername}} gemaild. We stellen u op de hoogte als op de uitnodiging is ingegaan. Controleer het uitnodigingentabblad op uw gebruikerspagina om uw uitnodigingen bij te houden." success_username: "We hebben die gebruiker uitgenodigd om aan dit topic deel te nemen." error: "Sorry, we konden deze persoon niet uitnodigen. Misschien is deze al uitgenodigd? (Uitnodigingen zijn in aantal beperkt)" + success_existing_email: "Een gebruiker met de e-mail {{emailOrUsername}} bestaat al. We hebben deze gebruiker uitgenodigd om aan dit topic deel te nemen." login_reply: 'Meld u aan om te antwoorden' filters: n_posts: @@ -1651,6 +1685,7 @@ nl: has_liked: "u hebt dit bericht geliket" undo_like: "like ongedaan maken" edit: "dit bericht bewerken" + edit_action: "Bewerken" edit_anonymous: "Sorry, maar u dient aangemeld te zijn om dit bericht te bewerken." flag: "een privémarkering aan dit topic geven of er een privébericht over sturen" delete: "dit bericht verwijderen" @@ -1800,6 +1835,7 @@ nl: settings: 'Instellingen' topic_template: "Topic-sjabloon" tags: "Tags" + tags_allowed_tags: "Tags die in deze categorie kunnen worden gebruikt:" tags_placeholder: "(Optioneel) lijst van toegestane tags" tag_groups_placeholder: "(Optioneel) lijst van toegestane tag-groepen" topic_featured_link_allowed: "Aanbevolen koppelingen in deze categorie toestaan" @@ -2767,6 +2803,8 @@ nl: block: "blokkeren" do_nothing: "niets doen" staff_actions: + all: "alle" + filter: "Filter:" title: "Stafacties" clear_filters: "Alles tonen" staff_user: "Staflid" @@ -2992,6 +3030,7 @@ nl: reset_bounce_score: label: "Terugzetten" title: "Bouncescore terugzetten naar 0" + visit_profile: "Ga naar de instellingen van deze gebruiker om hun profiel te bewerken" deactivate_explanation: "Een gedeactiveerde gebruiker moet zijn of haar e-mailadres opnieuw valideren." suspended_explanation: "Een geschorste gebruiker kan zich niet aanmelden." block_explanation: "Een geblokkeerde gebruiker kan geen berichten plaatsen of topics maken." diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 885d939bd5..3f984d6bcd 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -173,10 +173,10 @@ pl_PL: enabled: 'wylistowanie %{when}' disabled: 'odlistowanie %{when}' banner: - enabled: 'Ten temat został ustawiony jako baner. Będzie widoczny na górze każdej strony, dopóki nie zostanie zamknięty przez użytkownika.' - disabled: 'Baner został usunięty. Nie będzie więcej widoczny na górze każdej strony.' + enabled: 'Ten temat został ustawiony jako baner %{when}. Będzie widoczny na górze każdej strony, póki nie zostanie ukryty przez użytkownika.' + disabled: 'Ten temat nie jest już banerem. Nie będzie dalej wyświetlany na górze każdej strony.' topic_admin_menu: "akcje administratora" - wizard_required: "Witaj na Twoim nowym Discourse! Zacznij od kreatora konfiguracji ✨" + wizard_required: "Witaj na Twoim na nowym forum opartym na Discourse! Rozpocznij konfigurację w panelu konfiguracyjnym ✨" emails_are_disabled: "Wysyłanie e-maili zostało globalnie wyłączone przez administrację. Powiadomienia e-mail nie będą dostarczane." bootstrap_mode_enabled: "Aby ułatwić uruchomienie Twojego nowego serwisu, znajdujesz się w trybie bootstrap. Wszyscy nowi użytkownicy otrzymają 1. poziom zaufania i będą otrzymywać codzienne podsumowania drogą mailową. To się zmieni, gdy liczba użytkowników przekroczy liczbę %{min_users}." bootstrap_mode_disabled: "Tryb Bootstrap zostanie wyłączony w ciągu najbliższych 24 godzin." @@ -184,20 +184,21 @@ pl_PL: default_description: "Domyślny" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Irlandia)" - eu_west_2: "EU (Londyn)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "Ameryka Południowa (Sao Paulo)" + ap_south_1: "Asia Pacific (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "Chiny (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irlandia)" + eu_west_2: "Europa (Londyn)" + sa_east_1: "Ameryka Południowa (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_east_2: "Wschodnie USA (Ohio)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'edytuj tytuł i kategorię tego tematu' not_implemented: "Bardzo nam przykro, ale ta funkcja nie została jeszcze zaimplementowana." no_value: "Nie" @@ -404,6 +405,8 @@ pl_PL: add_members: "Dodaj użytkowników" delete_member_confirm: "Czy usunąć użytkownika '%{username}' z grupy '%{group}'?" name_placeholder: "Nazwa grupy, bez spacji, takie same zasady jak przy nazwie użytkownika" + public_admission: "Zezwól wszystkim użytkownikom na dołączanie do tej grupy (widoczność grupy musi być ustawiona na publiczną)" + public_exit: "Zezwól wszystkim użytkownikom na opuszczanie tej grupy" empty: posts: "Członkowie tej grupy nie opublikowali żadnych postów." members: "Nie ma użytkowników w tej grupie" @@ -415,6 +418,7 @@ pl_PL: join: "Dołącz do grupy" leave: "Opuść grupę" request: "Żądanie dołączenia do grupy" + message: "Wiadomość" automatic_group: Automatyczna grupa closed_group: Zamknięta grupa is_group_user: "Jesteś członkiem tej grupy" @@ -440,6 +444,12 @@ pl_PL: mentions: "Wzmianki" messages: "Wiadomości" notification_level: "Domyślny poziom powiadomień dla wiadomości grupy." + visibility_levels: + title: "Kto może widzieć grupę?" + public: "Wszyscy" + members: "Właściciele grupy, członkowie i administratorzy" + staff: "Właściciele grupy i zespół" + owners: "Właściciele grupy i administratorzy" alias_levels: title: "Kto może wysyłać wiadomości i używać @aliasu tej grupy?" nobody: "Nikt" @@ -541,6 +551,7 @@ pl_PL: mute: "Wycisz" edit: "Edytuj ustawienia" download_archive: + button_text: "Pobierz Wszystko" confirm: "Czy na pewno chcesz pobrać swoje wszystkie wpisy?" success: "Rozpoczęto eksport: otrzymasz wiadomość, gdy proces zostanie zakończony." rate_limit_error: "Wpisy mogą być pobierane raz dziennie, spróbuj ponownie jutro." @@ -570,6 +581,7 @@ pl_PL: first_notification: "Twoje pierwsze powiadomienie! Kliknij aby zacząć." disable_jump_reply: "Po odpowiedzi nie przechodź do nowego wpisu" dynamic_favicon: "Pokazuj licznik powiadomień na karcie jako dynamiczny favicon" + theme_default_on_all_devices: "Ustaw ten motyw jako domyślny na wszystkich moich urządzeniach" external_links_in_new_tab: "Otwieraj wszystkie zewnętrzne odnośniki w nowej karcie" enable_quoting: "Włącz cytowanie zaznaczonego tekstu" change: "zmień" @@ -608,6 +620,7 @@ pl_PL: watched_first_post_tags_instructions: "Zostaniesz powiadomiony tylko o pierwszym wpisie w każdym nowym temacie oznaczonym tymi tagami." muted_categories: "Wyciszone" muted_categories_instructions: "Nie będziesz powiadamiany o nowych tematach w tych kategoriach. Nie pojawią się na liście nieprzeczytanych." + no_category_access: "Jako moderator masz limitowany dostęp do kategorii, możliwość zapisu jest wyłączona." delete_account: "Usuń moje konto" delete_account_confirm: "Czy na pewno chcesz usunąć swoje konto? To nieodwracalne!" deleted_yourself: "Twoje konto zostało usunięte." @@ -806,8 +819,12 @@ pl_PL: expired: "To zaproszenie wygasło." rescind: "Usuń" rescinded: "Zaproszenie usunięte" + rescind_all: "Usuń wszystkie Zaproszenia" + rescinded_all: "Wszystkie Zaproszenia usunięte!" + rescind_all_confirm: "Jesteś pewny, że chcesz usunąć wszystkie zaproszenia?" reinvite: "Ponów zaproszenie" reinvite_all: "Wyślij ponownie wszystkie zaproszenia" + reinvite_all_confirm: "Jesteś pewny, że chcesz ponownie wysłać wszystkie zaproszenia?" reinvited: "Ponowne wysłanie zaproszenia" reinvited_all: "Wszystkie zaproszenia zostały ponownie wysłane!" time_read: "Czas odczytu" @@ -1004,6 +1021,9 @@ pl_PL: complete_email_found: "Znaleziono konto przypisane do adresu %{email}, wkrótce otrzymasz email z instrukcjami opisującymi reset hasła." complete_username_not_found: "Nie znaleziono konta o nazwie %{username}" complete_email_not_found: "Nie znaleziono konta przypisanego do %{email}" + help: "Email nie dotarł? Upewnij się najpierw czy sprawdziłeś folder spam.

    Nie jesteś pewny jaki email został użyty? Wprowadz adres tutaj, a dowiemy się czy taki istnieje.

    Jeśli nie masz już dostępu do adresu email zapisanego na twoim koncie, skontaktuj się z naszym pomocnym zespołem.

    " + button_ok: "OK" + button_help: "Pomoc" login: title: "Logowanie" username: "Użytkownik" @@ -1058,10 +1078,13 @@ pl_PL: accept_title: "Zaproszenie" welcome_to: "Witaj na %{site_name}!" invited_by: "Zostałeś zaproszony przez:" + social_login_available: "Będziesz mógł zalogować się za pomocą dowolnej platformy społecznościowej używając tego adresu e-mail." your_email: "Adres e-mail przypisany do twojego konta to %{email}." accept_invite: "Akceptuj zaproszenie" success: "Twje konto zostało utworzone i zostałeś zalogowany." name_label: "Nazwa" + password_label: "Ustaw hasło" + optional_description: "(opcjonalne)" password_reset: continue: "Przejdź do %{site_name}" emoji_set: @@ -1070,6 +1093,8 @@ pl_PL: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google Classic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Tylko kategorie" categories_with_featured_topics: "Kategorie z wybranymi tematami" @@ -1078,12 +1103,30 @@ pl_PL: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + filter_placeholder: Szukaj emoji + people: Ludzie + nature: Natura + food: Jedzenie + activity: Aktywność + travel: Podróż + objects: Obiekty + celebration: Celebracja + custom: Spersonalizowane emoji + recent: Ostatnio używane + default_tone: Brak koloru skóry + light_tone: Jasny kolor skóry + medium_light_tone: Średnio jasny kolor skóry + medium_tone: Średni kolor skóry + medium_dark_tone: Średnio ciemny kolor skóry + dark_tone: Ciemny kolor skóry composer: emoji: "Emoji :)" more_emoji: "więcej…" options: "Opcje" whisper: "szept" unlist: "nie widoczny" + blockquote_text: "Cytat" add_warning: "To jest oficjalne ostrzeżenie." toggle_whisper: "Przełącz szept" toggle_unlisted: "Przycisk ukrywania" @@ -1100,6 +1143,7 @@ pl_PL: cannot_see_mention: category: "Wspomniałeś o {{username}} lecz nie zostaną oni powiadomieni ponieważ nie mają dostępu do tej kategorii. Dodaj ich do grupy która ma dostęp do tej kategorii." private: "Wspomniałeś o {{username}} lecz nie zostaną powiadomieni ponieważ nie mogą odczytać tej wiadomości . Będziesz musiał ich zaprosić do odczytania tej wiadomości " + duplicate_link: "Wygląda na to, że Twój link do {{domain}} został już wcześniej przesłany w tym wątku przez @{{username}} w odpowiedzi przesłanej {{ago}} - jesteś pewien, że chcesz go wysłać ponownie?" error: title_missing: "tytuł jest wymagany" title_too_short: "tytuł musi zawierać co najmniej {{min}} znaków" @@ -1191,6 +1235,7 @@ pl_PL: moved_post: "

    {{username}} przenosi {{description}}

    " linked: "

    {{username}} {{description}}

    " granted_badge: "

    Otrzymujesz '{{description}}'

    " + topic_reminder: "

    {{username}} {{description}}

    " watching_first_post: "

    Nowy temat {{description}}

    " group_message_summary: one: "

    masz {{count}} wiadomość w skrzynce odbiorczej {{group_name}}

    " @@ -1245,10 +1290,10 @@ pl_PL: clear_all: "Wyczyść wszystkie" too_short: "Wyszukiwane określenie jest zbyt krótkie." result_count: - one: "1 wynik dla \"{{term}}\"" - few: "{{count}} wyniki dla \"{{term}}\"" - many: "{{count}} wyników dla \"{{term}}\"" - other: "{{count}} wyników dla \"{{term}}\"" + one: "1 wynik dla {[count}}" + few: "1 wynik dla {{term}}" + many: "1 wynik dla {{term}}" + other: "{{count}} wyników dla {{term}}" title: "szukaj tematów, wpisów, użytkowników lub kategorii" no_results: "Brak wyników wyszukiwania" no_more_results: "Nie znaleziono więcej wyników." @@ -1286,6 +1331,7 @@ pl_PL: seen: Przeczytałem unseen: Nie przeczytałem wiki: są postami wiki + all_tags: Zawiera wszystkie tagi statuses: label: Tematy gdzie open: są otwarte @@ -1311,6 +1357,7 @@ pl_PL: select_all: "Zaznacz wszystkie" clear_all: "Wyczyść wszystko" unlist_topics: "Ukryj tematy" + relist_topics: "Odśwież listę tematów" reset_read: "Wyzeruj przeczytane" delete: "Usuń tematy" dismiss: "Wyczyść" @@ -1321,6 +1368,7 @@ pl_PL: dismiss_new: "Wyczyść nowe" toggle: "włącz grupowe zaznaczanie tematów" actions: "Operacje grupowe" + change_category: "Ustaw kategorię" close_topics: "Zamknij wpisy" archive_topics: "Zarchiwizuj tematy" notification_level: "Powiadomienia" @@ -1449,6 +1497,7 @@ pl_PL: next_week: "Następny tydzień" next_month: "Następny miesiąc" pick_date_and_time: "Wybierz datę i czas" + set_based_on_last_post: "Zamknij na podstawie ostatniego postu" publish_to_category: title: "Zaplanuj publikację" temp_open: @@ -1461,11 +1510,18 @@ pl_PL: title: "Automatycznie zamknij wątek" label: "Godziny automatycznego zamknięcia:" error: "Wprowadź poprawną wartość." + based_on_last_post: "Nie zamykaj tematu dopóki od ostatniego wpisu nie upłynie przynajmniej tyle czasu." + auto_delete: + title: "Automatycznie usuń wątek" + reminder: + title: "Przypomnij mi" status_update_notice: auto_open: "Ten wątek zostanie automatycznie otwarty po %{timeLeft}." auto_close: "Ten wątek zostanie automatycznie zamknięty po %{timeLeft}." auto_publish_to_category: "Wątek zostanie opublikowany w #%{categoryName} %{timeLeft}." auto_close_based_on_last_post: "Ten watek zostanie automatycznie zamknięty po %{count} minutach od ostatniego wpisu." + auto_delete: "Ten wątek zostanie automatycznie zamknięty po %{timeLeft}." + auto_reminder: "Otrzymasz przypomnienie o tym temacie %{timeLeft}." auto_close_title: 'Ustawienia automatycznego zamykania' auto_close_immediate: one: "Ostatni wpis w tym temacie został zamieszczony 1 godzinę temu, więc zostanie on natychmiastowo zamknięty." @@ -1625,6 +1681,7 @@ pl_PL: success_email: "Wysłaliśmy zaproszenie do {{emailOrUsername}}. Otrzymasz powiadomienie, gdy zaproszenie zostanie przyjęte. Sprawdź zakładkę zaproszenia w swoim profilu, aby śledzić status tego i innych zaproszeń." success_username: "Wskazany użytkownik został zaproszony do udziału w tym temacie." error: "Przepraszamy, nie udało się zaprosić wskazanej osoby. Być może została już zaproszona? (Lub wysyłasz zbyt wiele zaproszeń)" + success_existing_email: "Użytkownik o e-mailu {{emailOrUsername}} już istnieje. Zaprosiliśmu tego użytkownika do udziału w tym temacie." login_reply: 'Zaloguj się, aby odpowiedzieć' filters: n_posts: @@ -1764,6 +1821,7 @@ pl_PL: has_liked: "lajkujesz ten wpis" undo_like: "wycofaj lajka" edit: "edytuj ten wpis" + edit_action: "Edytuj" edit_anonymous: "Przykro nam, ale musisz być zalogowany aby edytować ten wpis." flag: "oflaguj ten wpis lub wyślij powiadomienie o nim do moderatorów" delete: "usuń ten wpis" @@ -1906,6 +1964,12 @@ pl_PL: few: "Jesteś pewny(-a), że chcesz usunąć te wszystkie wpisy?" many: "Czy na pewno chcesz usunąć te wszystkie wpisy?" other: "Czy na pewno chcesz usunąć te wszystkie wpisy?" + merge: + confirm: + one: "Czy na pewno chcesz połączyć te posty?" + few: "Czy na pewno chcesz połączyć te {{count}} postów?" + many: "Czy na pewno chcesz połączyć te {{count}} postów?" + other: "Czy na pewno chcesz połączyć te {{count}} postów?" revisions: controls: first: "Pierwsza wersja" @@ -1951,6 +2015,8 @@ pl_PL: settings: 'Ustawienia' topic_template: "Szablon tematu" tags: "Tagi" + tags_allowed_tags: "Zezwól jedynie na użycie następujących tagów w tej kategorii:" + tags_allowed_tag_groups: "Zezwól jedynie na użycie tagów z następujących grup w tej kategorii:" tags_placeholder: "(Opcjonalnie) lista dozwolonych tagów" tag_groups_placeholder: "(Opcjonalnie) lista dozwolonych grup tagów" topic_featured_link_allowed: "Zezwól na wybrane linki w tej kategorii" @@ -1993,6 +2059,7 @@ pl_PL: subcategory_list_style: "Styl listy podkategorii:" sort_order: "Sortuj listę wątków po:" default_view: "Domyślna lista wątków:" + default_top_period: "Domyślny okres wyświetlania najlepszych postów:" allow_badges_label: "Włącz przyznawanie odznak na podstawie aktywności w tej kategorii" edit_permissions: "Edytuj uprawnienia" add_permission: "Dodaj uprawnienie" @@ -2306,6 +2373,8 @@ pl_PL: granted_on: "Przyznano %{date}" others_count: "Inni użytkownicy z tą odznaką (%{count})" title: Odznaki + allow_title: "Możesz użyc tej odznaki jako tytułu" + multiple_grant: "Możesz zdobyć tą odznakę wiele razy" badge_count: one: "1 odznaka" few: "%{count} odznak" @@ -2617,6 +2686,7 @@ pl_PL: event_type_missing: "Musisz ustawić co najmniej jeden typ wydarzenia." content_type: "Typ zawartości " secret: "Tajemnica" + event_chooser: "Jakie zdarzenia powinny wywołać uruchomienie tego webhooka?" wildcard_event: "Wysyłaj mi wszystko." individual_event: "Wybierz indywidualne zdarzenia" verify_certificate: "Sprawdź certyfikat TLS zawarty w url" @@ -2765,6 +2835,7 @@ pl_PL: copy_to_clipboard: "Skopiuj do schowka" copied_to_clipboard: "Tekst skopiowany do schowka." copy_to_clipboard_error: "Wystąpił błąd w trakcie kopiowania do schowka." + theme_owner: "Nie możliwe do edycji, należy do:" email_templates: title: "Szablony email" subject: "Temat" @@ -2777,8 +2848,10 @@ pl_PL: import_theme: "Importuj motyw" customize_desc: "Personalizacja:" title: "Motywy" + long_title: "Zmodyfikuj kolory, kod CSS i kod HTML zawartości Twojej strony" edit: "Edytuj" edit_confirm: "To jest zdalny styl. Jeśli edytujesz CSS/HTML, twoje zmiany zostaną usunięte po ponownej aktualizacji motywu." + common: "Częste" desktop: "Komputer" mobile: "Mobilnie" preview: "Podgląd" @@ -2786,19 +2859,34 @@ pl_PL: user_selectable: "Motyw może być ustawiany przez użytkowników" color_scheme: "Schemat kolorów" color_scheme_select: "Wybierz kolory, jakie mają być użyte w motywie" + custom_sections: "Spersonalizowane sekcje:" theme_components: "Komponenty motywu" + uploads: "Pliki" no_uploads: "Możesz załadować zasoby do swojego motywu np. fonty lub obrazy" + add_upload: "Dodaj plik" + upload_file_tip: "Wybierz plik do wysłania (png, woff2, itp.)" + variable_name: "Nazwa zmiennej SCSS:" + upload: "Plik" + child_themes_check: "Motyw importuje zawartość innych motywów" css_html: "Własny CSS/HTML" edit_css_html: "Edytuj CSS/HTML" edit_css_html_help: "Nie modyfikowałeś CSS ani HTML" delete_upload_confirm: "Czy usunąć ten plik? (CSS motywu może przestać działać!)" + import_web_tip: "Repozytorium zawierające motyw" + import_file_tip: "Plik .dcstyle.json zawierający motyw" about_theme: "O motywie" license: "Licencja" + component_of: "Motyw jest częścią:" update_to_latest: "Aktualizuj do najnowszego" check_for_updates: "Sprawdź dostępność aktualizacji" updating: "Trwa proces aktualizacji..." up_to_date: "Motyw jest aktualny. Ostatnio sprawdzano:" add: "Dodaj" + commits_behind: + one: "Motyw jest 1 aktualizację w tyle!" + few: "Motyw jest {{count}} aktualizacji w tyle!" + many: "Motyw jest {{count}} aktualizacji w tyle!" + other: "Motyw jest {{count}} aktualizacji w tyle!" scss: text: "CSS" title: "Wstaw własny CSS, przyjmujemy wszystkie prawidłowe style CSS i SCSS" @@ -2813,6 +2901,7 @@ pl_PL: title: "Wstaw kod HTML do wyświetlenia w stopce strony" embedded_scss: text: "Osadzony CSS" + title: "Podaj spersonalizowany kod CSS dostarczany w ramkowej wersji komentarzy" head_tag: text: "" title: "HTML, który zostanie umieszczony przed tagiem " @@ -2944,6 +3033,8 @@ pl_PL: block: "blok" do_nothing: "nic nie rób" staff_actions: + all: "wszystkie" + filter: "Filtruj:" title: "Działania obsługi" clear_filters: "Pokaż wszystko" staff_user: "Użytkownik obsługi" @@ -2997,6 +3088,8 @@ pl_PL: change_readonly_mode: "Zmień tryb tylko do otczytu" backup_download: "Pobierz kopię zapasową" backup_destroy: "Zniszcz kopię zapasową " + reviewed_post: "przejrzane posty" + custom_staff: "spersonalizowana akcja wtyczki" screened_emails: title: "Ekranowane emaile" description: "Kiedy ktoś próbuje założyć nowe konto, jego adres email zostaje sprawdzony i rejestracja zostaje zablokowana, lub inna akcja jest podejmowana." @@ -3029,6 +3122,32 @@ pl_PL: title: "Tworzy nowy ban dla podsieci jeśli jest co najmniej 'min_ban_entries_for_roll_up' pozycji." logster: title: "Logi błędów" + watched_words: + title: "Obserwowane słowa" + search: "szukaj" + clear_filter: "Wyczyść" + show_words: "pokaż słowa" + word_count: + one: "1 słowo" + few: "%{count} słów" + many: "%{count} słów" + other: "%{count} słów" + actions: + block: 'Zablokuj' + censor: 'Cenzuruj' + require_approval: 'Wymaga zatwierdzenia' + flag: 'Oflaguj' + action_descriptions: + block: 'Zapobiegaj publikacji postów zawierających te słowa. Użytkownicy zobaczą błąd próbując wysłać taki post.' + censor: 'Zezwalaj na wysyłanie postów zawierających te słowa, ale zamień na znaki ukrywające ocenzurowane słowa.' + require_approval: 'Posty zawierające te słowa będą wymagały zatwierdzenia przez moderatora zanim zostaną opublikowane.' + flag: 'Zezwalaj na publikację takich postów, ale oflaguj je jako nieodpowiednie, aby moderatorzy mogli przejrzeć ich zawartość.' + form: + label: 'Nowe słowo:' + add: 'Dodaj' + success: 'Sukces' + upload: "Prześlij" + upload_successful: "Przesyłanie zakończone sukcesem. Słowa zostały dodane." impersonate: title: "Zaloguj się na to konto" help: "Użyj tego narzędzia, aby logować się jako dowolny użytkownik w celach diagnozy problemów." @@ -3183,6 +3302,7 @@ pl_PL: reset_bounce_score: label: "Przywróć" title: "Przywróć stan licznika zwrotów z powrotem do wartości 0" + visit_profile: "Odwiedź ustawienia tego użytkownika, aby zedytować jego profil." deactivate_explanation: "Wymusza ponowne potwierdzenie adresu email tego konta." suspended_explanation: "Zawieszony użytkownik nie może się logować." block_explanation: "Zablokowany użytkownik nie może tworzyć wpisów ani zaczynać tematów." @@ -3397,6 +3517,7 @@ pl_PL: sample: "Użyj poniższego kodu HTML na swojej stronie, aby osadzić tematy z Discourse. Zastąp REPLACE_ME domyślnym adresem URL strony na której osadzasz." title: "Osadzanie" host: "Dozwolone hosty" + class_name: "Nazwa klasy" path_whitelist: "Ścieżka białej listy" edit: "edytuj" category: "Publikuj w kategorii" @@ -3416,6 +3537,7 @@ pl_PL: embed_classname_whitelist: "Dozwolone nazwy klas CSS" feed_polling_enabled: "Importowanie wpisów via RSS/ATOM" feed_polling_url: "URL kanału RSS/ATOM" + feed_polling_frequency_mins: "Częstotliwość aktualizacji strony (w minutach)" save: "Zapisz" permalink: title: "Permalinki" diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 0a25b9cffa..f627ad361f 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -145,20 +145,20 @@ pt: default_description: "Predefinido" s3: regions: - us_east_1: "Este dos E.U.A. (Virgínia do Norte)" - us_west_1: "Oeste dos E.U.A. (California do Norte)" - us_west_2: "Oeste dos E.U.A. (Óregon)" - us_gov_west_1: "AWS GovCloud (E.U.A.)" - eu_west_1: "U.E. (Irlanda)" - eu_west_2: "EU (Londres)" - eu_central_1: "U.E. (Francoforte)" - ap_southeast_1: "Ásia-Pacífico (Singapura)" - ap_southeast_2: "Ásia-Pacífico (Sydney)" - ap_south_1: "Ásia Pacifico (Bombaim)" ap_northeast_1: "Ásia-Pacífico (Tóquio)" ap_northeast_2: "Ásia-Pacífico (Seoul)" - sa_east_1: "América do Sul (São Paulo)" + ap_south_1: "Ásia Pacifico (Bombaim)" + ap_southeast_1: "Ásia-Pacífico (Singapura)" + ap_southeast_2: "Ásia-Pacífico (Sydney)" cn_north_1: "China (Beijing)" + eu_central_1: "U.E. (Francoforte)" + eu_west_1: "U.E. (Irlanda)" + eu_west_2: "UE (Londres)" + sa_east_1: "América do Sul (São Paulo)" + us_east_1: "Este dos E.U.A. (Virgínia do Norte)" + us_gov_west_1: "AWS GovCloud (E.U.A.)" + us_west_1: "Oeste dos E.U.A. (California do Norte)" + us_west_2: "Oeste dos E.U.A. (Óregon)" edit: 'editar o título e a categoria deste tópico' not_implemented: "Essa funcionalidade ainda não foi implementada, pedimos desculpa!" no_value: "Não" @@ -352,22 +352,22 @@ pt: empty: posts: "Não existem publicações por membros deste grupo." members: "Não existem membros neste grupo." - mentions: "Não há menções deste grupo." + mentions: "Não existem menções deste grupo." messages: "Não existem mensagens para este grupo." topics: "Não existem tópicos por membros deste grupo." logs: "Não existem registos para este grupo." add: "Adicionar" - join: "Juntar ao Grupo" + join: "Aderir ao Grupo" leave: "Sair do Grupo" - request: "Pedir para Entrar no Grupo." + request: "Pedir para Aderir ao Grupo" automatic_group: Grupo Automático closed_group: Grupo fechado - is_group_user: "Você é membro deste grupo" - allow_membership_requests: "Permitir aos utilizadores enviar pedidos de adesão aos donos do grupo" + is_group_user: "É um membro deste grupo" + allow_membership_requests: "Permitir que os utilizadores enviem pedidos de adesão aos donos do grupo" membership: "Adesão" name: "Nome" - user_count: "Numero de Membros" - bio: "Acerca do Grupo" + user_count: "Número de Membros" + bio: "Sobre o Grupo" selector_placeholder: "Adicionar membros" owner: "proprietário" index: @@ -376,13 +376,19 @@ pt: title: one: "grupo" other: "grupos" - activity: "Actividade" + activity: "Atividade" members: "Membros" topics: "Tópicos" posts: "Publicações" mentions: "Menções" messages: "Mensagens" - notification_level: "Nível de notificação por omissão para mensagens de grupo" + notification_level: "Nível de notificação predefinido para mensagens de grupo" + visibility_levels: + title: "Quem pode ver este grupo?" + public: "Toda a gente" + members: "Donos do grupo, membros e administradores" + staff: "Donos do grupo e equipa" + owners: "Donos do grupo e administradores" alias_levels: title: "Quem pode mandar mensagens e @mencionar este grupo?" nobody: "Ninguém" @@ -411,9 +417,11 @@ pt: description: "Não será notificado de nada relacionado com novos tópicos neste grupo." flair_url: "Imagem da Marca de Avatar" flair_bg_color: "Cor de Fundo da Marca de Avatar" - flair_bg_color_placeholder: "(Opcional) Valor hexadecimal de cor" + flair_bg_color_placeholder: "(Opcional) Valor hexadecimal da cor" flair_color: "Cor da Marca de Avatar" - flair_color_placeholder: "(Opcional) Valor hexadecimal de cor" + flair_color_placeholder: "(Opcional) Valor hexadecimal da cor" + flair_preview_icon: "Pré-visualizar Ícone" + flair_preview_image: "Pré-visualizar Imagem" user_action_groups: '1': "Gostos Dados" '2': "Gostos Recebidos" @@ -476,6 +484,7 @@ pt: mute: "Silenciar" edit: "Editar Preferências" download_archive: + button_text: "Transferir Tudo" confirm: "Tem a certeza que deseja descarregar as suas publicações?" success: "Descarregamento iniciado, será notificado via mensagem assim que o processo estiver concluído." rate_limit_error: "As publicações podem ser descarregadas uma vez por dia, por favor, tente novamente amanhã." @@ -1303,7 +1312,7 @@ pt: go_bottom: "fim" go: "ir" jump_bottom: "ir para a última publicação" - jump_prompt: "saltar para..." + jump_prompt: "ir para..." jump_prompt_of: "de %{count} publicações" jump_prompt_long: "Para que publicação gostaria de ir?" jump_bottom_with_number: "ir para a publicação %{post_number}" @@ -1662,7 +1671,7 @@ pt: show: "Mostrar revisão" revert: "Reverter para esta revisão" edit_wiki: "Editar Wiki" - edit_post: "Editar publicação" + edit_post: "Editar Publicação" comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 787cd28d3c..187dcfe91a 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -148,20 +148,20 @@ pt_BR: default_description: "Padrão" s3: regions: - us_east_1: "Leste dos EUA (N. da Virgínia)" - us_west_1: "Oeste dos EUA (N. da Califórnia)" - us_west_2: "Leste dos EUA (Oregon)" - us_gov_west_1: "AWS GovCloud (EUA)" - eu_west_1: "EU (Irlanda)" - eu_west_2: "UE (Londres)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Ásia Pacífico (Singapura)" - ap_southeast_2: "Ásia Pacífico (Sidney)" - ap_south_1: "Ásia-Pacífico (Mumbai)" ap_northeast_1: "Ásia Pacífico (Tóquio)" ap_northeast_2: "Ásia Pacífico (Seul)" - sa_east_1: "América do Sul (São Paulo)" + ap_south_1: "Ásia-Pacífico (Mumbai)" + ap_southeast_1: "Ásia Pacífico (Singapura)" + ap_southeast_2: "Ásia Pacífico (Sidney)" cn_north_1: "China (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irlanda)" + eu_west_2: "UE (Londres)" + sa_east_1: "América do Sul (São Paulo)" + us_east_1: "Leste dos EUA (N. da Virgínia)" + us_gov_west_1: "AWS GovCloud (EUA)" + us_west_1: "Oeste dos EUA (N. da Califórnia)" + us_west_2: "Leste dos EUA (Oregon)" edit: 'edite o título e a categoria deste tópico' not_implemented: "Esse recurso ainda não foi implementado, desculpe!" no_value: "Não" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 67e28fbb75..824e1add24 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -161,19 +161,19 @@ ro: default_description: "Implicit" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Irlanda)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "Asia Pacific (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irlanda)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'Editează titlul și categoria acestui subiect' not_implemented: "Această funcționalitate nu a fost implementată încă." no_value: "Nu" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index cc96fa19b4..d60bb7e4de 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -181,20 +181,20 @@ ru: default_description: "По умолчанию" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Ireland)" - eu_west_2: "EU (London)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "Asia Pacific (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ireland)" + eu_west_2: "EU (London)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'отредактировать название и раздел темы' not_implemented: "Извините, эта функция еще не реализована!" no_value: "Нет" @@ -434,6 +434,11 @@ ru: posts: "Сообщения" mentions: "Упоминания" messages: "Сообщения" + visibility_levels: + title: "Кто может видеть эту группу?" + public: "Все" + members: "Владельцы группы, участники и администраторы" + owners: "Владельцы группы и администраторы" alias_levels: title: "Кто может отправлять сообщения и @упоминать эту группу?" nobody: "Никто" @@ -803,6 +808,9 @@ ru: expired: "Это приглашение истекло." rescind: "Отозвать" rescinded: "Приглашение отозвано" + rescind_all: "Удалить все приглашения" + rescinded_all: "Все приглашения были удалены!" + rescind_all_confirm: "Вы уверены, что хотите удалить все приглашения?" reinvite: "Повторить приглашение" reinvite_all: "Повторить все приглашения" reinvite_all_confirm: "Вы действительно хотите отправить все приглашения повторно?" @@ -1074,6 +1082,8 @@ ru: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google Classic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Только разделы" categories_with_featured_topics: "Разделы и их лучшие темы" @@ -1082,12 +1092,21 @@ ru: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + emoji_picker: + people: People + nature: Nature + food: Food + activity: Activity + travel: Travel + objects: Objects + celebration: Celebration composer: emoji: "Смайлики :)" more_emoji: "еще..." options: "Дополнительные опции" whisper: "внутреннее сообщение" unlist: "исключена из списков тем" + blockquote_text: "Цитата" add_warning: "Это официальное предупреждение." toggle_whisper: "Внутреннее сообщение" toggle_unlisted: "Спрятать из списков тем" @@ -1332,6 +1351,7 @@ ru: change_category: "Задать раздел" close_topics: "Закрыть темы" archive_topics: "Архивировать темы" + notification_level: "Уведомления" choose_new_category: "Выберите новый раздел для этих тем:" selected: one: "Вы выбрали {{count}} тему." diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 7404b3376e..e1bd460e66 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -156,18 +156,18 @@ sk: bootstrap_mode_disabled: "Zavádzací režim bude zrušený v priebehu nasledujúcich 24 hodín." s3: regions: - us_east_1: "USA Východ (S. Virginia)" - us_west_1: "USA Západ (S. Kalifornia)" - us_west_2: "USA Západ (Oregon)" - us_gov_west_1: "AWS GovCloud (USA)" - eu_west_1: "EU (Írsko)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Ázia Tichomorie (Singapur)" - ap_southeast_2: "Ázia Tichomorie (Sydney)" ap_northeast_1: "Ázia Tichomorie (Tokio)" ap_northeast_2: "Asia Pacific (Soul)" - sa_east_1: "Južná Amerika (Sao Paulo)" + ap_southeast_1: "Ázia Tichomorie (Singapur)" + ap_southeast_2: "Ázia Tichomorie (Sydney)" cn_north_1: "Čína (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Írsko)" + sa_east_1: "Južná Amerika (Sao Paulo)" + us_east_1: "USA Východ (S. Virginia)" + us_gov_west_1: "AWS GovCloud (USA)" + us_west_1: "USA Západ (S. Kalifornia)" + us_west_2: "USA Západ (Oregon)" edit: 'upraviť názov a kategóriu témy' not_implemented: "Ľutujeme, táto funkcia ešte nie je implementovaná." no_value: "Nie" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index f249f3086f..62e66bb492 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -144,20 +144,20 @@ sq: default_description: "Paracaktuar" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Ireland)" - eu_west_2: "BE (Londër)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "Asia Pacific (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ireland)" + eu_west_2: "BE (Londër)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'redakto titullin dhe kategorinë e kësaj teme' not_implemented: "Kjo veçori nuk është implementuar akoma, na vjen keq!" no_value: "Jo" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 81c6da8c00..3384e8d34a 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -142,21 +142,25 @@ sv: emails_are_disabled: "All utgående e-post har blivit globalt inaktiverad av en administratör. Inga e-postnotifikationer av något slag kommer att skickas ut." bootstrap_mode_enabled: "Du är i bootstrap-läge för att göra lanseringen av din nya webbplats enklare. Alla nya användare kommer att beviljas förtroendenivå 1 och få dagliga sammanfattningar skickade via e-post. Det här stängs automatiskt av när det totala antalet användare överstiger %{min_users}." bootstrap_mode_disabled: "Bootstrap-läge otillgängliggörs i 24 timmar." + themes: + default_description: "Standard" s3: regions: - us_east_1: "Östra USA (N. Virginia)" - us_west_1: "Västra USA (N. Kalifornien)" - us_west_2: "Västra USA (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Irland)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asien Stillahavsområdet (Singapore)" - ap_southeast_2: "Asien Stillahavsområdet (Sydney)" - ap_south_1: "Asien Stillahavsområdet (Mumbai)" ap_northeast_1: "Asien Stillahavsområdet (Tokyo)" ap_northeast_2: "Asien Stillahavsområdet (Seoul)" - sa_east_1: "Sydamerika (Sao Paulo)" + ap_south_1: "Asien Stillahavsområdet (Mumbai)" + ap_southeast_1: "Asien Stillahavsområdet (Singapore)" + ap_southeast_2: "Asien Stillahavsområdet (Sydney)" cn_north_1: "Kina (Peking)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Irland)" + eu_west_2: "EU (London)" + sa_east_1: "Sydamerika (Sao Paulo)" + us_east_1: "Östra USA (N. Virginia)" + us_east_2: "Öster USA (Ohio)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "Västra USA (N. Kalifornien)" + us_west_2: "Västra USA (Oregon)" edit: 'redigera rubrik och kategori för det här ämnet' not_implemented: "Denna funktion har inte implementerats än, vi beklagar!" no_value: "Nej" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index c07bcc2704..7702482e41 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -123,19 +123,19 @@ tr_TR: bootstrap_mode_disabled: "Bootstrap modu önümüzdeki 24 saat içinde edilgen olacaktır." s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Ireland)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Asia Pacific (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "Asia Pacific (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Beijing)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ireland)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'bu konunun başlığını ve kategorisini düzenleyin' not_implemented: "Bu özellik henüz geliştirilmedi, üzgünüz!" no_value: "Hayır" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index f772a81237..8064e201e9 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -55,6 +55,7 @@ uk: other: "%{count} днів тому" previous_month: 'Попередній місяць' next_month: 'Наступний місяць' + placeholder: Виберіть дату share: topic: 'поширити посилання на цю тему' post: 'допис #%{postNumber}' @@ -67,16 +68,16 @@ uk: emails_are_disabled: "Надсилання повідомлень електронною поштою було глобально вимкнено адміністратором. Жодне сповіщення електронною поштою не буде надіслано." s3: regions: - us_west_2: "США Захід (Орегон)" - eu_west_1: "ЄС (Ірландія)" - eu_central_1: "ЄС (Франкфурт)" - ap_southeast_1: "Азія (Сингапур)" - ap_southeast_2: "Азія (Сідней)" - ap_south_1: "Азія (Мумбай)" ap_northeast_1: "Азія (Токіо)" ap_northeast_2: "Азія (Сеул)" - sa_east_1: "Південна Америка (Сан Паоло)" + ap_south_1: "Азія (Мумбай)" + ap_southeast_1: "Азія (Сингапур)" + ap_southeast_2: "Азія (Сідней)" cn_north_1: "Китай (Пекін)" + eu_central_1: "ЄС (Франкфурт)" + eu_west_1: "ЄС (Ірландія)" + sa_east_1: "Південна Америка (Сан Паоло)" + us_west_2: "США Захід (Орегон)" edit: 'редагувати назву та категорію цієї теми' not_implemented: "Цей функціонал ще не реалізовано, даруйте!" no_value: "Ні" @@ -1320,6 +1321,18 @@ uk: body: "Тіло" revert: "Повернути зміни" revert_confirm: "Чи впевнені ви, що хочете повернути ваші зміни?" + theme: + upload: "Завантажити" + about_theme: "Про тему" + license: "Ліцензія" + update_to_latest: "Оновити до останьої" + add: "Додати" + scss: + text: "CSS" + head_tag: + text: "" + body_tag: + text: "" colors: title: "Кольори" long_title: "Схеми Кольорів" @@ -1328,6 +1341,7 @@ uk: delete_confirm: "Видалити цю кольорову схему?" undo: "скасувати" undo_title: "Скасувати ваші зміни цього кольору з часу останнього збереження." + revert: "Повернути" primary: name: 'основний' secondary: @@ -1345,6 +1359,7 @@ uk: success: name: 'успіх' love: + name: 'любов' description: "Колір кнопки \"Подобається\"." email: title: "Листів" @@ -1415,6 +1430,8 @@ uk: block: "block" do_nothing: "do nothing" staff_actions: + all: "всі" + filter: "Filter:" title: "Дії персоналу" clear_filters: "Показати все" staff_user: "Член персоналу" @@ -1453,6 +1470,8 @@ uk: revoke_admin: "відкликати адміністраторство" grant_moderation: "зробити модератором" revoke_moderation: "відкликати модераторство" + deleted_tag: "видалити тег" + lock_trust_level: "заблокувати рівень довіри" screened_emails: title: "Screened Emails" description: "Коли хтось намагатиметься створити новий обліковий запис, наступні електронні скриньки буде перевірено, і реєстрацію заблоковано, або вжито якихось інших дій." @@ -1609,6 +1628,7 @@ uk: confirm: 'Підтвердження' site_text: edit: 'редагувати' + revert: "Повернути зміни" go_back: "Повернутись до пошуку" show_overriden: 'Показувати тільки перевизначені' site_settings: @@ -1639,9 +1659,14 @@ uk: uncategorized: 'Інше' backups: "Backups" login: "Вхід" + plugins: "Плагіни" user_preferences: "Налаштування" + tags: "Теги" search: "Пошук" + groups: "Групи" badges: + title: Значки + new_badge: Новий значок new: Новий name: Ім'я badge: Значок diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 877e03260c..324705cd3b 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -132,19 +132,19 @@ ur: bootstrap_mode_disabled: "بوٹسٹریپ کا موڈ اگلے 24 گھنٹوں میں غیر فعال کر دیا جائے گا۔" s3: regions: - us_east_1: "امریکی مشرق (شمالی ورجینیا)" - us_west_1: "امریکی مغرب (شمالی کیلی فورنیا)" - us_west_2: "امریکی مغرب (اوریگن)" - us_gov_west_1: "اے ڈبلیو ایس گَو کلاوڈ (امریکہ)" - eu_west_1: "یورپی یونین (آئر لینڈ)" - eu_central_1: "یورپی یونین (فرینکفرٹ)" - ap_southeast_1: "ایشیا پیسفک (سنگاپور)" - ap_southeast_2: "ایشیا پیسفک (سڈنی)" - ap_south_1: "ایشیا پیسفک (ممبئی)" ap_northeast_1: "ایشیا پیسفک (ٹوکیو)" ap_northeast_2: "ایشیا پیسفک (سیول)" - sa_east_1: "ایشیا پیسفک (ساؤ پالو)" + ap_south_1: "ایشیا پیسفک (ممبئی)" + ap_southeast_1: "ایشیا پیسفک (سنگاپور)" + ap_southeast_2: "ایشیا پیسفک (سڈنی)" cn_north_1: "چین (بیجنگ)" + eu_central_1: "یورپی یونین (فرینکفرٹ)" + eu_west_1: "یورپی یونین (آئر لینڈ)" + sa_east_1: "ایشیا پیسفک (ساؤ پالو)" + us_east_1: "امریکی مشرق (شمالی ورجینیا)" + us_gov_west_1: "اے ڈبلیو ایس گَو کلاوڈ (امریکہ)" + us_west_1: "امریکی مغرب (شمالی کیلی فورنیا)" + us_west_2: "امریکی مغرب (اوریگن)" edit: 'اس ٹاپک کے عنوان اور زمرے میں ترمیم کریں' not_implemented: "یہ خصوصیت ابھی تک لاگو نہیں کی گئی، معضرت!" no_value: "نہیں " diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 52935a2e79..9a29d1b349 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -130,20 +130,20 @@ vi: default_description: "Mặc định" s3: regions: - us_east_1: "US East (N. Virginia)" - us_west_1: "US West (N. California)" - us_west_2: "US West (Oregon)" - us_gov_west_1: "AWS GovCloud (US)" - eu_west_1: "EU (Ireland)" - eu_west_2: "EU (London)" - eu_central_1: "EU (Frankfurt)" - ap_southeast_1: "Asia Pacific (Singapore)" - ap_southeast_2: "Asia Pacific (Sydney)" - ap_south_1: "Châu Á Thái Bình Dương (Mumbai)" ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" - sa_east_1: "South America (Sao Paulo)" + ap_south_1: "Châu Á Thái Bình Dương (Mumbai)" + ap_southeast_1: "Asia Pacific (Singapore)" + ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "Trung Quốc (Bắc Kinh)" + eu_central_1: "EU (Frankfurt)" + eu_west_1: "EU (Ireland)" + eu_west_2: "EU (London)" + sa_east_1: "South America (Sao Paulo)" + us_east_1: "US East (N. Virginia)" + us_gov_west_1: "AWS GovCloud (US)" + us_west_1: "US West (N. California)" + us_west_2: "US West (Oregon)" edit: 'sửa tiêu đề và chuyên mục của chủ đề' not_implemented: "Tính năng này chưa được hoàn thiện hết, xin lỗi!" no_value: "Không" @@ -370,7 +370,7 @@ vi: none: "Không có gì" notifications: watching: - title: "Đang xem" + title: "Đang theo dõi" description: "Bạn sẽ được thông báo khi có bài viết mới trong mỗi tin nhắn, và số lượng trả lời mới sẽ được hiển thị" watching_first_post: title: "Theo dõi chủ đề đầu tiên" @@ -458,7 +458,7 @@ vi: rate_limit_error: "Bài viết chỉ được tải về một lần mỗi người, hãy thử lại vào ngày mai." new_private_message: "Tin nhắn mới" private_message: "Tin nhắn" - private_messages: "Các tin nhắn" + private_messages: "Tin nhắn" activity_stream: "Hoạt động" preferences: "Tùy chỉnh" expand_profile: "Mở" @@ -482,6 +482,7 @@ vi: first_notification: "Thông báo đầu tiên của bạn! Chọn để bắt đầu" disable_jump_reply: "Đừng tới bài viết của tôi sau khi tôi trả lời" dynamic_favicon: "Hiện số chủ đề mới / cập nhật vào biểu tượng trình duyệt" + theme_default_on_all_devices: "Đặt giao diện này làm giao diện mặc định trên tất cả thiết bị của tôi" external_links_in_new_tab: "Mở tất cả liên kết bên ngoài trong thẻ mới" enable_quoting: "Bật chế độ làm nổi bật chữ trong đoạn trích dẫn trả lời" change: "thay đổi" @@ -511,7 +512,8 @@ vi: tracked_tags_instructions: "Chế độ theo dõi sẽ tự động bật với những chủ đề được gắn thẻ này. Số lượng bài viết mới sẽ xuất hiện bên cạnh chủ đề." muted_tags: "Im lặng" muted_tags_instructions: "Bạn sẽ không được thông báo về bất kì hoạt động nào ở những chủ đề có thẻ này, chúng cũng sẽ không xuất hiện như là những chủ đề mới nhất." - watched_categories: "Xem" + watched_categories: "Đã theo dõi" + watched_categories_instructions: "Bạn sẽ tự động theo dõi tất cả các chủ đề trong những chuyên mục này. Bạn sẽ nhận được tin báo về những bài viết và chủ đề mới, cùng với số lượng bài viết mới cũng sẽ xuất hiện kế bên chủ đề đó." tracked_categories: "Theo dõi" watched_first_post_categories: "Xem bài viết đầu tiên" watched_first_post_categories_instructions: "Bạn sẽ nhận được thông báo khi có ai đó đăng chủ đề mới trong thư mục này." @@ -529,7 +531,7 @@ vi: muted_users: "Im lặng" muted_users_instructions: "Ngăn chặn tất cả các thông báo từ những thành viên." muted_topics_link: "Hiển thị chủ đề Im Lặng" - watched_topics_link: "HIển thị chủ đề đã xem" + watched_topics_link: "HIển thị các chủ đề đã theo dõi." automatically_unpin_topics: "Tự động bỏ ghim chủ đề khi tôi xuống cuối trang." apps: "Ứng dụng" revoke_access: "Lấy lại quyền" @@ -558,7 +560,9 @@ vi: profile: "Hồ sơ" emails: "Email" notifications: "Thông báo" + categories: "Chuyên mục" tags: "Thẻ" + interface: "Giao diện" apps: "Ứng dụng" change_password: success: "(email đã gửi)" @@ -853,6 +857,7 @@ vi: trust_level: 'Độ tin tưởng' search_hint: 'username, email or IP address' create_account: + disclaimer: "Với việc đăng ký, tức là bạn đồng ý với chính sách riêng tưđiều khoản dịch vụ." title: "Tạo tài khoản mới" failed: "Có gì đó không đúng, có thể email này đã được đăng ký, thử liên kết quên mật khẩu" forgot_password: @@ -966,6 +971,7 @@ vi: title: "Hoặc nhất Ctrl+Enter" users_placeholder: "Thêm thành viên " title_placeholder: "Tóm tắt lại thảo luận này trong một câu ngắn gọn" + title_or_link_placeholder: "Nhập tiêu đề, hoặc dán đường dẫn vào đây" edit_reason_placeholder: "Tại sao bạn sửa" show_edit_reason: "(thêm lý do sửa)" reply_placeholder: "Gõ ở đây. Sử dụng Markdown, BBCode, hoặc HTML để định dạng. Kéo hoặc dán ảnh." @@ -1251,6 +1257,7 @@ vi: auto_close_title: 'Tự động-Đóng các Cài đặt' timeline: back: "Quay lại" + replies_short: "%{current} / %{total}" progress: title: tiến trình của chủ đề go_top: "trên cùng" @@ -1264,21 +1271,23 @@ vi: current: bài viết hiện tại notifications: reasons: - '3_6': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chuyên mục nàyotification' - '3_5': 'Bạn sẽ nhận được các thông báo bởi vì bạn đã bắt đầu xem chủ đề này một cách tự động' - '3_2': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chủ đề này' - '3_1': 'Bạn sẽ được nhận thông báo bởi bạn đã tạo chủ để này.' - '3': 'Bạn sẽ nhận được các thông báo bởi vì bạn đang xem chủ đề này' - '1_2': 'Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn' - '1': 'Bạn sẽ được thông báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn' - '0_7': 'Bạn đang bỏ qua tất cả các thông báo trong chuyên mục này' + '3_6': 'Bạn sẽ nhận được các tin báo bởi vì bạn đang theo dõi chuyên mục này.' + '3_5': 'Bạn sẽ nhận được tin báo bởi vì bạn đã bắt đầu theo dõi chủ đề này một cách tự động.' + '3_2': 'Bạn sẽ nhận được các tin báo bởi vì bạn đang theo dõi chủ đề này.' + '3_1': 'Bạn sẽ được nhận các tin báo bởi bạn đã tạo chủ để này.' + '3': 'Bạn sẽ nhận được các tin báo bởi vì bạn đang theo dõi chủ đề này.' + '2_8': 'Bạn sẽ thấy được 1 số lượng bài viết mới bởi vì bạn đang theo dấu chuyên mục này.' + '2': 'Bạn sẽ xem được các bài trả lời bởi vì bạn đọc chủ đề này.' + '1_2': 'Bạn sẽ được tin báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn.' + '1': 'Bạn sẽ được tin báo nếu ai đó đề cập đến @tên bạn hoặc trả lời bạn.' + '0_7': 'Bạn đang bỏ qua tất cả các tin báo trong chuyên mục này.' '0_2': 'Bạn đang bỏ qua tất cả các thông báo trong chủ đề này' '0': 'Bạn đang bỏ qua tất cả các thông báo trong chủ đề này' watching_pm: - title: "Đang xem" + title: "Đang theo dõi" description: "Bạn sẽ được thông báo về từng trả lời mới trong tin nhắn này, và một số trả lời mới sẽ được hiển thị" watching: - title: "Dang theo dõi" + title: "Đang theo dõi" description: "Bạn sẽ được thông báo về từng trả lời mới trong tin nhắn này, và một số trả lời mới sẽ được hiển thị" tracking_pm: title: "Đang theo dõi" @@ -1659,7 +1668,7 @@ vi: sort_options: default: "mặc định" likes: "Thích" - views: "Xem" + views: "Lượt xem" posts: "Bài viết" activity: "Hoạt động" posters: "Người gửi" @@ -1805,7 +1814,7 @@ vi: other: "{{categoryName}} ({{count}})" help: "Những chủ đề mới nhất trong chuyên mục{{categoryName}} " top: - title: "Trên" + title: "Top" help: "Các chủ đề tích cực nhất trong năm, tháng, tuần, hoặc ngày trước" all: title: "Từ trước tới nay" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index a482f16dde..aee14c49ca 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -9,7 +9,7 @@ zh_CN: js: number: format: - separator: "." + separator: ".暂无链接" delimiter: "," human: storage_units: @@ -130,20 +130,20 @@ zh_CN: default_description: "默认" s3: regions: - us_east_1: "美国东部(N. Virginia)" - us_west_1: "美国西部(N. California)" - us_west_2: "美国西部(Oregon)" - us_gov_west_1: "政府专用(US)" - eu_west_1: "欧洲(Ireland)" - eu_west_2: "欧洲(London)" - eu_central_1: "欧洲(Frankfurt)" - ap_southeast_1: "亚太地区(Singapore)" - ap_southeast_2: "亚太地区(Sydney)" - ap_south_1: "亚太地区(Mumbai)" ap_northeast_1: "亚太地区(Tokyo)" ap_northeast_2: "亚太地区(Seoul)" - sa_east_1: "南美(Sao Paulo)" + ap_south_1: "亚太地区(Mumbai)" + ap_southeast_1: "亚太地区(Singapore)" + ap_southeast_2: "亚太地区(Sydney)" cn_north_1: "中国(Beijing)" + eu_central_1: "欧洲(Frankfurt)" + eu_west_1: "欧洲(Ireland)" + eu_west_2: "欧洲(London)" + sa_east_1: "南美(Sao Paulo)" + us_east_1: "美国东部(N. Virginia)" + us_gov_west_1: "政府专用(US)" + us_west_1: "美国西部(N. California)" + us_west_2: "美国西部(Oregon)" edit: '编辑标题和分类' not_implemented: "非常抱歉,这个功能仍在开发中!" no_value: "否" @@ -322,7 +322,7 @@ zh_CN: to: "发至" edit: title: '编辑群组' - full_name: '名字' + full_name: '群组名' add_members: "增加组员" delete_member_confirm: "从“%{group}”群组中移除用户“%{username}'?" name_placeholder: "群组名,没有空格,和用户名一样的规则" @@ -337,12 +337,13 @@ zh_CN: join: "加入群组" leave: "离开群组" request: "请求加入群组" + message: "信息" automatic_group: 自动群组 closed_group: 封闭群组 is_group_user: "你是该群组的一个成员" allow_membership_requests: "允许用户向小组拥有者发送\b\b成员请求" membership: "成员资格" - name: "名字" + name: "群组ID" user_count: "成员数目" bio: "关于群组" selector_placeholder: "添加成员" @@ -359,6 +360,9 @@ zh_CN: mentions: "提及" messages: "私信" notification_level: "小组私信的默认通知等级" + visibility_levels: + public: "所有人" + members: "小组创建人,成员和管理员" alias_levels: title: "谁能@该群组和发送私信?" nobody: "没有人" @@ -770,11 +774,11 @@ zh_CN: no_badges: "暂无徽章。" more_badges: "更多徽章" top_links: "热门链接" - no_links: "暂无链接" + no_links: "暂无链接。" most_liked_by: "谁赞最多" most_liked_users: "赞谁最多" most_replied_to_users: "最多回复至" - no_likes: "暂无赞" + no_likes: "暂无赞。" associated_accounts: "登录" ip_address: title: "最后使用的 IP 地址" @@ -2376,7 +2380,7 @@ zh_CN: plugins: title: "插件" installed: "安装的插件" - name: "名字" + name: "插件名" none_installed: "你没有安装任何插件。" version: "版本" enabled: "启用?" @@ -2755,6 +2759,15 @@ zh_CN: title: "如果有至少 'min_ban_entries_for_roll_up' 个记录,创建一个子网封禁记录" logster: title: "错误日志" + watched_words: + search: "搜索" + clear_filter: "清除" + actions: + flag: '旗标' + form: + add: '新增' + success: '成功' + upload: "上传" impersonate: title: "检视用户视角" help: "使用此工具来检视其他用户的帐号以方便调试。你应该在完成后立即退出。" @@ -2935,7 +2948,7 @@ zh_CN: title: "单点登录" external_id: "外部 ID" external_username: "用户名" - external_name: "名字" + external_name: "外部系统中的名字" external_email: "电子邮件" external_avatar_url: "头像URL" user_fields: diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 3749292d44..0e63b67de4 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -123,19 +123,19 @@ zh_TW: bootstrap_mode_disabled: "初始化模式將會在24小時後關閉。" s3: regions: - us_east_1: "美國東部 (北維珍尼亞州)" - us_west_1: "美國西部 (北加州)" - us_west_2: "美國西部 (奧勒岡州)" - us_gov_west_1: "AWS GovCloud (美國)" - eu_west_1: "歐洲 (愛爾蘭)" - eu_central_1: "歐洲 (法蘭克福)" - ap_southeast_1: "亞太地區 (新加坡)" - ap_southeast_2: "亞太地區 (悉尼)" - ap_south_1: "亞太地區 (孟買)" ap_northeast_1: "亞太地區 (東京)" ap_northeast_2: "亞太地區 (首爾)" - sa_east_1: "南美洲 (聖保羅)" + ap_south_1: "亞太地區 (孟買)" + ap_southeast_1: "亞太地區 (新加坡)" + ap_southeast_2: "亞太地區 (悉尼)" cn_north_1: "中國 (北京)" + eu_central_1: "歐洲 (法蘭克福)" + eu_west_1: "歐洲 (愛爾蘭)" + sa_east_1: "南美洲 (聖保羅)" + us_east_1: "美國東部 (北維珍尼亞州)" + us_gov_west_1: "AWS GovCloud (美國)" + us_west_1: "美國西部 (北加州)" + us_west_2: "美國西部 (奧勒岡州)" edit: '編輯此討論話題的標題與分類' not_implemented: "抱歉,此功能尚未開放。" no_value: "否" diff --git a/config/locales/plurals.rb b/config/locales/plurals.rb index 9e6ea0b56e..ad223446ff 100644 --- a/config/locales/plurals.rb +++ b/config/locales/plurals.rb @@ -2,115 +2,115 @@ # source: https://github.com/svenfuchs/i18n/blob/master/test/test_data/locales/plurals.rb { - :af => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :am => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :ar => { :i18n => { :plural => { :keys => [:zero, :one, :two, :few, :many, :other], :rule => lambda { |n| n == 0 ? :zero : n == 1 ? :one : n == 2 ? :two : [3, 4, 5, 6, 7, 8, 9, 10].include?(n % 100) ? :few : [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99].include?(n % 100) ? :many : :other } } } }, - :az => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :be => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, - :bg => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :bh => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :bn => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :bo => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :bs => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, - :ca => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :cs => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n == 1 ? :one : [2, 3, 4].include?(n) ? :few : :other } } } }, - :cy => { :i18n => { :plural => { :keys => [:one, :two, :many, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : n == 8 || n == 11 ? :many : :other } } } }, - :da => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :de => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :dz => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :el => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :en => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :eo => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :es => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :et => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :eu => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :fa_IR => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :fi => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :fil => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :fo => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :fr => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n.between?(0, 2) && n != 2 ? :one : :other } } } }, - :fur => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :fy => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ga => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :gl => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :gu => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :guw => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :ha => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :he => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :hi => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :hr => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, - :hu => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :id => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :is => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :it => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :iw => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ja => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :jv => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :ka => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :km => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :kn => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :ko => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :ku => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :lb => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ln => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :lt => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n % 10 == 1 && ![11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :one : [2, 3, 4, 5, 6, 7, 8, 9].include?(n % 10) && ![11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :few : :other } } } }, - :lv => { :i18n => { :plural => { :keys => [:zero, :one, :other], :rule => lambda { |n| n == 0 ? :zero : n % 10 == 1 && n % 100 != 11 ? :one : :other } } } }, - :mg => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :mk => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n % 10 == 1 ? :one : :other } } } }, - :ml => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :mn => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :mo => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n == 1 ? :one : n == 0 ? :few : :other } } } }, - :mr => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ms => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :mt => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n == 1 ? :one : n == 0 || [2, 3, 4, 5, 6, 7, 8, 9, 10].include?(n % 100) ? :few : [11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :many : :other } } } }, - :my => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :nah => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :nb_NO => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ne => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :nl => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :nn => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :no => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :nso => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :om => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :or => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :pa => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :pap => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :pl_PL => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n == 1 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : :other } } } }, - :ps => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :pt => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :pt_BR => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ro => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n == 1 ? :one : n == 0 || ((n % 100) >= 1 && (n % 100) <= 19) ? :few : :other } } } }, - :ru => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : :other } } } }, - :se => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :sh => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, - :sk => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n == 1 ? :one : [2, 3, 4].include?(n) ? :few : :other } } } }, - :sl => { :i18n => { :plural => { :keys => [:one, :two, :few, :other], :rule => lambda { |n| n % 100 == 1 ? :one : n % 100 == 2 ? :two : [3, 4].include?(n % 100) ? :few : :other } } } }, - :sma => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :smi => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :smj => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :smn => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :sms => { :i18n => { :plural => { :keys => [:one, :two, :other], :rule => lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, - :so => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :sq => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :sr => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, - :sv => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :sw => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :ta => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :te => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :th => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :ti => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :tk => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :tl => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :to => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :tr_TR => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, + af: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + am: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + ar: { i18n: { plural: { keys: [:zero, :one, :two, :few, :many, :other], rule: lambda { |n| n == 0 ? :zero : n == 1 ? :one : n == 2 ? :two : [3, 4, 5, 6, 7, 8, 9, 10].include?(n % 100) ? :few : [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99].include?(n % 100) ? :many : :other } } } }, + az: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + be: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, + bg: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + bh: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + bn: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + bo: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + bs: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, + ca: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + cs: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n == 1 ? :one : [2, 3, 4].include?(n) ? :few : :other } } } }, + cy: { i18n: { plural: { keys: [:one, :two, :many, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : n == 8 || n == 11 ? :many : :other } } } }, + da: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + de: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + dz: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + el: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + en: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + eo: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + es: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + et: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + eu: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + fa_IR: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + fi: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + fil: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + fo: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + fr: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n.between?(0, 2) && n != 2 ? :one : :other } } } }, + fur: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + fy: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ga: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + gl: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + gu: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + guw: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + ha: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + he: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + hi: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + hr: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, + hu: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + id: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + is: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + it: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + iw: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ja: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + jv: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + ka: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + km: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + kn: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + ko: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + ku: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + lb: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ln: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + lt: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n % 10 == 1 && ![11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :one : [2, 3, 4, 5, 6, 7, 8, 9].include?(n % 10) && ![11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :few : :other } } } }, + lv: { i18n: { plural: { keys: [:zero, :one, :other], rule: lambda { |n| n == 0 ? :zero : n % 10 == 1 && n % 100 != 11 ? :one : :other } } } }, + mg: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + mk: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n % 10 == 1 ? :one : :other } } } }, + ml: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + mn: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + mo: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n == 1 ? :one : n == 0 ? :few : :other } } } }, + mr: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ms: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + mt: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n == 1 ? :one : n == 0 || [2, 3, 4, 5, 6, 7, 8, 9, 10].include?(n % 100) ? :few : [11, 12, 13, 14, 15, 16, 17, 18, 19].include?(n % 100) ? :many : :other } } } }, + my: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + nah: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + nb_NO: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ne: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + nl: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + nn: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + no: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + nso: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + om: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + or: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + pa: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + pap: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + pl_PL: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n == 1 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : :other } } } }, + ps: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + pt: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + pt_BR: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ro: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n == 1 ? :one : n == 0 || ((n % 100) >= 1 && (n % 100) <= 19) ? :few : :other } } } }, + ru: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : :other } } } }, + se: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + sh: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, + sk: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n == 1 ? :one : [2, 3, 4].include?(n) ? :few : :other } } } }, + sl: { i18n: { plural: { keys: [:one, :two, :few, :other], rule: lambda { |n| n % 100 == 1 ? :one : n % 100 == 2 ? :two : [3, 4].include?(n % 100) ? :few : :other } } } }, + sma: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + smi: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + smj: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + smn: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + sms: { i18n: { plural: { keys: [:one, :two, :other], rule: lambda { |n| n == 1 ? :one : n == 2 ? :two : :other } } } }, + so: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + sq: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + sr: { i18n: { plural: { keys: [:one, :few, :many, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, + sv: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + sw: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + ta: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + te: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + th: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + ti: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + tk: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + tl: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + to: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + tr_TR: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, # TODO put this back when translations are correct: :uk => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } }, - :uk => { :i18n => { :plural => { :keys => [:one, :few, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :other : :other } } } }, - :ur => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } }, - :vi => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :wa => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, - :yo => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :zh => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :zh_CN => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :zh_TW => { :i18n => { :plural => { :keys => [:other], :rule => lambda { |n| :other } } } }, - :zu => { :i18n => { :plural => { :keys => [:one, :other], :rule => lambda { |n| n == 1 ? :one : :other } } } } + uk: { i18n: { plural: { keys: [:one, :few, :other], rule: lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :other : :other } } } }, + ur: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, + vi: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + wa: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, + yo: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + zh: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + zh_CN: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + zh_TW: { i18n: { plural: { keys: [:other], rule: lambda { |n| :other } } } }, + zu: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } } } diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index bff1fc2adf..eda44f9408 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -27,11 +27,15 @@ ar: topics: "المواضيع" posts: "المشاركات" loading: "يحمّل" + powered_by_html: 'تدعمه دسكورس ، يفضّل عرضه وجافاسكربت مفعّل' log_in: "لِج" purge_reason: "حُذف آليًّا باعتباره حسابًا إمّا مهجورًا أو غير مفعّل" disable_remote_images_download_reason: "عُطّل تنزيل الصور عن بعد بسبب نفاذ مساحة القرص الحرّة." anonymous: "مجهول" remove_posts_deleted_by_author: "حذفها الكاتب" + themes: + bad_color_scheme: "لا يمكن تحديث السمة ، مخطط ألوان غير صحيح" + other_error: "حصل خطأ لتحديث السمة" emails: incoming: default_subject: "يحتاج هذا الموضوع عنوانًا" @@ -122,6 +126,7 @@ ar: default_categories_already_selected: "لا يمكنك تحديد فئة تستخدم في قائمة أخرى." s3_upload_bucket_is_required: "لا يمكنك تفعيل الرفع إلى S3 حتّى توفّر 's3_upload_bucket'." bulk_invite: + file_should_be_csv: "الملف المرفوع يجب أن يكون بنسق csv . " error: "حدث عطل أثناء رفع الملفّ. رجاء أعد المحاولة لاحقًا." topic_invite: user_exists: "آسف، ذلك المستخدم قد تمت دعوته من قبل. تستطيع فقط أن تدعوا عضواً لموضوعِ مرة واحدة." @@ -146,6 +151,8 @@ ar: embed: start_discussion: "ابدأ النّقاش" continue: "تابع النّقاش" + error: "خطأ في الدمج" + referer: "المرجع:" more_replies: zero: "لا ردود أخرى" one: "رد واحد آخر" @@ -285,6 +292,12 @@ ar: -نرحب بالنقد البناء، ولكن انتقد الأفكار ولا تنتقد الأشخاص! للمزيد, [انظر إرشادات المجتمع](/guidelines). هذه اللوحة ستظهر فقط لأول %{education_posts_text} لك. + dominating_topic: | + ### دع الآخرون ينضمون للمحادثة + + هذا الموضوع واضحٌ أنه مهم بالنسبة لك – قمت بمشاركة أكثر من %{percent}% الردود هنا. + + هل أنت متأكد من أنك أعطيت وقت كافٍ للآخرين لمشاركة نقاط نظرهم أيضاً ؟ activerecord: attributes: category: @@ -946,7 +959,6 @@ ar: notify_mods_when_user_blocked: "إذا تم حظر المستخدم تلقائيا، وإرسال رسالة الى جميع المشرفين." flag_sockpuppets: "إذا رد أحد المستخدمين جديد إلى موضوع من عنوان IP نفسه باسم المستخدم الجديد الذي بدأ هذا الموضوع، علم كل من مناصبهم كدعاية المحتملين." traditional_markdown_linebreaks: "استعمل السطور التالفه التقليديه في Markdown, التي تتطلب مساحتين بيضاوين للسطور التالفه" - allow_html_tables: "كل الجداول يجب ان تدخل ب لغة ال HTML مثال TABLE , THEAD , TD , TR , TH سوف يأوذن لهم ( تتطلب مراجعة لكل المقالات القديمة )" post_undo_action_window_mins: "عدد الدقائق التي يسمح فيها للأعضاء بالتراجع عن آخر إجراءاتهم على المنشور (إعجاب، اشارة، إلخ...)" must_approve_users: "يجب أن الموظفين يوافق على جميع حسابات المستخدم الجديدة قبل أن يتم السماح لهم للوصول إلى الموقع. تحذير: تمكين هذا لموقع الحية إلغاء وصول المستخدمين الحاليين غير الموظفين!" pending_users_reminder_delay: "نبه المشرفين إذا وجد اعضاء ينتظرون الموافقة لمدة اطول من الساعات ، قم بوضع الخيار -1 لايقاف التنبيهات ." @@ -1240,6 +1252,7 @@ ar: default_categories_watching: "قائمة الفئات التي تشاهد إفتراضيا." default_categories_tracking: "قائمة الفئات التي تتابع إفتراضيا." default_categories_muted: "قائمة الفئات التي توضع صامتة إفتراضيا." + tagging_enabled: "فعّل الأوسمة على المواضيع." company_short_name: "اسم الشّركة (قصير)" company_full_name: "اسم الشّركة (الكامل)" company_domain: "نطاق الشّركة" @@ -1364,7 +1377,7 @@ ar: errors: "%{errors}" not_available: "غير متاح. جرّب %{suggestion} ؟" something_already_taken: "حدث خطأ ما, ربما اسم المستخدم و البريد الالكتروني مسجل مسبقا, جرب رابط نسيان كلمة المرور." - omniauth_error: "نأسف, هناك خطأ في تصريح حسابك. ربما لم تعطي تصريح؟" + omniauth_error: "نأسف، حدث خطأ في استيثاق حسابك. لربمّا لم تقبل الاستيثاق؟" omniauth_error_unknown: "حدث خطأ ما في معالجة دخولك، الرجاء إعادة المحاولة." new_registrations_disabled: "لا يُسمح بتسجيل حساب جديد بهذا الوقت " password_too_long: "كلمة السّرّ مقصورة على 200 محرف." @@ -1441,16 +1454,20 @@ ar: deferred: "نشكرك لإعلامنا بذلك. نحن ننظر في الأمر." deferred_and_deleted: "نشكرك لإعلامنا بذلك. لقد أزلنا المشاركة." system_messages: + contents_hidden: "يُرجى زيارة المشاركة لرؤية محتوياتها" post_hidden: title: "المشاركة مخفية" subject_template: "المشاركة مخفية بتصويت الاعضاء" welcome_user: + title: "ترحيب المستخدم" subject_template: "مرحبا بك في %{site_name}!" welcome_invite: + title: "ترحيب دعوة" subject_template: "مرحبا بك في %{site_name}!" backup_succeeded: subject_template: "اكتملت عملية النسخ الإحتياطي بنجاح" backup_failed: + title: "النسخة إحتياطية فشلت" subject_template: "فشل النسخ الإحتياطي" restore_succeeded: subject_template: "أكتملت الإستعادة بنجاح" @@ -1465,7 +1482,8 @@ ar: subject_template: "اكتمل تصدير البيانات" csv_export_failed: subject_template: "فشل تصدير البيانات" - text_body_template: "نأسف، فشل تصدير البيانات. رجاء افحص السّجلّات أو راسل أحد أعضاء الطّاقم." + email_reject_blocked_user: + title: "رفض البريد حجب المستخدم" email_reject_empty: text_body_template: | آسفون، لكن رسالة البريد الالكتروني إلى %{destination} (titled %{former_title}) لم تنجح . @@ -1494,6 +1512,10 @@ ar: download_remote_images_disabled: subject_template: "الغاء تفعيل تحميل الصور عن بعد " text_body_template: "تم تعطيل الإعداد 'download_remote_images_to_local' لأنه تم الوصول إلى حد مساحة القرص في 'download_remote_images_threshold'." + unsubscribe_mailing_list: | + استلمت هذا بسبب أنك قمت بتفعيل وضع القائمة البريدية. + + لإلغاء الاشتراك من البريد الالكتروني ، [ انقر هنا](%{unsubscribe_url}). subject_re: "اعادة " user_notifications: previous_discussion: "الردود السابقة " @@ -1501,6 +1523,8 @@ ar: unsubscribe: title: "غير مشترك " description: "لست مهتما في تلقي هذه الرسائل الالكترونيه؟ لا مشكله! اضغط تحت ليتم الغاء اشتركك فورا:" + reply_by_email: "[زيارة الموضوع](%{base_url}%{url}) أو بالرد على هذا البريد للإجابه." + visit_link_to_respond: "[زيارة الموضوع](%{base_url}%{url}) للرد." visit_link_to_respond_pm: "[زُر الرّسالة](%{base_url}%{url}) للرّدّ." posted_by: "مشاركة بواسطة %{username} على %{post_date}" user_posted_pm_staged: @@ -1508,6 +1532,9 @@ ar: digest: why: "ملخص المؤجز من %{site_link} منذ زيارتك الاخيره في %{last_seen_at}" click_here: "أنقر هنا" + account_created: + title: "الحساب أُنشئ" + subject_template: "حسابك الجديد [%{email_prefix}]" signup_after_approval: subject_template: "قد وافقت على %{site_name}!" page_not_found: @@ -1744,6 +1771,8 @@ ar: performance_report: initial_post_raw: 'هذا الموضوع يحتوي على معلومات الاداء اليومي للموقع ' initial_topic_title: التبليغ عن اداء الموقع + tags: + title: "الأوسمة" finish_installation: congratulations: "تهانينا على تثبيت دسكورس!" wizard: diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 7251571b05..0dc576ad1f 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -164,6 +164,7 @@ cs: admins: "admins" moderators: "moderators" staff: "staff" + trust_level_0: "trust_level_0" trust_level_1: "trust_level_1" trust_level_2: "trust_level_2" trust_level_3: "trust_level_3" @@ -187,6 +188,8 @@ cs: attributes: category: name: "Název kategorie" + topic: + title: 'Nadpis' post: raw: "Tělo" user_profile: @@ -206,12 +209,17 @@ cs: common: "je jedno z 10 000 nejčastějších hesel. Použijte prosím bezpečnější heslo." same_as_username: "je stejné jako vaše uživatelské jméno. Zkuste vymyslet bezpečnější heslo." same_as_email: "je stejné jako váš email. Zkuste vymyslet bezpečnější heslo." + same_as_current: "je stejné jako vaše současné heslo." + unique_characters: "má příliš mnoho opakujících se znaků. Použijte prosím bezpečnější heslo." ip_address: signup_not_allowed: "Registrace z této adresy není povolena." color_scheme_color: attributes: hex: invalid: "není platná barva" + post_reply: + base: + different_topic: "Příspěvek a odpověď musí patřit ke stejnému tématu." <<: *errors user_profile: no_info_other: "
    %{name} o sobě zatím žádné informace nevyplnil
    " diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index b8244744d0..5651d7964a 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -903,7 +903,6 @@ da: notify_mods_when_user_blocked: "Send en besked til alle moderatorer hvis en bruger blokeres automatisk" flag_sockpuppets: "Hvis en ny bruger svarer på et emne fra den samme IP adresse som den der startede emnet, så rapporter begge at deres indlæg potentielt er spam." traditional_markdown_linebreaks: "Brug traditionelle linjeskift i Markdown, som kræver 2 mellemrum i slutningen af sætningen." - allow_html_tables: "Tillad tabeller at blive oprettet i Markdown med brug af HTML tags. TABLE, THEAD, TD, TR, TH vil blive tilladt (kræver en fuld re-indeksering af gamle indlæg som benytter tabeller) " post_undo_action_window_mins: "Antal minutter som brugere er tilladt at fortryde handlinger på et indlæg (like, flag, etc)." must_approve_users: "Personale skal godkende alle nye bruger konti inden de kan tilgå sitet. ADVARSEL: aktivering af dette for et live site vil medføre en ophævning af adgang for eksisterende ikke-personale brugere." pending_users_reminder_delay: "Underret moderatorer hvis nye brugere har ventet på godkendelse i længere end så mange timer. Skriv -1 for at deaktivere notifikationer." @@ -1151,40 +1150,8 @@ da: invite_mailer: subject_template: "%{invitee_name} inviterede dig til '%{topic_title}' på %{site_domain_name}" - text_body_template: | - %{invitee_name} inviterede dig til en debat om - - > **%{topic_title}** - > - > %{topic_excerpt} - - på - - > %{site_title} -- %{site_description} - - Hvis du er interesseret, så klik på linket herunder: - - %{invite_link} custom_invite_mailer: subject_template: "%{invitee_name} inviterede dig til '%{topic_title}' på %{site_domain_name}" - text_body_template: | - %{invitee_name} inviterede dig til en debat - - > **%{topic_title}** - > - > %{topic_excerpt} - - på - - > %{site_title} -- %{site_description} - - Med denne besked - - > %{user_custom_message} - - Hvis du er interesseret, så klik på linket herunder: - - %{invite_link} invite_forum_mailer: subject_template: "%{invitee_name} inviterede dig til at oprette dig på %{site_domain_name}" custom_invite_forum_mailer: @@ -1301,16 +1268,6 @@ da: Den konto, der er knyttet til denne mailadresse, er blokeret. - email_reject_reply_user_not_matching: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Dit svar blev sendt fra en anden emailadresse end den forventede, så vi er ikke sikre på, at det er den samme person. Prøv at sende fra en anden emailadresse eller kontakt hjælperteamet. - email_reject_no_account: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Vi kan ikke finde nogen konto, der matcher din emailadresse. Prøv at sende fra en anden emailadresse eller kontakt hjælperteamet. email_reject_empty: text_body_template: |+ Vi beklager, men din mailbesked til %{destination} (titled %{former_title}) fungerede ikke. @@ -1324,16 +1281,6 @@ da: Vi beklager, men din mailbesked til %{destination} (titled %{former_title}) fungerede ikke. Vi kunne ikke finde dit svar i mailen. **Sørg for at dit svar står øverst i mailen** -- vi kan ikke behandle svar, der er indsat i den oprindelige tekst. - email_reject_invalid_access: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Din konto har ikke tilladelse til at oprette nye emner i kategorien. Hvis du mener, at det er en fejl, så kontakt hjælperteamet. - email_reject_strangers_not_allowed: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Kategorien du sendte mailen til tillader kun svar fra brugere med gyldige kontoer og kendte emailadresser. Hvis du mener, at det er en fejl, så kontakt hjælperteamet. email_reject_invalid_post: text_body_template: | Vi beklager, men din mailbesked til %{destination} (titled %{former_title}) fungerede ikke. @@ -1354,31 +1301,11 @@ da: Send-handlingen blev ikke genkendt. Prøv igen, eller send via websitet, hvis det fortsætter. - email_reject_reply_key: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Svarknappen i emailen er ugyldig eller ukendt, så vi ved ikke, hvad denne email er et svar til. Kontakt hjælpeteamet. email_reject_bad_destination_address: text_body_template: | Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. Ingen af modtageradresserne kan genkendes. Dobbelttjek at du sender til den korrekte emailadresse fra hjælperteamet. - email_reject_topic_not_found: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Emnet du besvarer eksisterer ikke længere - Det er måske slettet? Hvis du mener, at det er en fejl, så kontakt hjælperteamet. - email_reject_topic_closed: - text_body_template: | - Desværre, din mail til %{destination} (titled %{former_title}) virkede ikke. - - Emnet du besvarer er i øjeblikket lukket og modtager ikke længere svar. Hvis du mener, at det er en fejl, så kontakt hjælperteamet. - email_reject_auto_generated: - text_body_template: | - Vi beklager, men din mailbesked til %{destination} (titled %{former_title}) fungerede ikke. - - Din mail blev markeret som "autogenereret", hvilket betyder, at den er blevet oprettet automastisk af en computer i stedet for at blive tastet ind af en peron; vi kan ikke acceptere den slags mails. Hvis du mener, det er en fejl, kan du kontakte et medlem af staben. too_many_spam_flags: title: "For Mange Flagmarkeringer for Spam" subject_template: "Nyoprettet konto sat på hold" diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 024f52ccf2..a9703654f1 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -860,7 +860,6 @@ de: educate_until_posts: "Zeige das Hilfe-Panel im Editor sobald ein Benutzer einen seiner ersten (n) Beiträge zu schreiben beginnt." title: "Der Name dieser Site, wird für das Title-Tag verwendet." site_description: "Beschreibe diese Site in einem Satz. Wird für das \"description\" Meta-Tag verwendet." - contact_email: "Die E-Mail-Adresse des Hauptverantwortlichen dieser Site. Wird verwendet bei kritischen Benachrichtigungen wie beispielsweise unbearbeitete Meldungen, als auch solche die via /about Kontaktformular eintreffen. " contact_url: "URL für Kontaktanfragen bezüglich dieser Site. Wird unter /about im Kontaktformular für dringende Angelegenheiten verwendet." queue_jobs: "NUR FÜR ENTWICKLER! ACHTUNG! Benutze Sidekiq zur Ausführung von Jobs. Wenn dies abgeschaltet wird, dann wird die Site nicht mehr richtig funktionieren!" crawl_images: "Lade Bilder von fremden URLs herunter, um ihre Höhe und Breite zu bestimmen." @@ -921,7 +920,6 @@ de: notify_mods_when_user_blocked: "Wenn ein Benutzer automatisch gesperrt wird, sende eine Nachricht an alle Moderatoren." flag_sockpuppets: "Wenn ein neuer Benutzer auf ein Thema antwortet, das von einem anderen neuen Benutzer aber mit der gleichen IP-Adresse begonnen wurde, markiere beide Beiträge als potenziellen Spam." traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, die zwei nachfolgende Leerzeichen für einen Zeilenumbruch benötigen." - allow_html_tables: "Erlaube es, Tabellen in Markdown mit HTML-Tags einzugeben. TABLE, THEAD, TD, TR, TH werden erlaubt (alle Beiträge mit Tabellen müssen ihr HTML erneuern)" post_undo_action_window_mins: "Minuten, die ein Benutzer hat, um Aktionen auf einen Beitrag rückgängig zu machen (Gefällt mir, Meldung, usw.)." must_approve_users: "Team-Mitglieder müssen alle neuen Benutzerkonten freischalten, bevor diese Zugriff auf die Website erhalten. ACHTUNG: Das Aktivieren dieser Option für eine Live-Site entfernt den Zugriff auch für alle existierenden Benutzer außer für Team-Mitglieder!" pending_users_reminder_delay: "Benachrichtige die Moderatoren, falls neue Benutzer mehr als so viele Stunden auf ihre Genehmigung gewartet haben. Stelle -1 ein, um diese Benachrichtigungen zu deaktivieren." @@ -1520,41 +1518,9 @@ de: invite_mailer: title: "Einladung zu einem Thema" subject_template: "%{invitee_name} hat dich zum Thema '%{topic_title}' auf %{site_domain_name} eingeladen" - text_body_template: | - %{invitee_name} hat dich eingeladen zur Diskussion - - > **%{topic_title}** - > - > %{topic_excerpt} - - bei - - > %{site_title} -- %{site_description} - - Wenn du interessiert bist, klicke auf den folgenden Link: - - %{invite_link} custom_invite_mailer: title: "Einladung zu einem Thema (mit Nachricht)" subject_template: "%{invitee_name} hat dich eingeladen zu '%{topic_title}' auf %{site_domain_name}" - text_body_template: | - %{invitee_name} hat dich eingeladen zur Diskussion - - > **%{topic_title}** - > - > %{topic_excerpt} - - bei - - > %{site_title} -- %{site_description} - - Mit dieser Notiz: - - > %{user_custom_message} - - Falls du Interesse hast, klicke auf den nachfolgenden Link: - - %{invite_link} invite_forum_mailer: title: "Einladung zur Teilnahme" subject_template: "%{invitee_name} hat dich eingeladen %{site_domain_name} beizutreten" @@ -1840,28 +1806,15 @@ de: csv_export_failed: title: "CSV-Export fehlgeschlagen" subject_template: "Datenexport fehlgeschlagen" - text_body_template: "Entschuldigung, beim Exportieren deiner Daten trat ein Fehler auf. Bitte kontaktiere ein Team-Mitglied oder überprüfe die Logdateien." email_reject_insufficient_trust_level: title: "E-Mail abgelehnt weil Vertrauensstufe unzureichend" subject_template: "[%{email_prefix}] E-Mail-Probme -- Vertrauensstufe nicht ausreichend" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (Titel: %{former_title}) konnte nicht zugestellt werden. - - Du hast nicht die notwendige Vertrauensstufe, um neue Themen über diese E-Mail-Adresse zu erstellen. Wenn du glaubst, dass das ein Irrtum ist, dann kontaktiere ein Team-Mitglied. email_reject_user_not_found: title: "E-Mail abgelehnt weil Benutzer nicht gefunden" subject_template: "[%{email_prefix}] E-Mail-Problem -- Benutzer nicht gefunden" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Deine Antwort wurde von einer unbekannten E-Mail-Adresse gesendet. Probiere eine andere Absende-Adresse oder wende dich an ein Team-Mitglied. email_reject_screened_email: title: "E-Mail abgelehnt weil E-Mail-Adresse gesperrt" subject_template: "[%{email_prefix}] E-Mail-Problem -- E-Mail-Adresse blockiert" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Deine Antwort wurde von einer blockierten E-Mail-Adresse gesendet. Probiere eine andere Absende-Adresse oder wende dich an ein Team-Mitglied. email_reject_inactive_user: title: "E-Mail abgelehnt weil Benutzer inaktiv" subject_template: "[%{email_prefix}] E-Mail-Problem -- Benutzer inaktiv" @@ -1879,17 +1832,9 @@ de: email_reject_reply_user_not_matching: title: "E-Mail abgelehnt weil E-Mail-Adresse abweichend" subject_template: "[%{email_prefix}] E-Mail-Problem -- Antwortadresse unerwartet" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Deine Antwort wurde von einer anderen E-Mail-Adresse versandt als wir erwartet haben, weshalb wir nicht sicher wissen ob das die gleiche Person ist. Probiere eine andere Absende-Adresse oder wende dich an ein Team-Mitglied. email_reject_no_account: title: "E-Mail abgelehnt weil unbekanntes Konto" subject_template: "[%{email_prefix}] E-Mail-Problem -- Konto unbekannt" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Wir konnten kein Konto finden, das zu deiner E-Mail-Adresse passt. Probiere eine andere Absende-Adresse oder wende dich an ein Team-Mitglied. email_reject_empty: title: "E-Mail abgelehnt weil kein Inhalt" subject_template: "[%{email_prefix}] E-Mail-Problem -- Kein Inhalt" @@ -1909,17 +1854,9 @@ de: email_reject_invalid_access: title: "E-Mail abgelehnt weil nicht erlaubt" subject_template: "[%{email_prefix}] E-Mail-Problem -- Zugriff ungültig" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Du hast nicht die notwendigen Zugriffsrechte, um neue Themen in dieser Kategorie zu erstellen. Wenn du glaubst, dass das ein Irrtum ist, dann kontaktiere ein Team-Mitglied. email_reject_strangers_not_allowed: title: "E-Mail abgelehnt weil Zugriff nicht erlaubt" subject_template: "[%{email_prefix}] E-Mail-Problem -- Zugriff ungültig" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Die Kategorie, an die du die E-Mail geschickt hast, erlaubt nur Antworten von Benutzern mit gültigem Konto und von bekannten E-Mail-Adressen. Wenn du glaubst, dass das ein Irrtum ist, dann kontaktiere ein Team-Mitglied. email_reject_invalid_post: title: "E-Mail abgelehnt weil ungültiger Beitrag" subject_template: "[%{email_prefix}] E-Mail-Problem -- Beitrag fehlerhaft" @@ -1948,10 +1885,6 @@ de: email_reject_reply_key: title: "E-Mail abgelehnt weil unbekannter Antwort-Schlüssel" subject_template: "[%{email_prefix}] E-Mail-Problem -- Antwortschlüssel unbekannt" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Der angegebene Antwort-Schlüssel ist ungültig oder unbekannt. Wir wissen daher nicht auf welchen Beitrag diese E-Mail antwortet. Bitte kontaktiere ein Team-Mitglied. email_reject_bad_destination_address: title: "E-Mail abgelehnt weil unbekannte Empfänger-Adresse" subject_template: "[%{email_prefix}] E-Mail-Problem -- Empfängeradresse unbekannt" @@ -1962,24 +1895,12 @@ de: email_reject_topic_not_found: title: "E-Mail abgelehnt weil Thema nicht gefunden" subject_template: "[%{email_prefix}] E-Mail-Problem -- Thema nicht gefunden" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Das Thema, auf das du geantwortet hast, existiert nicht mehr -- vielleicht wurde es gelöscht? Wenn du glaubst, dass dies ein Irrtum ist, nimm Bitte Kontakt mit einem Team-Mitglied auf. email_reject_topic_closed: title: "E-Mail abgelehnt weil Thema geschlossen" subject_template: "[%{email_prefix}] E-Mail-Problem -- Thema geschlossen" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Das Thema, auf das du geantwortet hast, ist derzeit geschlossen und akzeptiert keine Antworten mehr. Wenn du glaubst, dass dies ein Irrtum ist, nimm bitte Kontakt mit einem Team-Mitglied auf. email_reject_auto_generated: title: "E-Mail abgelehnt weil automatisch generierte Antwort" subject_template: "[%{email_prefix}] E-Mail-Problem -- Antwort automatisch generiert" - text_body_template: | - Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - - Deine E-Mail wurde als „automatisch generiert“ markiert, was bedeutet, dass sie automatisch von einem Computer erstellt wurde statt von einem Menschen getippt; wir können diese Arten von E-Mails nicht akzeptieren. Wenn du glaubst, dass dies ein Irrtum ist, nimm bitte Kontakt mit einem Team-Mitglied auf. email_error_notification: title: "Benachrichtigung zu E-Mail-POP-Authentifizierungsfehler" subject_template: "[%{email_prefix}] E-Mail-Problem -- POP-Authentifizierungsfehler" @@ -2101,26 +2022,6 @@ de: visit_link_to_respond: "[Rufe das Thema auf](%{base_url}%{url}), um zu antworten." visit_link_to_respond_pm: "[Rufe die Nachricht auf](%{base_url}%{url}), um zu antworten." posted_by: "Erstellt von %{username} am %{post_date}" - invited_to_private_message_body: | - %{username} hat dich zu einer Unterhaltung eingeladen - - > **%{topic_title}** - > - > %{topic_excerpt} - - auf - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} hat dich zu einer Diskussion eingeladen - - > **%{topic_title}** - > - > %{topic_excerpt} - - auf - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "Benutzer zu Nachricht eingeladen" subject_template: "[%{email_prefix}] %{username} hat dich zu einer Unterhaltung '%{topic_title}' eingeladen" @@ -2369,10 +2270,6 @@ de: see_more: "Mehr" search_title: "Diese Site durchsuchen" search_google: "Google" - login_required: - welcome_message: | - #[Wilkommen bei %{title}](#welcome) - Ein Benutzerkonto ist notwendig. Bitte registriere dich oder melde dich an. terms_of_service: title: "Nutzungsbedingungen" signup_form_message: 'Ich habe die Nutzungsbedingungen gelesen und akzeptiert.' diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index ed8e3e7554..6e649499bd 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -27,11 +27,15 @@ el: topics: "Νήματα" posts: "αναρτήσεις" loading: "Φόρτωση" + powered_by_html: 'Powered by Discourse, best viewed with JavaScript enabled' log_in: "Σύνδεση" purge_reason: "Διεγράφη αυτόματα ως εγκαταλελειμμένος, μη ενεργοποιημένος λογαριασμός." disable_remote_images_download_reason: "Το κατέβασμα εικόνων απενεργοποιήθηκε επειδή δεν υπάρχει αρκετός χώρος στο δίσκο." anonymous: "Ανώνυμα" remove_posts_deleted_by_author: "Διαγράφηκε από τον συγγραφέα" + themes: + bad_color_scheme: "Το θέμα δεν μπόρεσε να ενημερωθεί, σφάλμα χρωματικής παλέτας" + other_error: "Κάτι πήγε στραβά κατά την ενημέρωση του θέματος" emails: incoming: default_subject: "Αυτό το θέμα χρειάζεται τίτλο" @@ -109,6 +113,7 @@ el: s3_upload_bucket_is_required: "Δεν μπορείτε να ενεργοποιήσετε μεταφορτώσεις στο S3 εάν δεν παρέχετε το 's3_upload_bucket'." invite: not_found: "Η πρόσκληση δεν είναι έγγυρη. Παρακαλώ επικοινωνήστε με τον διαχειρηστή της ιστοσελίδας." + user_exists: "Δεν υπάρχει λόγος να προσκαλέσετε τον/την %{email}, έχει ήδη λογαριασμό χρήστη!" bulk_invite: file_should_be_csv: "To ανεβασμένο αρχείο πρέπει να έχει μορφή csv." error: "Παρουσίαστηκε ένα σφάλμα κατά το ανέβασμα του αρχείου σας, Παρακαλώ δοκιμάστε αργότερα." @@ -170,6 +175,7 @@ el: user_is_suspended: "Δεν επιτρέπεται να δημοσιεύσουν οι χρήστες που βρίσκονται σε κατάσταση αναστολής." topic_not_found: "Κάτι πήγε στραβά. Ίσως το νήμα έκλεισε ή διαγράφηκε ενώ το κοιτούσες;" not_accepting_pms: "Λυπούμαστε, ο/η %{username} δεν δέχεται μηνύματα αυτή τη στιγμή." + max_pm_recepients: "Λυπούμαστε, μπορείτε να στείλετε μήνυμα το μέγιστο σε %{recipients_limit} παραλήπτες." just_posted_that: "είναι παρόμοιο με αυτό που δημοσιεύσατε πρόσφατα" invalid_characters: "περιέχει μη έγκυρους χαρακτήρες" is_invalid: "φαίνεται ασαφές, είναι μια ολοκληρωμένη πρόταση;" @@ -208,6 +214,8 @@ el: queue: delete_reason: "Διαγράφτηκαν μέσω της ουράς επανελέγχου " groups: + success: + bulk_add: "%{users_added} χρήστες προστέθηκαν στην ομάδα." errors: can_not_modify_automatic: "Δεν μπορείς να τροποποιήσεις μια αυτόματη ομάδα" member_already_exist: "'%{username}' είναι ήδη μέλος της ομάδας." @@ -215,6 +223,7 @@ el: invalid_incoming_email: "'%{email}' δεν είναι έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου." email_already_used_in_group: "'%{email}' χρησιμοποιείται ήδη από την ομάδα '%{group_name}'" email_already_used_in_category: "'%{email}' χρησιμοποιείται ήδη από την κατηγορία '%{category_name}'." + cant_allow_membership_requests: "Δεν μπορείτε να επιτρέψε τα αιτήματα μέλους σε μια ομάδα χωρίς ιδιοκτήτη." default_names: everyone: "όλοι" admins: "διαχειριστές" @@ -225,6 +234,9 @@ el: trust_level_2: "επίπεδο_εμπιστοσύνης_2" trust_level_3: "επίπεδο_εμπιστοσύνης_3" trust_level_4: "επίπεδο_εμπιστοσύνης_4" + request_membership_pm: + title: "Αίτημα Μέλους για @%{group_name}" + body: "Θα ήθελα να υποβάλλω αίτημα μέλους για την ομάδα @%{group_name}." education: until_posts: one: "1 ανάρτηση" @@ -271,6 +283,14 @@ el: Αυτό το νήμα φαίνεται να είναι σημαντικό για εσάς &ndash. Έχετε δημοσιεύσει περισσότερο από %{percent}% των απαντήσεων εδώ. Είστε σίγουρος ότι παρέχετε επαρκή χρόνο σε άλλους ανθρώπους να μοιραστούν κι εκείνοι τις δικές τους απόψεις; + get_a_room: | + ### Γιατί δεν δοκιμάζετε να απαντήσετε και σε άλλους χρήστες; + + Έχετε ήδη απαντήσει %{count} φορές προς @%{reply_username} στο συγκεκριμένο θέμα. + + Σκεφτήκατε να απαντήσετε και σε άλλους συμμετέχοντες σε αυτή την συζήτηση; Μια σωστή συζήτηση πρέπει να περιλαμβάνει πολλές διαφορετικές φωνές και απόψεις. + + Αν θέλετε να συνεχίσετε την συζήτηση με τον ίδιο χρήστη, θα ήταν ίσως προτιμότερο να [στείλετε ένα προσωπικό μήνυμα] (/u/%{reply_username}). too_many_replies: | ### Έχετε φτάσει το όριο απαντήσεων γι' αυτό το νήμα @@ -320,12 +340,24 @@ el: attributes: hex: invalid: "δεν είναι ένα έγκυρο χρώμα" + post_reply: + base: + different_topic: "Η ανάρτηση και η απάντηση θα πρέπει να ανήκουν στο ίδιο νήμα." web_hook: attributes: payload_url: invalid: "Το URL δεν είναι έγκυρο. To URL πρέπει να συμπεριλαμβάνει http:// ή https://. Και δεν επιτρέπεται κανένα κενό." + custom_emoji: + attributes: + name: + taken: είναι ήδη σε χρήση από κάποιο άλλο emoji + topic_timer: + attributes: + execute_at: + in_the_past: "θα πρέπει να είναι στο μέλον." <<: *errors user_profile: + no_info_me: "το πεδίο Σχετικά Με Μένα στο προφίλ σας είναι κενό, θα θέλατε να προσθέσετε κάποιες πληροφορίες;" no_info_other: "
    %{name} δεν έχει εισάγει τίποτα στο πεδίο Σχετικά με Εμένα στο προφίλ του
    " vip_category_name: "Σαλόνι" vip_category_description: "Κατηγορία αποκλειστικά για μέλη με επίπεδο εμπιστοσύνης (trust level) 3 ή υψηλότερο." @@ -334,6 +366,7 @@ el: staff_category_name: "Προσωπικό" staff_category_description: "Ιδιωτική κατηγορία για συζητήσεις του προσωπικού. Τα θέματα είναι ορατά μόνο σε διαχειριστές και συντονιστές." assets_topic_title: "Στοιχεία/assets για τον σχεδιασμό της ιστοσελίδας" + assets_topic_body: "Αυτό το νήμα, το οποίο είναι ορατό μόνο στο προσωπικό, υπάρχει για να αποθηκεύετε εικόνες και αρχεία τα οποία θα χρησιμοποιήσετε στην σχεδίαση της σελίδας. Μήν το διαγράψετε!\n\n\nΟδηγίες:\n\n\n1. Απαντήστε στο νήμα.\n2. Ανεβάστε στο πεδίο απάντησης όλες τις εικόνες που θέλετε να χρησιμοποιήσετε. (Χρησιμοποιήστε το εικονίδιο μεταφόρτωσης στην μπάρα εργαλείων ή απλά σύρετε ή κάντε επικόλληση τις εικόνες στο πεδίο απάντησης.)\n3. Καταχωρήστε την απάντησή σας για να αναρτηθεί.\n4. Κάντε δεξί κλικ στις εικόνες της νέας σας ανάρτησης για να δείτε από τις πληροφορίες εικόνας την πλήρη διαδρομή προς το αποθηκευμένο αρχείο ή κάντε κλικ στο εικονίδιο επεξεργασίας για να επεξεργαστείτε την ανάρτησή σας και να πάρετε την πλήρη διαδρομή για όλες σας τις εικόνες. Αντιγράψτε την διαδρομή που θέλετε.\n5. Κάντε επικόλληση της διαδρομής στην σελίδα [βασικών ρυθμίσεων](/admin/site_settings/category/required).\n\n\nΑν θέλετε να ενεργοποιήσετε περισσότερους τύπους αρχείων, επεξεργαστείτε την ρύθμιση των `authorized_extensions` στο [file settings](/admin/site_settings/category/files)." discourse_welcome_topic: title: "Καλώς ήρθατε στο Discourse" body: |2+ @@ -385,7 +418,7 @@ el: category: topic_prefix: "Σχετικά με την κατηγορία %{category} " replace_paragraph: "(Αντικατέστησε αυτή την πρώτη παράγραφο με μια σύντομη περιγραφή της νέας σας κατηγορίας. Αυτή η οδηγία θα εμφανιστεί στην περιοχή επιλογής κατηγορίας, επομένως προσπάθησε να είναι κάτω απο 200 χαρακτήρες. **Μέχρι να επεξεργαστείς αυτή την περιγραφή ή να δημιουργήσεις θέματα, αυτή η κατηγορία δεν θα εμφανίζεται στην σελίδα κατηγοριών. **)" - post_template: "%{replace_paragraph}\n\nΧρησιμοποιήστε τις επόμενες παραγράφους για μια μεγαλύτερη περιγραφή, ή για να καθορίσεις οδηγίες και κανόνες για τις κατηγορίες:\n\n- Γιατί μπορεί να χρησιμοποιηθεί αυτή η κατηγορία? Ποιος είναι ο σκοπός της?\n\n- Αυτή η κατηγορία είναι διαφορετική από αυτές που έχουμε ήδη?\n\n- Τι πρέπει να περιέχουν τα θέματα αυτής της κατηγορίας γενικότερα?\n\n- Χρειαζόμαστε αυτή την κατηγορία? Μπορούμε να την ενώσουμε με άλλη κατηγορία, ή υποκατηγορία?\n\n\n" + post_template: "%{replace_paragraph}\n\nΧρησιμοποιήστε τις επόμενες παραγράφους για μια μεγαλύτερη περιγραφή ή για να ορίσεις οδηγίες και κανόνες για τις κατηγορίες:\n\n- Γιατί μπορεί να χρησιμοποιηθεί αυτή η κατηγορία; Ποιος είναι ο σκοπός της;\n\n- Αυτή η κατηγορία είναι διαφορετική από αυτές που έχουμε ήδη;\n\n- Τι πρέπει να περιέχουν τα θέματα αυτής της κατηγορίας γενικότερα;\n\n- Χρειαζόμαστε αυτή την κατηγορία; Μπορούμε να την ενώσουμε με άλλη κατηγορία ή υποκατηγορία;\n\n\n" errors: uncategorized_parent: "Μη κατηγοριοποιημένο δεν μπορεί να έχει κύρια κατηγορία" self_parent: "Η γονική κατηγορία μιας υποκατηγορίας δεν μπορεί να είναι ο εαυτός της" @@ -393,6 +426,7 @@ el: invalid_email_in: "'%{email}' δεν είναι έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου." email_already_used_in_group: "'%{email}' χρησιμοποιείται ήδη από την ομάδα '%{group_name}'" email_already_used_in_category: "%{email}' χρησιμοποιείται ήδη από την κατηγορία '%{caregory_name}'." + description_incomplete: "Η ανάρτηση περιγραφής κατηγορίας θα πρέπει να έχει τουλάχιστον μία παράγραφο." cannot_delete: uncategorized: "Δεν μπορείτε να διαγράψετε την 'Mη κατηγοριοποιημένο'" has_subcategories: "Δεν είναι δυνατή η διαγραφή αυτής της κατηγορίας, επειδή έχει υπο-κατηγορίες." @@ -538,43 +572,58 @@ el: approval_required: "Ένας συντονιστής πρέπει να εγκρίνει το νέο σας λογαριασμό πριν αποκτήσετε πρόσβαση σε αυτό το φόρουμ. Θα λάβετε ένα μήνυμα ηλεκτρονικού ταχυδρομείου όταν ο λογαριασμός σας εγκριθεί!" missing_session: "Δεν μπορούμε να ανιχνεύσουμε αν δημιουργήθηκε ο λογαριασμός σας, παρακαλούμε βεβαιωθείτε ότι έχετε ενεργοποιήσει τα cookies." activated: "Λυπάμαι, αυτός ο λογαριασμός έχει ήδη ενεργοποιηθεί." + admin_confirm: + title: "Επιβεβαιώστε τον Λογαριασμό Διαχειριστή" + description: "Σίγουρα θέλετε να γίνει ο χρήστης %{target_username} διαχειριστής;" + grant: "Παραχώρηση Δικαιωμάτων Διαχειριστή" + complete: "%{target_username} είναι πλέον διαχειριστής." + back_to: "Επιστροφή σε %{title}" post_action_types: off_topic: title: 'Εκτός-Θέματος' description: 'Αυτή η δημοσίευση δεν είναι σχετική με την τρέχουσα συζήτηση όπως ορίζεται από τον τίτλο και την πρώτη δημοσίευση και θα πρέπει μάλλον να μετακινηθεί αλλού.' + short_description: 'Μη σχετικό με την συζήτηση' long_form: 'το επισήμαναν σαν εκτός θέματος' spam: title: 'Ανεπιθύμητα' + description: 'Αυτή η ανάρτηση είναι διαφήμιση ή δολιοφθορά. Δεν είναι σχετική με το παρόν νήμα.' + short_description: 'Αυτή είναι μια διαφήμιση ή δολιοφθορά' long_form: 'το επισήμαναν σαν ανεπιθύμητο' email_title: '"%{title}" επισημάνθηκε ως ανεπιθύμητο' email_body: "%{link}\n\n%{message}" inappropriate: title: 'Ανάρμοστο' description: 'Το περιεχόμενο αυτής της δημοσίευσης θα θεωρούνταν από κάθε λογικό άνθρωπο, προσβλητικό, καταχρηστικό ή αντίθετο με τις οδηγίες χρήσης της κοινότητάς μας' + short_description: 'Παράβαση των Οδηγιών της Κοινότητας' long_form: 'Επισημάνετέ το ως ανάρμοστο' notify_user: title: 'Αποστολή μηνύματος στον @{{username}} ' description: 'Θέλω να μιλήσω με αυτό το άτομο άμεσα και ιδιωτικά σχετικά με αυτή τη δημοσίευση.' + short_description: 'Θα ήθελα να επικονωνήσω με αυτό το μέλος άμεσα και ιδιωτικά, σχετικά με αυτή την ανάρτηαη.' long_form: 'ειδοποιήσε το μέλος' email_title: 'Η δημοσίευση σας στο "%{title}"' email_body: "%{link}\n\n%{message}" notify_moderators: title: "Κάτι άλλο" description: 'Η συγκεκριμένη ανάρτηση απαιτεί έλεγχο από το προσωπικό για έναν άλλον λόγο που δεν αναφέρεται παραπάνω.' + short_description: 'Χρειάζεται έλεγχο από το προσωπικό για διαφορετικό λόγο' long_form: 'αναφέρθηκε για έλεγχο από το προσωπικό.' email_title: 'Μια ανάρτηση στο "%{title}" απαιτεί έλεγχο από το προσωπικό.' email_body: "%{link}\n\n%{message}" bookmark: title: 'Σελιδοδείκτης' description: 'Τοποθετήστε σελιδοδείκτη σε αυτή την ανάρτηση' + short_description: 'Προσθήκη σελιδοδείκτη στην ανάρτηση' long_form: 'τοποθετήσατε σελιδοδείκτη σε αυτή την ανάρτηση' like: title: 'Σας αρέσει' description: 'Σας αρέσει αυτή η δημοσίευση' + short_description: 'Μου αρέσει η ανάρτηση' long_form: 'Μου άρεσε' vote: title: 'Ψηφίστε' description: 'Ψηφίστε για αυτή την δημοσίευση ' + short_description: 'Ψηφίστε την ανάρτηση' long_form: 'ψηφίσατε για αυτή την δημοσίευση' user_activity: no_bookmarks: @@ -595,8 +644,8 @@ el: notify_moderators: title: "Κάτι άλλο" description: 'Το θέμα απαιτεί την γενική προσοχή του προσωπικού, βασισμένη στα κατευθυντήριες γραμμές,TOS ή για άλλο λόγο που δεν αναφέρεται πιο πάνω.' - long_form: 'αναφέρθηκε για έλεγχο από το προσωπικό.' - email_title: 'Το θέμα "%{title}" χρειάζεται έλεγχο από διαμεσολαβητή' + long_form: 'αναφέρθηκε για έλεγχο από συντονιστή.' + email_title: 'Το θέμα "%{title}" χρειάζεται έλεγχο από συντονιστή' email_body: "%{link}\n\n%{message}" flagging: you_must_edit: '

    Η δημοσίευσή σου έχει επισημανθεί από την κοινότητα. Παρακαλώ δες τα μηνύματά σου.

    ' @@ -611,6 +660,7 @@ el: remove: "Αυτό το νήμα δεν είναι πλέον διαφημιστικό. Δε θα εμφανίζεται πια στην κορυφή της κάθε σελίδας." unsubscribed: title: "Επιτυχής απεγγραφή!" + description: "%{email} έχει διαγραφτεί. Για να αλλάξετε τις ρυθμίσεις του email σας επισκεφθείτε την σελίδα ρυθμίσεων." topic_description: "Για να εγγραφείτε ξανά στο %{link}, χρησιμοποιήστε τον έλεγχο ειδοποιήσεων στην κάτω ή στην δεξιά πλευρά του νήματος." unsubscribe: title: "Απεγγραφή" @@ -887,13 +937,12 @@ el: num_users_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη πάρουν num_spam_flags_to_block_new_user σήμανση ανεπιθύμητου από τόσους χρηστες, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποίηση." num_tl3_flags_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη πάρουν τόσες πολλές σημάνσεις από num_tl3_users_to_block_new_user, διαφορετικούς χρήστες επιπέδου εμπιστοσύνης 3, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποίηση." num_tl3_users_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη πάρουν num_tl3_flags_to_block_new_user σημάνσεις από τόσους χρηστες επιπέδου εμπιστοσύνης 3, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποίηση." - notify_mods_when_user_blocked: "Εάν ένας χρήστης αυτόματα μπλοκαριστει, στείλε μήνυμα σε όλους τους συντονιστές." + notify_mods_when_user_blocked: "Εάν ένας χρήστης μπλοκαριστει αυτόματα, στείλε μήνυμα σε όλους τους συντονιστές." flag_sockpuppets: "Εάν ένας νέος χρήστης απαντήσει σε ένα νήμα από την ίδια διεύθυνση ΙP όπως ο νέος χρήστης, ο οποίος ξεκίνησε το νήμα, και οι δυο δημοσιεύσεις τους θα επισημανθούν ως δυνητικά ανεπιθύμητες." traditional_markdown_linebreaks: "Χρήση παραδοσιακών αλλαγών γραμμών στη Markdown, η οποία απαιτεί δύο κενά διαστήματα για μια αλλαγή γραμμής." - allow_html_tables: "Αποδοχή εισδοχής πινάκων στη Markdown με τη χρήση ετικετών HTML. TABLE, THEAD, TD, TR, TH θα μπαίνουν στη λίστα επιτρεπόμενων (απαιτείται πληρής αντιγραφή σε όλες τις αναρτήσεις που περιέχουν πίνακες)" post_undo_action_window_mins: "Αριθμός των λεπτών όπου οι χρήστες δικαιούνται να αναιρέσουν πρόσφατες ενέργειες πάνω σε ένα θέμα (μου αρέσει, επισήμανση, κτλ) " must_approve_users: "Το προσωπικό πρέπει να εγκρίνει όλους τους λογαριασμούς των νέων χρηστών προτού τους επιτραπεί να έχουν πρόσβαση στην ιστοσελίδα. Προειδοποίηση: ενεργοποιώντας το για μια ζωντανή ιστοσελίδα θα έχει ως αποτέλεσμα την ανάκληση για τους υπάρχοντες χρήστες που δεν ανήκουν στο προσωπικό!" - pending_users_reminder_delay: "Ειδοποίηση συντονιστών αν καινούργιοι χρήστες περιμένουν για αποδοχή για μεγαλύτερο απο αυτό το χρονικό διάστημα. Όρισέ το στο -1 για να απενεργοποιηθούν οι ειδοποιήσεις." + pending_users_reminder_delay: "Ειδοποίηση των συντονιστών αν υπάρχουν νέοι χρήστες που περιμένουν για αποδοχή του λογαριασμού τους για μεγαλύτερο απο αυτό το χρονικό διάστημα. Όρισέ το στο -1 για να απενεργοποιηθούν οι ειδοποιήσεις." maximum_session_age: "Ο χρήστης θα παραμείνει συνδεδεμένος για n ώρες από την τελευταία του επίσκεψη" ga_tracking_code: "ΠΑΡΩΧΗΜΕΝΟ: Google analytics (ga.js) tracking code code, eg: UA-12345678-9; βλέπε http://google.com/analytics" ga_domain_name: "ΠΑΡΩΧΗΜΕΝΟ: Google analytics (ga.js) όνομα τομέα , π.χ. eg: mysite.com, βλέπε http://google.com/analytics" @@ -936,55 +985,55 @@ el: invite_expiry_days: "Για πόσο καιρό οι κύριες προσκλήσεις ισχύουν, σε μέρες " invite_passthrough_hours: "Για πόσο καιρό ο χρήστης μπορεί να χρησιμοποιήσει ένα ήδη εξαργυρωμένο κλειδί πρόσκλησης για να συνδεθεί, σε ώρες" login_required: "Απαιτήστε την επικύρωση για να διαβάσετε το περιεχόμενο σε αυτήν την ιστοσελίδα, απαγορεύσετε την ανώνυμη πρόσβαση." - min_username_length: "Ελάχιστο μέγεθος όνομα χρήστη σε χαρακτήρες. ΠΡΟΣΟΧΗ: εάν ο όποιος δήποτε από τους χρήστες που είδη υπάρχουν ή από της ομάδες έχουν ονόματα πιο μικρά από αυτό, η ιστοσελίδα σου θα διακοπεί!" - max_username_length: "Μέγιστο μέγεθος όνομα χρήστη με χαρακτήρες. ΠΡΟΣΟΧΗ:\nαν υπάρχοντες χρήστες ή ομάδες έχουν ονόματα περισσότερο από αυτό, η ιστοσελίδα σας θα παραβιαστεί!" + min_username_length: "Ελάχιστο μέγεθος ονόματος χρήστη σε χαρακτήρες. ΠΡΟΣΟΧΗ: εάν υπάρχουν χρήστες ή ομάδες που έχουν ονόματα πιο μικρά από την ρύθμιση αυτή, θα δημιουργηθεί πρόβλημα στην λειτουργία της ιστοσελίδας!" + max_username_length: "Μέγιστο μέγεθος ονόματος χρήστη σε χαρακτήρες. ΠΡΟΣΟΧΗ: εάν υπάρχουν χρήστες ή ομάδες που έχουν ονόματα μεγαλύτερα από την ρύθμιση αυτή, θα δημιουργηθεί πρόβλημα στην λειτουργία της ιστοσελίδας!" min_password_length: "Ελάχιστο μήκος κωδικού πρόσβασης." min_admin_password_length: "Ελάχιστο μήκος κωδικού πρόσβασης για το διαχειριστή" password_unique_characters: "Ελάχιστος αριθμός μοναδικών χαρακτήρων που πρέπει να έχει ένας κωδικός." block_common_passwords: "Μην επιτρέπεις τους κωδικούς πρόσβασης που είναι ανάμεσα στους 10,000 πιο κοινούς κωδικούς πρόσβασης." enable_sso: "Ενεργοποίηση καθολικής σύνδεσης μέσω ενός εξωτερικού συνδέσμου (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: ΟΙ ΔΙΕΥΘΥΝΣΕΙΣ ΗΛΕΚΤΡΟΝΙΚΟΥ ΤΑΧΥΔΡΟΜΕΙΟΥ ΤΩΝ ΧΡΗΣΤΩΝ ΘΑ ΠΡΕΠΕΙ ΝΑ ΕΠΙΚΥΡΩΘΟΥΝ ΑΠΟ ΤΟΝ ΕΞΩΤΕΡΙΚΟ ΣΥΝΔΕΣΜΟ!)" - verbose_sso_logging: "SSO λεπτομερούς καταγραφής διαγνωστικών σχετίζονται με /logs" + verbose_sso_logging: "Κατέγραψε αναλυτικά διαγνωστικά σχετικά με SSO στο /logs" enable_sso_provider: "Εφαρμογή πρωτοκόλλου παροχέα Discourse SSO στο τελικό σημείο του /session/sso_provider, απαιτεί να τεθει το sso_secret." - sso_url: "H διεύθυνση URL της ενιαίας σύνδεσης στο τέλος ( πρέπει να συμπεριλαμβάνει http:// or https://)" + sso_url: "URL του single sign on endpoint (πρέπει να περιέχει http:// or https://)" sso_secret: "Η μυστική συμβολοσειρά που χρησιμοποιείται για να πιστοποιήσει κρυπτογραφικά πληροφορίες SSO, σιγουρευτείτε οτι έχει 10 χαρακτήρες ή περισσότερους." - sso_overrides_bio: "Παρακάμπτει την βιογραφία του χρήστη στο προφίλ του χρήστη και τον αποτρέπει απ'το να την αλλάξει. " + sso_overrides_bio: "Παρακάμπτει την βιογραφία του χρήστη στο προφίλ του χρήστη και τον αποτρέπει να την αλλάξει. " sso_overrides_email: "Παράκαμψη τοπικής διεύθυνσης ηλεκτρονικού ταχυδρομείου με τη διεύθυνση ηλεκτρονικού ταχυδρομείου από τον εξωτερικό ιστότοπο με φορτίο SSO σε κάθε σύνδεση, και αποτροπή τοπικών αλλαγών. (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: μπορεί να προκληθούν ασυμφωνίες λόγω της ομαλοποίησης των τοπικών διευθύνσεων ηλεκτρονικού ταχυδρομείου)" sso_overrides_username: "Παράκαμψη τοπικού ονόματος χρήστη με το όνομα χρήστη από τον εξωτερικό ιστότοπο SSO σε κάθε σύνδεση και αποτροπή τοπικών αλλαγών. (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: μπορεί να προκληθούν ασυμφωνίες λόγω των διαφορών μεταξύ των ονομάτων χρηστων όσο αναφορά το μάρκος/απαιτήσεις)" sso_overrides_name: "Παράκαμψη τοπικού ονόματεπώνυμου με το όνομα χρήστη από τον εξωτερικό ιστότοπο SSO σε κάθε σύνδεση και αποτροπή τοπικών αλλαγών. " sso_overrides_avatar: "Παράκαμψη τοπικού άβαταρ χρήστη με το άβαταρ από τον εξωτερικό ιστότοπο SSO Εάν ενεργοποιημένο, η αποσύνδεση του allow_uploaded_avatars συνιστάται." sso_not_approved_url: "Ανακατευθύνετε μη εγκεκριμένους λογαριασμούς SSO σε αυτό το URL" sso_allows_all_return_paths: "Μην αναχαιτίσεις το κυρίαρχο return_paths που παρέχεται από το SSO (από προεπιλογή η διαδρομή γυρισμόυ πρέπει να είναι στην τρεχούμενη ιστοσελίδα)" - enable_local_logins: "Ενεργοποίηση τοπικών λογαριασμών σύνδεσης με βάση το όνομα χρήστη και τον κωδικό πρόσβασης. (Σημείωση: αυτό πρέπει να είναι ενεργοποιημένο για καλέσματα στην εργασία)" - allow_new_registrations: "Επιτρέψτε τις νέες εγγραφές χρηστών.Καταργήστε την επιλογή αυτή για να αποτρέψετε σε οποιονδήποτε να δημιουργήσει ένα νέο λογαριασμό" + enable_local_logins: "Ενεργοποίηση τοπικών λογαριασμών χρήστη με βάση το χρηστώνυμο και τον κωδικό πρόσβασης. (Σημείωση: αυτό πρέπει να είναι ενεργοποιημένο για να λειτουργούν οι προσκλήσεις)" + allow_new_registrations: "Επιτρέψτε τις νέες εγγραφές χρηστών. Απενεργοποιήστε την επιλογή αυτή για να αποτρέψετε σε οποιονδήποτε να δημιουργήσει ένα νέο λογαριασμό" enable_signup_cta: "Δείξε μια ανακοίνωση στους ανώνυμους χρήστες που επιστρέφουν, η οποία να τους παροτρύνει να δημιουργήσουν λογαριασμό." enable_yahoo_logins: "Ενεργοποιήστε την επαλήθευση μέσω Yahoo" - enable_google_oauth2_logins: "Ενεργοποίησε την επαλήθευση μέσω Google Oauth2. Είναι μια μέθοδος επαλήθευσης που η Google υποστηρίζει προς το παρόν. Απαιτεί κλειδί και μυστικό." - google_oauth2_client_id: "Ταυτότητα πελάτη της εφαρμογής σας Google." - google_oauth2_client_secret: "Μυστικό πελάτη του λογαριασμού σας Google. " - enable_twitter_logins: "Επίτρεψε επαλήθευση μέσω Twitter, απαιτεί twitter_κλειδί_καταναλωτή και twitter_μυστικό_καταναλωτή" - twitter_consumer_key: "Κλειδί καταναλωτή για επαλήθευση μέσω Twitter, εγγεγραμμένο στο http://dev.twitter.com" - twitter_consumer_secret: "Μυστικό καταναλωτή για επαλήθευση μέσω Twitter, εγγεγραμμένο στο http://dev.twitter.com" - enable_instagram_logins: "Επίτρεψε επαλήθευση μέσω Instagram, απαιτεί Instagram_κλειδί_καταναλωτή και Instagram_μυστικό_καταναλωτή" - instagram_consumer_key: "Κλειδί καταναλωτή για επαλήθευση μέσω Instagram" - instagram_consumer_secret: "Μυστικό καταναλωτή για επαλήθευση μέσω Instagram" - enable_facebook_logins: "Επίτρεψε επαλήθευση μέσω Facebook, απαιτεί Facebook_ταυτότητα_εφαρμογής και Facebook_μυστικό_εφαρμογής" - facebook_app_id: "Ταυτότητα εφαρμογής για επαλήθευση μέσω Facebook, εγγεγραμμένο στο https://developers.facebook.com/apps" - facebook_app_secret: "Μυστικό εφαρμογής για επαλήθευση μέσω Facebook, εγγεγραμμένο στο https://developers.facebook.com/apps" - facebook_request_extra_profile_details: "Ζήτησε σχετικά με μένα, τοποθεσία και ιστοσελίδα του facebook. (απαιτείται έγκριση της αίτησης επαλήθευσής σου από το facebook)" - enable_github_logins: "Επίτρεψε επαλήθευση μέσω Github, απαιτεί Github_ταυτότητα_εφαρμογής και Github_μυστικό_εφαρμογής" - github_client_id: "Ταυτότητα καταναλωτή για επαλήθευση μέσω Github, εγγεγραμμένο στο https://github.com/settings/applications" - github_client_secret: "Μυστικό καταναλωτή για επαλήθευση μέσω Github, εγγεγραμμένο στο https://github.com/settings/applications" + enable_google_oauth2_logins: "Ενεργοποίησε την επαλήθευση μέσω Google Oauth2. Είναι η μέθοδος επαλήθευσης που η Google υποστηρίζει προς το παρόν. Απαιτεί κλειδί και μυστικό." + google_oauth2_client_id: "Client ID της Google εφαρμογής σας." + google_oauth2_client_secret: "Μυστικό πελάτη της Google εφαρμογής σας. " + enable_twitter_logins: "Ενεργοποίησε την επαλήθευση μέσω Twitter. Απαιτεί twitter_consumer_key και twitter_consumer_secret" + twitter_consumer_key: "Consumer key για επαλήθευση μέσω Twitter, εγγεγραμμένο στο http://dev.twitter.com" + twitter_consumer_secret: "Consumer secret για επαλήθευση μέσω Twitter, εγγεγραμμένο στο http://dev.twitter.com" + enable_instagram_logins: "Ενεργοποίησε την επαλήθευση μέσω Instagram, απαιτεί instagram_consumer_key και instagram_consumer_secret" + instagram_consumer_key: "Consumer key για επαλήθευση μέσω Instagram" + instagram_consumer_secret: "Consumer secret για επαλήθευση μέσω Instagram" + enable_facebook_logins: "Ενεργοποίησε την επαλήθευση μέσω Facebook, απαιτεί facebook_app_id και facebook_app_secret" + facebook_app_id: "App id για επαλήθευση μέσω Facebook, εγγεγραμμένο στο https://developers.facebook.com/apps" + facebook_app_secret: "App secret για επαλήθευση μέσω Facebook, εγγεγραμμένο στο https://developers.facebook.com/apps" + facebook_request_extra_profile_details: "Ζήτησε σχετικά με μένα, τοποθεσία και ιστοσελίδα από το facebook. (απαιτείται έγκριση της εφαρμογής σου από το facebook)" + enable_github_logins: "Ενεργοποίησε την επαλήθευση μέσω Github, απαιτεί github_client_id και github_client_secret" + github_client_id: "Client id για επαλήθευση μέσω Github, εγγεγραμμένο στο https://github.com/settings/applications" + github_client_secret: "Client secret για επαλήθευση μέσω Github, εγγεγραμμένο στο https://github.com/settings/applications" readonly_mode_during_backup: "Ενεργοποίηση της λειτουργίας \"μόνο-ανάγνωση\" κατά τη διάρκεια δημιουργίας αντίγραφου ασφαλείας" allow_restore: "Επίτρεψε επαναφορά, η οποία μπορεί να αντικαταστήσει ΟΛΑ τα δεδομένα του ιστότοπου! Αφήστε σφάλμα εκτός αν σχεδιάζεις να επαναφέρεις αντίγραφο ασφαλείας" - maximum_backups: "Το μέγιστο ποσό αντιγράφω ασφαλείας για να διατηρηθούν στο δίσκο. Παλαιότερα αντίγραφα ασφαλείας διαγράφονται αυτόματα" - automatic_backups_enabled: "Τρέξε αυτόματα αντίγραφα ασφαλείας όπως ορίζεται στη συχνότητα αντιγράφων ασφαλείας" + maximum_backups: "Το μέγιστο ποσό αντιγράφω ασφαλείας το οποία θα διατηρηθούν στο δίσκο. Παλαιότερα αντίγραφα ασφαλείας διαγράφονται αυτόματα" + automatic_backups_enabled: "Πάρε αυτόματα αντίγραφα ασφαλείας όπως ορίζεται στη συχνότητα αντιγράφων ασφαλείας" backup_frequency: "Πόσο συχνά δημιουργούμε αντίγραφο ασφαλείας ιστοτόπου, σε ημέρες." - enable_s3_backups: "Ανέβασε αντίγραφα αφαλείας στο S3 όταν ολοκληρωθεί. ΣΗΜΑΝΤΙΚΟ: απαιτεί έγκυρα S3 πιστοποιητικά στις Ρυθμίσεις αρχείων." - s3_backup_bucket: "Απομακρυσμένος κουβάς για αντίγραφα ασφαλείας. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Βεβαιωθείτε οτι είναι απόρρητος κουβάς." - s3_disable_cleanup: "Απενεργοποιήστε την αφαίρεση αντιγράφων ασφαλείας από την S3 όταν αφαιρεθεί σε τοπικό επίπεδο." - backup_time_of_day: "Ώρα της ημέρας σε ζώνη UTC που το αντίγραφο ασφαλείας πρέπει να πραγματοποιηθεί." - backup_with_uploads: "Συμπεριέλαβε μεταφορτώσεις σε προγραμματισμένα αντίγραφα ασφαλείας. Απενεργοποιώντας το θα δημιουργηθούν αντίγραφα ασφαλείας μόνο για τη βάση δεδομένων." - active_user_rate_limit_secs: "Πόσο συχνά ενημερώνουμε το 'τελευταία_εθεάθη_σε' πεδίο, σε δευτερόλεπτα" + enable_s3_backups: "Ανέβασε τα αντίγραφα αφαλείας στο S3 όταν ολοκληρωθούν. ΣΗΜΑΝΤΙΚΟ: απαιτεί έγκυρα διαπιστευτήρια S3 καταχωρημένα στις ρυθμίσεις αρχείων." + s3_backup_bucket: "Απομακρυσμένος αποθηκευτικός χώρος για τα αντίγραφα ασφαλείας. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Βεβαιωθείτε οτι ο χώρος είναι προσωπικός." + s3_disable_cleanup: "Απενεργοποιήστε την αφαίρεση των αντιγράφων ασφαλείας από το S3 όταν αφαιρεθούν τοπικά" + backup_time_of_day: "Ώρα σε ζώνη UTC που το αντίγραφο ασφαλείας πρέπει να πραγματοποιηθεί." + backup_with_uploads: "Συμπεριέλαβε τις μεταφορτώσεις στα αντίγραφα ασφαλείας. Απενεργοποιώντας το θα δημιουργηθούν αντίγραφα ασφαλείας μόνο για τη βάση δεδομένων." + active_user_rate_limit_secs: "Πόσο συχνά ενημερώνουμε το 'last_seen_at' πεδίο, σε δευτερόλεπτα" verbose_localization: "Εμφάνιζε εκτεταμένες συμβουλές εντοπισμού στην διεπαφή χρήστη. " previous_visit_timeout_hours: "Πόσο διαρκεί μια επίσκεψη προτού τη θεωρήσουμε \"προηγούμενη\" επίσκεψη, σε ώρες" top_topics_formula_log_views_multiplier: "Ο πολλαπλασιαστής (n) της αξίας προβολών καταγραφής μπαίνει στην αρχή του νήματος της φόρμουλας: `log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" @@ -1451,42 +1500,40 @@ el: title: "Προσκάλεσε τον αποστολέα" subject_template: "%{invitee_name} σε προσκάλεσε σε '%{topic_title}' πάνω σε %{site_domain_name}" text_body_template: | - %{invitee_name} σας προσκάλεσε σε μία συζήτηση + Ο/Η %{invitee_name} σας προσκάλεσε σε μία συζήτηση - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} - στο + στην > %{site_title} -- %{site_description} - Εάν σε ενδιαφέρει, κάνε κλικ στο σύνδεσμο παρακάτω: + Αν σας ενδιαφέρει, κάντε κλικ στον παρακάτω σύνδεσμο: %{invite_link} custom_invite_mailer: title: "Προσαρμοσμένη Πρόσκληση Ταχυδρομείου" subject_template: "%{invitee_name} σε προσκάλεσε στο %{topic_title}' του %{site_domain_name}" text_body_template: | - %{invitee_name} σας προσκάλεσε σε μία συζήτηση + Ο/Η %{invitee_name} σας προσκάλεσε σε μία συζήτηση - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} - στο + στην > %{site_title} -- %{site_description} - Με αυτή τη σημείωση + με αυτό το μήνυμα > %{user_custom_message} - Εάν σε ενδιαφέρει, κάνε κλικ στο σύνδεσμο παρακάτω: + Αν σας ενδιαφέρει, κάντε κλικ στον παρακάτω σύνδεσμο: %{invite_link} - - Αυτή η πρόσκληση προέρχεται από έμπιστο χρήστη, επομένως ένας λογαριασμός θα δημιουργηθεί αυτόματα για εσένα χρησιμοποιώντας αυτή τη διεύθυνση ηλεκτρονικού ταχυδρομείου. invite_forum_mailer: title: "Προσκαλέστε τον Μέιλερ του Φόρουμ" subject_template: "%{invitee_name} σε προσκάλεσε να συμμετέχεις στο %{site_domain_name}" @@ -1502,6 +1549,7 @@ el: %{invite_link} custom_invite_forum_mailer: + title: "Προσαρμογή Πρόσκλησης Forum " subject_template: "%{invitee_name} σε προσκάλεσε να συμμετέχεις στο %{site_domailn_name}" text_body_template: | %{invitee_name} σας προσκάλεσε να γίνετε μέλος @@ -1522,6 +1570,8 @@ el: invite_password_instructions: title: "Προσκάλεσε οδηγίες κωδικού" subject_template: "Όρισε κωδικό πρόσβασης για τον λογαρισμό σου %{site_name}" + admin_confirmation_mailer: + title: "Επικύρωση Διαχειριστή" test_mailer: title: "Δοκιμάστε τον αποστολέα" text_body_template: | @@ -1580,6 +1630,7 @@ el: - Επισκέψου [meta.discourse.org](https://meta.discourse.org) για νέα, συζήτηση, και υποστήριξη για το Discourse new_version_mailer_with_notes: title: "Καινούργια έκδοση Αποστολέα με Σημειώσεις" + subject_template: "[%{email_prefix}] διαθέσιμη ενημέρωση" text_body_template: | Γιούπιιι, μια νέα έκδοση του [Discourse](http://www.discourse.org) είανι διαθέσιμη! @@ -1605,7 +1656,7 @@ el: off_topic: "Η ανάρτησή σου επισημάθηκε ως **εκτός θέματος**: η κοινότητα πιστεύει ότι δεν ταιριάζει με το νήμα, όπως ορίζεται από τον τίτλο και την πρώτη ανάρτηση αυτή τη στιγμή. " inappropriate: "Η δημοσίευσή σου επισημάνθηκε ως \"ακατάλληλη\": η κοινότητα θεωρεί ότι είναι προσβλητική, υβριστική ή παραβίαση των [οδηγιών της κοινότητας](/οδηγίες)." spam: "Η δημοσίευσή σου επισημάνθηκε ως \"ανεπιθύμητη\": η κοινότητα θεωρεί ότι πρόκειται για διαφήμιση ή κάτι που αφορά προώθηση και δεν είναι χρήσιμη ή σχετική με το νήμα όπως αναμενόταν." - notify_moderators: "Η ανάρτησή σου επισημάνθηκε ως **προς την προσοχή του συντονιστή**: η κοινότητα πιστεύει ότι κάτι σχετικά με την ανάρτηση απαιτεί χειροκίνητη παρέμβαση από ένα συνεργάτη." + notify_moderators: "Η ανάρτησή σου επισημάνθηκε **προς έλεγχο από συντονιστή**. Η κοινότητα πιστεύει ότι κάτι σχετικά με την ανάρτησή σου, απαιτεί την παρέμβαση ενός συνεργάτη." flags_dispositions: agreed: "Ευχαριστούμε που μας ενημερώσατε. Συμφωνούμε ότι υπάρχει κάποιο θέμα και το εξετάζουμε." agreed_and_deleted: "Ευχαριστούμε που μας ενημερώσατε. Συμφωνούμε ότι υπάρχει κάποιο θέμα και έχουμε απομακρύνει τη δημοσίευση." @@ -1613,6 +1664,8 @@ el: deferred: "Ευχαριστούμε που μας ειδοποιήσατε. Το εξετάζουμε." deferred_and_deleted: "Ευχαριστούμε που μας ειδοποίησες. Έχουμε αφαιρέσει τη δημοσίευση." system_messages: + private_topic_title: "Νήμα #%{id}" + contents_hidden: "Παρακαλώ επισκεφθείτε αυτή την ανάρτηση για να δείτε τα περιεχόμενά της." post_hidden: title: "Η δημοσίευση κρύφτηκε" subject_template: "Η δημοσίευση κρύφτηκε από τις επισημάνσεις της κοινότητας" @@ -1918,6 +1971,15 @@ el: Μερικά προβλήματα αναφέρονται στον πίνακα του διαχειριστή σας. [Παρακαλώ επανεξετάστετα διορθώστετα](%{base_url}/admin). + new_user_of_the_month: + title: "Έχεις επιλεχθεί ως Χρήστης του Μήνα!" + subject_template: "Έχεις επιλεχθεί ως Χρήστης του Μήνα!" + text_body_template: | + Συγχαρητήρια, κέρδισες το βραβείο **Χρήστης του Μήνα για %{month_year}**. :trophy: + + Αυτό το βραβείο επονέμεται σε δυο νέους χρήστες κάθε μήνα και θα είναι για πάντα ορατό στην [σελίδα χρήστη](%{base_url}/my/badges). + + Γίνατε γρήγορα ένα πολύτιμο μέλος της κοινότητάς μας. Σας ευχαριστούμε για την συμμετοχή σας και ελπίζουμε να συνεχίσετε έτσι και στο μέλλον! unsubscribe_link: |+ Για να απεγγραφείς από αυτά τα ηλεκτρονικά μηνύματα, [click here](%{unsubscribe_url}). @@ -1942,28 +2004,47 @@ el: visit_link_to_respond: "[Visit Topic](%{base_url}%{url}) για να αποκριθείς." visit_link_to_respond_pm: "[Visit Message](%{base_url}%{url}) για να αποκριθείς." posted_by: "Δημοσιεύθηκε από %{username} στις %{post_date}" - invited_to_private_message_body: | - %{username} σας προσκάλεσε σε ένα μήνυμα + invited_group_to_private_message_body: | + Ο/Η %{username} προσκλήθηκε @%{group_name} σε ένα μήνυμα - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} - στο + στην + + > %{site_title} -- %{site_description} + invited_to_private_message_body: | + Ο/Η %{username} σας προσκάλεσε σε ένα μήνυμα + + > #### %{topic_title} + > + > %{topic_excerpt} + + στην > %{site_title} -- %{site_description} invited_to_topic_body: | - %{username} σας προσκάλεσε σε μία συζήτηση + Ο/Η %{username} σας προσκάλεσε σε μια συζήτηση - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} - στο + στην > %{site_title} -- %{site_description} + user_invited_to_private_message_pm_group: + subject_template: "[%{email_prefix}] %{username} προσκλήθηκε @%{group_name} σε ένα μήνυμα '%{topic_title}'" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} user_invited_to_private_message_pm: title: "Ο χρήστης προσκλήθηκε σε προσωπικό μήνυμα" + subject_template: "[%{email_prefix}] %{username} σας προσκάλεσε σε ένα μήνυμα '%{topic_title}'" text_body_template: | %{header_instructions} @@ -1972,6 +2053,7 @@ el: %{respond_instructions} user_invited_to_private_message_pm_staged: title: "Ο χρήστης προσκλήθηκε στο στάδιο προσωπικού μηνύματος" + subject_template: "[%{email_prefix}] %{username} σας προσκάλεσε σε ένα μήνυμα '%{topic_title}'" text_body_template: |+ %{header_instructions} @@ -1981,6 +2063,7 @@ el: user_invited_to_topic: title: "Ο χρήστης προσκλήθηκε στο νήμα" + subject_template: "[%{email_prefix}] %{username} σας προσκάλεσε στο '%{topic_title}'" text_body_template: | %{header_instructions} @@ -1989,6 +2072,7 @@ el: %{respond_instructions} user_replied: title: "Ο χρήστης απάντησε" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -1999,6 +2083,7 @@ el: %{respond_instructions} user_replied_pm: title: "Ο χρήστης απάντησε σε προσωπικό μηνύμα" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: | %{header_instructions} @@ -2009,6 +2094,7 @@ el: %{respond_instructions} user_quoted: title: "Φράση χρήστη" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -2019,6 +2105,7 @@ el: %{respond_instructions} user_linked: title: "Ο χρήστης συνδέθηκε" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -2029,6 +2116,7 @@ el: %{respond_instructions} user_mentioned: title: "Ο χρήστης αναφέρθηκε" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -2039,6 +2127,7 @@ el: %{respond_instructions} user_group_mentioned: title: "Αναφέρθηκε χρήστης ομάδας" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | &{header_instructions} @@ -2049,6 +2138,7 @@ el: %{respond_instructions} user_posted: title: "Ο χρήστης δημοσίευσε" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -2059,6 +2149,7 @@ el: %{respond_instructions} user_watching_first_post: title: "Ο χρήστης παρακολουθεί την πρώτη δημοσίευση" + subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: |+ %{header_instructions} @@ -2070,6 +2161,7 @@ el: user_posted_pm: title: "Ο χρήστης δημοσίευσε προσωπικό μήνυμα" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: |+ %{header_instructions} @@ -2099,11 +2191,14 @@ el: join_the_discussion: "Διάβασε περισσότερα" popular_posts: "Δημοφιλείς Δημοσιεύσεις" more_new: "Νέο για εσένα" + subject_template: "[%{email_prefix}] Σύνοψη" + unsubscribe: "Λάβατε αυτή την σύνοψη από την %{site_link} επειδή δεν σας είδαμε εδώ και μερικές μέρες. Αν δεν θέλετε να λαμβάνετε αυτή την σύνοψη, αλλάξτε τις ρυθμίσεις σας ή επιλέξτε εδώ %{unsubscribe_link} για διαγραφή από την λίστα." click_here: "κάνε κλικ εδώ" from: "%{site_name} περίληψη" preheader: "Μία σύντομη περίληψη από την τελευταία σου επίσκεψη στο %{last_seen_at}" forgot_password: title: "Ξέχασα τον κωδικό πρόσβασης" + subject_template: "[%{email_prefix}] Επαναφορά κωδικού" set_password: title: "Όρισε κωδικό πρόσβασης" admin_login: @@ -2675,6 +2770,11 @@ el: name: Πρώτη απάντηση μέσω ηλεκτρονικού ταχυδρομείου long_description: | Το παράσημο χορηγείται την πρώτη φορά που απαντάς σε μια δημοσίευση μέσω ηλεκτρονικού ταχυδρομείου :ηλεκτρονική διεύθυνση:. + new_user_of_the_month: + name: "Νέος Χρήστης του Μήνα" + description: Εξαιρετική συνεισφορά κατά τον πρώτο μήνα + long_description: | + Αυτό το βραβείο απονέμεται σε δύο νέους χρήστες κάθε μήνα για την εξαιρετική συνεισφορά τους στην κοινότητα, όπως αυτή μετρήθηκε από το πόσο σύχνα άρεσαν οι αναρτήσεις τους και σε ποιους. admin_login: success: "Το Email απεστάλη" error: "Σφάλμα!" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ca5914b74e..4d2fec269f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -208,6 +208,7 @@ en: too_many_links: one: "Sorry, new users can only put one link in a post." other: "Sorry, new users can only put %{count} links in a post." + contains_blocked_words: "Your post contains words that aren't allowed." spamming_host: "Sorry you cannot post a link to that host." user_is_suspended: "Suspended users are not allowed to post." @@ -413,8 +414,11 @@ en: translation_overrides: attributes: value: - invalid_interpolation_keys: 'The following interpolation key(s) are invalid: "%{keys}"' missing_interpolation_keys: 'The following interpolation key(s) are missing: "%{keys}"' + watched_word: + attributes: + word: + too_many: "Too many words for that action" user_profile: no_info_me: "
    the About Me field of your profile is currently blank, would you like to fill it out?
    " @@ -486,6 +490,7 @@ en: invalid_email_in: "'%{email}' is not a valid email address." email_already_used_in_group: "'%{email}' is already used by the group '%{group_name}'." email_already_used_in_category: "'%{email}' is already used by the category '%{category_name}'." + description_incomplete: "The category description post must have at least one paragraph." cannot_delete: uncategorized: "Can't delete Uncategorized" has_subcategories: "Can't delete this category because it has sub-categories." @@ -948,13 +953,15 @@ en: search_tokenize_chinese_japanese_korean: "Force search to tokenize Chinese/Japanese/Korean even on non CJK sites" search_prefer_recent_posts: "If searching your large forum is slow, this option tries an index of more recent posts first" search_recent_posts_size: "How many recent posts to keep in the index" + log_search_queries: "Log search queries performed by users" + search_query_log_max_size: "Maximum amount of search queries to keep" allow_uncategorized_topics: "Allow topics to be created without a category. WARNING: If there are any uncategorized topics, you must recategorize them before turning this off." allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles." unique_posts_mins: "How many minutes before a user can make a post with the same content again" educate_until_posts: "When the user starts typing their first (n) new posts, show the pop-up new user education panel in the composer." title: "The name of this site, as used in the title tag." site_description: "Describe this site in one sentence, as used in the meta description tag." - contact_email: "Email address of key contact responsible for this site. Used for critical notifications such as unhandled flags, as well as on the /about contact form for urgent matters." + contact_email: "Email address of key contact responsible for this site. Used for critical notifications, as well as on the /about contact form for urgent matters." contact_url: "Contact URL for this site. Used on the /about contact form for urgent matters." queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken." crawl_images: "Retrieve images from remote URLs to insert the correct width and height dimensions." @@ -978,6 +985,7 @@ en: show_pinned_excerpt_desktop: "Show excerpt on pinned topics in desktop view." post_onebox_maxlength: "Maximum length of a oneboxed Discourse post in characters." onebox_domains_blacklist: "A list of domains that will never be oneboxed." + inline_onebox_domains_whitelist: "A list of domains that will be oneboxed in miniature form if linked without a title" max_oneboxes_per_post: "Maximum number of oneboxes in a post." logo_url: "The logo image at the top left of your site, should be a wide rectangle shape. If left blank site title text will be shown." @@ -1025,9 +1033,7 @@ en: flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." - enable_experimental_markdown_it: "Enable the experimental markdown.it CommonMark engine, WARNING: some plugins may not work correctly" enable_markdown_typographer: "Use basic typography rules to improve text readability of paragraphs of text, replaces (c) (tm) etc, with symbols, reduces number of question marks and so on" - allow_html_tables: "Allow tables to be entered in Markdown using HTML tags. TABLE, THEAD, TD, TR, TH will be whitelisted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." 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!" pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications." @@ -1036,6 +1042,7 @@ en: ga_domain_name: "OBSOLETE: Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; see http://google.com/analytics" + ga_universal_auto_link_domains: "Enable Google Universal Analytics (analytics.js) cross-domain tracking. Outgoing links to these domains will have the client id added to them. See Google's Cross-Domain Tracking guide." gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF" enable_escaped_fragments: "Fall back to Google's Ajax-Crawling API if no webcrawler is detected. See https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Allow moderators to create new categories" @@ -1205,6 +1212,8 @@ en: allow_staff_to_upload_any_file_in_pm: "Allow staff members to upload any files in PM." + strip_image_metadata: "Strip image metadata." + enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks." default_invitee_trust_level: "Default trust level (0-4) for invited users." @@ -1380,7 +1389,7 @@ en: email_prefix: "The [label] used in the subject of emails. It will default to 'title' if not set." email_site_title: "The title of the site used as the sender of emails from the site. Default to 'title' if not set. If your 'title' contains characters that are not allowed in email sender strings, use this setting." - find_related_post_with_key: "Only use the reply key to find the replied-to post. (Recommended if using Amazon SES)" + find_related_post_with_key: "Only use the 'reply key' to find the replied-to post. WARNING: disabling this allows user impersonation based on email address." minimum_topics_similar: "How many topics need to exist before similar topics are presented when composing new topics." @@ -1765,7 +1774,7 @@ en: text_body_template: | %{invitee_name} invited you to a discussion - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} @@ -1783,7 +1792,7 @@ en: text_body_template: | %{invitee_name} invited you to a discussion - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} @@ -2108,7 +2117,7 @@ en: csv_export_failed: title: "CSV Export Failed" subject_template: "Data export failed" - text_body_template: "We're sorry, but your data export failed. Please check the logs or contact a staff member." + text_body_template: "We're sorry, but your data export failed. Please check the logs or [contact a staff member](%{base_url}/about)." email_reject_insufficient_trust_level: title: "Email Reject insufficient Trust Level" @@ -2116,7 +2125,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your account does not have the required trust level to post new topics to this email address. If you believe this is an error, contact a staff member. + Your account does not have the required trust level to post new topics to this email address. If you believe this is an error, [contact a staff member](%{base_url}/about). email_reject_user_not_found: title: "Email Reject User Not Found" @@ -2124,7 +2133,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your reply was sent from an unknown email address. Try sending from another email address, or contact a staff member. + Your reply was sent from an unknown email address. Try sending from another email address, or [contact a staff member](%{base_url}/about). email_reject_screened_email: title: "Email Reject Screened Email" @@ -2132,7 +2141,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your reply was sent from a blocked email address. Try sending from another email address, or contact a staff member. + Your reply was sent from a blocked email address. Try sending from another email address, or [contact a staff member](%{base_url}/about). email_reject_inactive_user: title: "Email Reject Inactive User" @@ -2156,7 +2165,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your reply was sent from a different email address than the one we expected, so we're not sure if this is the same person. Try sending from another email address, or contact a staff member. + Your reply was sent from a different email address than the one we expected, so we're not sure if this is the same person. Try sending from another email address, or [contact a staff member](%{base_url}/about). email_reject_no_account: title: "Email Reject No Account" @@ -2164,7 +2173,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - We can't find any accounts that match your email address. Try sending from a different email address, or contact a staff member. + We can't find any accounts that match your email address. Try sending from a different email address, or [contact a staff member](%{base_url}/about). email_reject_empty: title: "Email Reject Empty" @@ -2190,7 +2199,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your account does not have the privileges to post new topics in that category. If you believe this is an error, contact a staff member. + Your account does not have the privileges to post new topics in that category. If you believe this is an error, [contact a staff member](%{base_url}/about). email_reject_strangers_not_allowed: title: "Email Reject Strangers Not Allowed" @@ -2198,7 +2207,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - The category you sent this email to only allows replies from users with valid accounts and known email addresses. If you believe this is an error, contact a staff member. + The category you sent this email to only allows replies from users with valid accounts and known email addresses. If you believe this is an error, [contact a staff member](%{base_url}/about). email_reject_invalid_post: title: "Email Reject Invalid Post" @@ -2235,7 +2244,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - The reply key in the email is invalid or unknown, so we can't figure out what this email is in reply to. Contact a staff member. + The reply key in the email is invalid or unknown, so we can't figure out what this email is in reply to. [Contact a staff member](%{base_url}/about). email_reject_bad_destination_address: title: "Email Reject Bad Destination Address" @@ -2251,7 +2260,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - The topic you are replying to no longer exists -- perhaps it was deleted? If you believe this is an error, contact a staff member. + The topic you are replying to no longer exists -- perhaps it was deleted? If you believe this is an error, [contact a staff member](%{base_url}/about). email_reject_topic_closed: title: "Email Reject Topic Closed" @@ -2259,7 +2268,7 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - The topic you are replying to is currently closed and no longer accepting replies. If you believe this is an error, contact a staff member. + The topic you are replying to is currently closed and no longer accepting replies. If you believe this is an error, [contact a staff member](%{base_url}/about). email_reject_auto_generated: title: "Email Reject Auto Generated" @@ -2267,7 +2276,15 @@ en: text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your email was marked as "auto generated", which means it was automatically created by a computer instead of being typed by a human; we can't accept those kinds of emails. If you believe this is an error, contact a staff member. + Your email was marked as "auto generated", which means it was automatically created by a computer instead of being typed by a human; we can't accept those kinds of emails. If you believe this is an error, [contact a staff member](%{base_url}/about). + + email_reject_unrecognized_error: + title: "Email Reject Unrecognized Error" + subject_template: "[%{email_prefix}] Email issue -- Unrecognized Error" + text_body_template: | + We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. + + There was an unrecognized error while processing your email and it wasn't posted. You should try again, or [contact a staff member](%{base_url}/about). email_error_notification: title: "Email Error Notification" @@ -2412,10 +2429,21 @@ en: posted_by: "Posted by %{username} on %{post_date}" + invited_group_to_private_message_body: | + %{username} invited @%{group_name} to a message + + > #### %{topic_title} + > + > %{topic_excerpt} + + at + + > %{site_title} -- %{site_description} + invited_to_private_message_body: | %{username} invited you to a message - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} @@ -2426,7 +2454,7 @@ en: invited_to_topic_body: | %{username} invited you to a discussion - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} @@ -2434,6 +2462,16 @@ en: > %{site_title} -- %{site_description} + user_invited_to_private_message_pm_group: + title: "User Invited Group to PM" + subject_template: "[%{email_prefix}] %{username} invited @%{group_name} to a message '%{topic_title}'" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_private_message_pm: title: "User Invited to PM" subject_template: "[%{email_prefix}] %{username} invited you to a message '%{topic_title}'" @@ -2735,7 +2773,7 @@ en: login_required: welcome_message: | - #[Welcome to %{title}](#welcome) + ## [Welcome to %{title}](#welcome) An account is required. Please create an account or log in to continue. terms_of_service: diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 1dbe6baf1f..1c2b48bfed 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -357,7 +357,6 @@ es: translation_overrides: attributes: value: - invalid_interpolation_keys: 'Las siguientes claves de interpolación no son válidas: "%{keys}"' missing_interpolation_keys: 'Las siguientes claves de interpolación se perdieron: "%{keys}"' <<: *errors user_profile: @@ -932,9 +931,7 @@ es: notify_mods_when_user_blocked: "Si un usuario es bloqueado automáticamente, enviar un mensaje a todos los moderadores." 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." - enable_experimental_markdown_it: "Habilitar el motor experimental CommonMark markdown.it, ADVERTENCIA: algunos plugins podrían no funcionar correctamente." enable_markdown_typographer: "Utilice reglas básicas de tipografía para mejorar la legibilidad de texto de los párrafos de texto, reemplaza (c) (tm) etc, con símbolos, reduce el número de signos de interrogación y así sucesivamente" - allow_html_tables: "Permitir la inserción de tablas en Markdown usando etiquetas HTML. Se permitirá usar TABLE, THEAD, TD, TR o TH (requiere un rebake completo para los posts antiguos que contengan tablas)" 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!" pending_users_reminder_delay: "Notificar a los moderadores si hay nuevos usuarios que hayan estado esperando aprbación durante más estas horas. Usa -1 para desactivar estas notificaciones." @@ -1536,45 +1533,9 @@ es: invite_mailer: title: "Invitar Usuario" subject_template: "%{invitee_name} te invitó a '%{topic_title}' en %{site_domain_name}" - text_body_template: | - %{invitee_name} te invitó a un debate - - > **%{topic_title}** - > - > %{topic_excerpt} - - en - - > %{site_title} -- %{site_description} - - Si estás interesado, haz clic en el siguiente enlace: - - %{invite_link} - - Esta invitación proviene de un usuario de confianza, por lo que puedes comentar en el tema inmediatamente. custom_invite_mailer: title: "Personalizar Invitar Usuario" subject_template: "%{invitee_name} te ha invitado a '%{topic_title}' en %{site_domain_name}" - text_body_template: | - %{invitee_name} te ha invitado a un debate - - > **%{topic_title}** - > - > %{topic_excerpt} - - en - - > %{site_title} -- %{site_description} - - Mensaje de %{invitee_name}: - - %{user_custom_message} - - Si estás interesado en participar, haz clic en el enlace debajo: - - %{invite_link} - - Esta invitación viene de un usuario en el que confiamos, por lo que podrás responder inmediatamente. invite_forum_mailer: title: "Invitación suscripción del foro" subject_template: "%{invitee_name} te invitó a unirte a %{site_domain_name}" @@ -2139,26 +2100,6 @@ es: visit_link_to_respond: "[Visita el tema](%{base_url}%{url}) para responder." visit_link_to_respond_pm: "[Visita el mensaje](%{base_url}%{url}) para responder." posted_by: "Publicado por %{username} el %{post_date}" - invited_to_private_message_body: | - %{username} te invitó a un mensaje directo - - > **%{topic_title}** - > - > %{topic_excerpt} - - en - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} te invitó a un tema de debate - - > **%{topic_title}** - > - > %{topic_excerpt} - - en - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "Usuario invitado a MP" subject_template: "[%{email_prefix}] %{username} te ha invitado a un hilo de mensajes en '%{topic_title}'" diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 209ab34571..b8bd1bcb32 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -884,7 +884,6 @@ fa_IR: notify_mods_when_user_blocked: "اگر کاربر به‌طور خودکار مسدود شد، به تمام مدیران پیام بفرست." flag_sockpuppets: "اگر کاربری به موضوع با ای پی برابر با کاربری که نوشته را شروع کرده ٬ آنها را به عنوان هرزنامه پرچم گزاری کن." traditional_markdown_linebreaks: "در مدل‌های نشانه گزاری از خط جدید سنتی استفاده کن،‌ که برای linebreak نیاز به دو فضای انتهایی دارد ." - allow_html_tables: "اجازه ارسال جدول به صورت markdown با تگ های HTML. TABLE, THEAD, TD, TR, TH قابل استفاده هستند. (نیازمند ایجا دوباره در نوشته‌های قدیمی که شامل جدول هستند)" post_undo_action_window_mins: "تعداد دقایقی که کاربران اجازه دارند اقدامی را که در نوشته انجام داده اند باز گردانند. (پسند، پرچم گذاری،‌ چیزهای دیگر)." must_approve_users: "همکاران باید تمامی حساب‌های کاربری را قبل از اجازه دسترسی به سایت تایید کنند. اخطار: فعال‌سازی این گزینه ممکن است باعث جلوگیری از دسترسی کاربرانی که قبلا عضو شده‌اند نیز بشود!" pending_users_reminder_delay: "اگر کاربر‌ها بیشتر از این مقدار ساعت منتظر تایید بودند به مدیران اعلام کن. مقدار -1 برای غیرفعال‌سازی." @@ -1465,41 +1464,9 @@ fa_IR: invite_mailer: title: "دعوت نامه‌رسان" subject_template: "%{invitee_name} شما را به %{topic_title}' در %{site_domain_name} دعوت کرده" - text_body_template: | - %{invitee_name} شما را به گفتگو دعوت کرده - - > **%{topic_title}** - > - > %{topic_excerpt} - - at - - > %{site_title} -- %{site_description} - - اگر علاقه‌مند هستید روی لینک زیر کلیک کنید: - - %{invite_link} custom_invite_mailer: title: "دعوت سفارشی نامه‌رسان" subject_template: "%{invitee_name} شما را به '%{topic_title}' در %{site_domain_name} دعوت کرده" - text_body_template: | - %{invitee_name} شما را به گفتگو دعوت کرده - - > **%{topic_title}** - > - > %{topic_excerpt} - - at - - > %{site_title} -- %{site_description} - - با این یادداشت - - > %{user_custom_message} - - اگر علاقه‌مند هستید روی لینک زیر کلیک کنید: - - %{invite_link} invite_forum_mailer: title: "دعوت نامه‌رسان انجمن" subject_template: "%{invitee_name} به عضویت در %{site_domain_name} دعوت شدید" @@ -2001,26 +1968,6 @@ fa_IR: visit_link_to_respond: "[موضوع را](%{base_url}%{url}) برای ارسال پاسخ ببینید." visit_link_to_respond_pm: "[پیام را](%{base_url}%{url}) برای ارسال پاسخ ببینید." posted_by: "نوشته شده توسط {username}% در تاریخ {post_date}%" - invited_to_private_message_body: | - %{username} شما را به یک پیام دعوت کرده - - > **%{topic_title}** - > - > %{topic_excerpt} - - در - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} شما را به یک بحث دعوت کرده - - > **%{topic_title}** - > - > %{topic_excerpt} - - در - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "کاربر دعوت شده به پیام خصوصی" subject_template: "[%{email_prefix}] %{username} شما را به پیام '%{topic_title}' دعوت کرده" @@ -2471,7 +2418,7 @@ fa_IR: long_description: | این نشان زمانی اعطا می‌شود که برای بار اول یک نوشته را در پاسخ خود نقل‌قول کنید. نقل‌قول بخش‌های مربوطه کمک می‌کند که بحث‌های موضوع به هم متصل شوند. بهترین راه برای نقل قول انتخاب متن در نوشته و کلیک روی دکمه پاسخ است. سخاوتمندانه نقل‌قول کنید! read_guidelines: - name: دستور‌العمل را خوانده + name: خواندن دستورالعمل‌ها description: دستورالعمل های انجمن را بخوانید long_description: | این نشان زمانی اعطا می‌شود راهنمای انجمن را بخوانید. دنبال کردن و به اشتراک گذاری این راهنمای ساده به ایجاد یک انجمن امن، لذت‌بخش و پایدار برای همه کمک می‌کند. همیشه به یاد داشته باشید که همیشه فردی وجود دارد که خیلی شبیه شماست، و برعکس این مسئله هم وجود دارد. خوب باشید! diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 9c699ac9ec..ca789d95a9 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -171,10 +171,12 @@ fi: too_many_links: one: "Pahoittelut, uudet käyttäjät voivat laittaa vain yhden linkin viestiin." other: "Pahoittelut, uusi käyttäjä voi laittaa vain %{count} linkkiä viestiin." + contains_blocked_words: "Viestissäsi on kielletty sanoja." spamming_host: "Pahoittelut, linkit tuolle sivulle eivät ole sallittuja." user_is_suspended: "Hyllytetyt käyttäjät eivät saa luoda viestejä." topic_not_found: "Jotain on mennyt pieleen. Ehkä tämä ketju on suljettu tai poistettu sillä välin, kun katselit sitä?" not_accepting_pms: "Pahoittelut, %{username} ei ota vastaan yksityisviestejä tällä hetkellä." + max_pm_recepients: "Pahoittelut, voit lähettää viestin enintään %{recipients_limit} vastaanottajalle." just_posted_that: "on liian samanlainen kuin aiempi viestisi" invalid_characters: "sisältää epäkelpoja merkkejä" is_invalid: "vaikuttaa epäselvältä, olihan se kokonainen virke?" @@ -358,8 +360,11 @@ fi: translation_overrides: attributes: value: - invalid_interpolation_keys: 'Nämä interpolaatioavaimet eivät kelpaa: "%{keys}"' missing_interpolation_keys: 'Nämä interpolaatioavaimet puuttuvat: "%{keys}"' + watched_word: + attributes: + word: + too_many: "Toiminnolle on määritelty liian monta sanaa" <<: *errors user_profile: no_info_me: "
    Tietoa minusta -kenttä käyttäjätiedoissasi on täyttämättä. Haluaisitko täyttää sen nyt?
    " @@ -422,6 +427,7 @@ fi: invalid_email_in: "'%{email}' ei ole käypä sähköpostiosoite" email_already_used_in_group: "'%{email}' on jo käytössä ryhmällä '%{group_name}'." email_already_used_in_category: "'%{email}' on jo käytössä alueella '%{category_name}'." + description_incomplete: "Alueen kuvauksessa on oltava ainakin yksi kappale." cannot_delete: uncategorized: "Alueettomat-aluetta ei voi poistaa" has_subcategories: "Aluetta ei voi poistaa, koska sillä on tytäralueita." @@ -655,6 +661,7 @@ fi: remove: "Tämä ketju ei ole enää banneri. Sitä ei näytetä enää jokaisen sivun ylälaidassa." unsubscribed: title: "Tilaus peruttu!" + description: "Tilaus osoitteeseen %{email} on peruttu. Voit muuttaa sähköpostiasetuksia käyttäjäasetuksissasi." topic_description: "Tilaa %{link} uudelleen ketjun alla tai oikealla puolen sijaitsevan ilmoitusvalikon kautta." unsubscribe: title: "Peru tilaus" @@ -865,6 +872,8 @@ fi: search_tokenize_chinese_japanese_korean: "Pakota haku käsittelemään kiinaa/japania/koreaa myös muunkielisillä sivustoilla" search_prefer_recent_posts: "Jos hakeminen suurelta palstaltasi on hidasta, tämä asetus kokeilee hakemistorakennetta, jossa tuoreimmat viestit ovat ensin" search_recent_posts_size: "Kuinka monta tuoretta viestiä pidetään hakemistossa" + log_search_queries: "Pidä lokia käyttäjien tekemistä hauista" + search_query_log_max_size: "Kuinka monta hakukyselyä säilötään enintään" allow_uncategorized_topics: "Salli ketjujen luominen valitsematta aluetta. VAROITUS: Alueettomat ketjut täytyy siirtää jollekin alueelle ennen kuin poistat asetuksen käytöstä." allow_duplicate_topic_titles: "Salli ketjun luominen identtisellä otsikolla." unique_posts_mins: "Kuinka monen minuutin kuluttua käyttäjä voi lähettää uudestaan samansisältöisen viestin" @@ -894,6 +903,7 @@ fi: show_pinned_excerpt_desktop: "Näytä katkelma kiinnitetyistä ketjuista työpöytänäkymässä." post_onebox_maxlength: "Discourse-viestin Onebox-esikatselun merkkien maksimimäärä." onebox_domains_blacklist: "Verkko-osoitteet, joista ei luoda Onebox-esikatselua" + inline_onebox_domains_whitelist: "Verkko-osoitteet, joista luodaan Onebox-esikatselu, jos niihin linkitetään määrittämättä otsikkoa." max_oneboxes_per_post: "Oneboxien enimmäismäärä yhdessä viestissä" logo_url: "Logo sivustosi vasemmassa yläkulmassa. Sen pitäisi olla leveä suorakulmio muodoltaan. Jos jätetään tyhjäksi, näytetään sivuston otsikkoteksti." digest_logo_url: "Vaihtoehtoinen logo, jota käytetään sähköpostikoosteessa. Muodoltaan sen tulisi olla leveä suorakulmio, eikä se saa olla SVG-muodossa. Jos ei aseteta, käytetään `logo_url`-kuvaa." @@ -931,9 +941,7 @@ fi: notify_mods_when_user_blocked: "Jos käyttäjä estetään automaattisesti, lähetä viesti kaikille valvojille." flag_sockpuppets: "Jos uuden käyttäjän luomaan ketjuun vastaa toinen uusi käyttäjä 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." - enable_experimental_markdown_it: "Ota käyttöön kokeellinen markdown.it Commonmark ohjelmistomoottori. VAROITUS: jotkut lisäosat voivat lakata toimimasta oikein" enable_markdown_typographer: "Käytetään tavanomaisia typografisia sääntöjä parantamaan tekstikappaleiden luettavuutta, (c), (tm) ym. korvataan symboleilla, kysymysmerkkien määrää vähennetään jne." - allow_html_tables: "Salli taulukoiden syöttäminen Markdowniin käyttäen HTML tageja. TABLE, THEAD, TD, TR, TH valkolistataan (edellyttää kaikkien taulukoita sisältävien vanhojen viestien uudelleen rakentamisen)" 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ä." pending_users_reminder_delay: "Ilmoita valvojille, jos uusi käyttäjä on odottanut hyväksyntää kauemmin kuin näin monta tuntia. Aseta -1, jos haluat kytkeä ilmoitukset pois päältä." @@ -1298,6 +1306,7 @@ fi: auto_close_messages_post_count: "Maksimimäärä viestejä yksityisketjussa, kunnes se suljetaan automaattisesti (0 poistaaksesi käytöstä)" auto_close_topics_post_count: "Maksimimäärä viestejä ketjussa, kunnes se suljetaan automaattisesti (0 poistaaksesi käytöstä)" code_formatting_style: "Viestikentän koodipainike käyttää oletuksena tätä koodimuotoilutyyliä" + max_allowed_message_recipients: "Kuinka monta vastaanottajaa yksityisviestillä voi olla." default_email_digest_frequency: "Kuinka usein käyttäjille lähetetään sähköpostikooste oletuksena." default_include_tl0_in_digests: "Sisällytä uusien käyttäjien viestit sähköpostikoosteisiin oletuksena. Tätä voi muuttaa käyttäjäasetuksissa." default_email_private_messages: "Lähetä oletuksena sähköposti, kun joku lähettää käyttäjälle viestin." @@ -1366,6 +1375,8 @@ fi: email_polling_disabled: "Sinun täytyy ottaa käyttöön joko manuaalinen tai POP3 pollaus ennen sähköpostivastauksia." user_locale_not_enabled: "'allow user locale' täytyy olla asetettuna ennen tämän asetuksen ottamista käyttöön." invalid_regex: "Säännöllinen lauseke ei kelpaa tai ei ole sallittu." + email_editable_enabled: "Asetus 'email editable' on otettava pois käytöstä ennen tämän asetuksen käyttöönottoa." + enable_sso_disabled: "Asetus 'enable sso' on otettava käyttöön ennen tämän asetuksen käyttöönottoa." search: within_post: "#%{post_number} käyttäjältä %{username}" types: @@ -1530,36 +1541,35 @@ fi: invite_mailer: title: "Kutsu keskusteluun" subject_template: "%{invitee_name} kutsui sinut ketjuun '%{topic_title}' sivustolla %{site_domain_name}" - text_body_template: |+ + text_body_template: | %{invitee_name} kutsui sinut keskusteluun - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} - keskustelupalstalla + sivustolla > %{site_title} -- %{site_description} Jos kiinnostuit, klikkaa alla olevaa linkkiä: %{invite_link} - custom_invite_mailer: title: "Kutsu keskusteluun saatesanoilla" subject_template: "%{invitee_name} kutsui sinut ketjuun '%{topic_title}' sivustolla %{site_domain_name}" text_body_template: | %{invitee_name} kutsui sinut keskusteluun - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} - keskustelupalstalla + sivustolla > %{site_title} -- %{site_description} - tällaisin saatesanoin: + tällaisin saatesanoin > %{user_custom_message} @@ -1851,28 +1861,28 @@ fi: csv_export_failed: title: "CSV:n vienti epäonnistui" subject_template: "Datan vienti epäonnistui" - text_body_template: "Pahoittelemme, mutta datan vientisi epäonnistui. Tarkasta lokit tai ota yhteyttä henkilökuntaan." + text_body_template: "Pahoittelemme, mutta datan vienti epäonnistui. Tarkasta lokit tai [ota yhteyttä henkilökuntaan](%{base_url}/about)." email_reject_insufficient_trust_level: title: "Sähköposti hylätty - riittämätön luottamustaso" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Riittämätön luottamustaso" text_body_template: | - Pahoittelut, sähköpostiviestiäsi tänne: %{destination} (otsikolla %{former_title}) ei voitu toimittaa. + Pahoittelut, sähköpostin lähettäminen kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. - Tilisi luottamustaso ei ole riittävä, jotta saisit lähettää uusia ketjuja tähän sähköpostiosoitteeseen. Jos uskot, että tämä johtuu virheestä, ota yhteyttä henkilökuntaan. + Et voi lähettää uusia ketjun avauksia tähän sähköpostiosoitteeseen, koska luottamustasosi ei riitä. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_user_not_found: title: "Sähköposti hylätty - käyttäjää ei löytynyt" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Käyttäjää ei löytynyt" text_body_template: | - Pahoittelemme, mutta sähköpostin lähettäminen tänne %{destination} (otsikolla %{former_title}) ei onnistunut. + Pahoittelemme, mutta sähköpostin lähettäminen kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. - Vastauksesi lähetettiin tuntemattomasta sähköpostiosoitteesta. Yritä lähettää viesti toisesta osoitteesta, tai ota yhteyttä henkilökuntaan. + Vastauksesi lähetettiin tuntemattomasta sähköpostiosoitteesta. Yritä lähettää viesti toisesta osoitteesta tai [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_screened_email: title: "Sähköposti hylätty - estetty sähköpostiosoite" subject_template: "[%{email_prefix}] Sähköpostiongelma-- Estetty sähköpostiosoite" text_body_template: |+ Pahoittelemme, mutta sähköpostin lähettäminen tänne %{destination} (otsikolla %{former_title}) ei onnistunut. - Viestisi lähetettiin estetystä sähköpostiosoitteesta. Yritä lähettää viesti toisesta sähköpostiosoitteesta tai ota yhteyttä henkilökuntaan. + Vastauksesi lähetettiin estetystä sähköpostiosoitteesta. Yritä lähettää viesti toisesta sähköpostiosoitteesta tai [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_inactive_user: title: "Sähköposti hylätty - aktivoimaton käyttäjä" @@ -2123,7 +2133,7 @@ fi: invited_to_private_message_body: | %{username} kutsui sinut yksityiskeskusteluun - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} @@ -2131,9 +2141,9 @@ fi: > %{site_title} -- %{site_description} invited_to_topic_body: | - %{username} kutsui sinut keskusteluun + %{username} kustui sinut keskusteluun - > **%{topic_title}** + > #### %{topic_title} > > %{topic_excerpt} diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 2b85e00688..79dafa2f0a 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -357,7 +357,6 @@ fr: translation_overrides: attributes: value: - invalid_interpolation_keys: 'Les clés d''interpolation suivantes sont invalides : « %{keys} »' missing_interpolation_keys: 'Les clés d''interpolation suivantes sont absentes : « %{keys} »' <<: *errors user_profile: @@ -931,7 +930,6 @@ fr: notify_mods_when_user_blocked: "Si un utilisateur est bloqué automatiquement, envoyer un message à tous les modérateurs." flag_sockpuppets: "Si un nouvel utilisateur répond à un sujet avec la même adresse IP que le nouvel utilisateur qui a commencé le sujet, alors leurs messages seront automatiquement marqués comme spam." traditional_markdown_linebreaks: "Utiliser le retour à la ligne traditionnel dans Markdown, qui nécessite deux espaces pour un saut de ligne." - allow_html_tables: "Autoriser la saisie des tableaux dans le Markdown en utilisant les tags HTML : TABLE, THEAD, TD, TR, TH sont autorisés (nécessite un rebake de tous les anciens messages contenant des tableaux)" post_undo_action_window_mins: "Nombre de minutes pendant lesquelles un utilisateur peut annuler une action sur un message (J'aime, signaler, etc.)" must_approve_users: "Les responsables doivent approuver les nouveaux utilisateurs afin qu'ils puissent accéder au site. ATTENTION : activer cette option sur un site en production suspendra l'accès des utilisateurs existants qui ne sont pas des responsables !" pending_users_reminder_delay: "Avertir les modérateurs si des nouveaux utilisateurs sont en attente d'approbation depuis x heures. Mettre -1 pour désactiver les notifications." @@ -1528,41 +1526,9 @@ fr: invite_mailer: title: "Invitiation" subject_template: "%{invitee_name} vous a invité(e) à participer à « %{topic_title} » sur %{site_domain_name}" - text_body_template: | - %{invitee_name} vous a invité(e) à une discussion - - > **%{topic_title}** - > - > %{topic_excerpt} - - à - - > %{site_title} -- %{site_description} - - Si cela vous intéresse, cliquez sur le lien ci-dessous : - - %{invite_link} custom_invite_mailer: title: "Invitation personnalisée" subject_template: "%{invitee_name} vous a invité(e) à participer à « %{topic_title} » sur %{site_domain_name}" - text_body_template: | - %{invitee_name} vous a invité(e) à une discussion - - > **%{topic_title}** - > - > %{topic_excerpt} - - à - - > %{site_title} -- %{site_description} - - Avec ce message : - - > %{user_custom_message} - - Si cela vous intéresse, cliquez sur le lien ci-dessous : - - %{invite_link} invite_forum_mailer: title: "Invitation sur le forum" subject_template: "%{invitee_name} vous a invité(e) à rejoindre %{site_domain_name}" @@ -2092,26 +2058,6 @@ fr: visit_link_to_respond: "[Voir le sujet](%{base_url}%{url}) pour répondre." visit_link_to_respond_pm: "[Voir le message](%{base_url}%{url}) pour répondre." posted_by: "Redigé par %{username} le %{post_date}" - invited_to_private_message_body: | - %{username} vous a invité(e) à un message - - > **%{topic_title}** - > - > %{topic_excerpt} - - sur - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} vous a invité(e) à une discussion - - > **%{topic_title}** - > - > %{topic_excerpt} - - sur - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "Utilisateur invité au MP" subject_template: "[%{email_prefix}] %{username} vous a invité dans la conversation « %{topic_title} »" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 5795be1d51..989cecda71 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -28,10 +28,10 @@ he: posts: "פוסטים" loading: "טוען" powered_by_html: 'מופעל על ידי דיסקורס, לצפיה מיטבית יש להפעיל JavaScript' - log_in: "התחברות" + log_in: "כניסה" purge_reason: "נמחק אוטומטית כחשבון נטוש ולא פעיל" disable_remote_images_download_reason: "הורדת תמונות מרחוק נחסמה בשל היעדר מספיק שטח אחסון פנוי." - anonymous: "אנונימי" + anonymous: "אלמוני" remove_posts_deleted_by_author: "נמחק על ידי הכותב" themes: bad_color_scheme: "לא ניתן לעדכן את התמה, סכמת צבעים שגויה" @@ -121,11 +121,11 @@ he: user_exists: "מצטערים, המשתמשים כבר הוזמנו. ניתן להזמין משתמשים לנושא רק פעם אחת." backup: operation_already_running: "פעולה רצה כרגע. לא יכול להתחיל עבודה חדשה עכשיו." - backup_file_should_be_tar_gz: "קובץ הגיבוי צריך להיות .tar.gz." - not_enough_space_on_disk: "אין מספיק מקום על הדיסק כדי להעלות את הגיבוי הזה." + backup_file_should_be_tar_gz: "קובץ הגיבוי צריך להיות ‎.tar.gz." + not_enough_space_on_disk: "אין מספיק מקום על הכונן כדי להעלות את הגיבוי הזה." invalid_filename: "שם קובץ הגיבוי מכיל תווים שגויים. תווים תקינים הם a-z 0-9 . - _." not_logged_in: "אתם צריכים להיות מחוברים כדי לעשות את זה." - not_found: "המשאב או כתובת ה-URL המבוקשת לא נמצאו." + not_found: "המשאב או הכתובת המבוקשת לא נמצאו." invalid_access: "אינכם מורשים לצפות במשאב שביקשתם." read_only_mode_enabled: "האתר הזה במצב קריאה בלבד. פעולות אין אפשריות." reading_time: "זמן קריאה" @@ -171,10 +171,12 @@ he: too_many_links: one: "מצטערים, משתמשים חדשים יכולים להוסיף רק קישור אחד בפוסט." other: "מצטערים, משתמשים חדשים יכולים להוסיף רק %{count} קישורים בפוסט." + contains_blocked_words: "הפוסט שלך מכיל מילים אסורות." spamming_host: "סליחה אך אינכם יכולים להוסיף קישור לאתר זה." user_is_suspended: "משתמשים מושעים אינם מורשים לפרסם." topic_not_found: "משהו השתבש אולי נושא זה נסגר או נמחק בזמן שקראתם אותו?" not_accepting_pms: "מצטעים, %{username} לא מקבל הודעות כרגע." + max_pm_recepients: "מצטערים, אתם יכולים לשלוח הודעה לכל היותר ל-%{recipients_limit} נמענים." just_posted_that: "דומה מידי למה שפרסמתם לאחרונה" invalid_characters: "מכיל תווים לא תקניים" is_invalid: "נראה לא ברור, האם זה משפט שלם?" @@ -351,6 +353,10 @@ he: attributes: execute_at: in_the_past: "חייב להיות בעתיד" + watched_word: + attributes: + word: + too_many: "יש יותר מדי מילים בפעולה הזו" <<: *errors user_profile: no_info_me: "
    השדה ״אודותי״ בפרופיל שלכם הוא ריק כרגע, תרצו למלא אותו?
    " @@ -411,6 +417,7 @@ he: invalid_email_in: "'%{email}' אינה כתובת מייל תקפה." email_already_used_in_group: "'%{email}' כבר בשימוש על ידי הקבוצה '%{group_name}'." email_already_used_in_category: "'%{email}' כבר בשימוש על ידי הקטגוריה '%{category_name}'." + description_incomplete: "לתיאור קטגוריה זו צריכה להיות לפחות פסקה אחת." cannot_delete: uncategorized: "לא ניתן למחוק תוכן ללא קטגוריה" has_subcategories: "לא ניתן למחוק קטגוריה זו משום שיש בה תת-קטגוריות." @@ -854,6 +861,8 @@ he: search_tokenize_chinese_japanese_korean: "אלצו את החיפוש לנתח סינית/יפנית/קוריאנית גם באתרים שאינם בשפות אלו" search_prefer_recent_posts: "אם חיפוש בפורום הגדול שלכם איטי, אופציה זו מנסה לאנדקס קודם כל את הפוסטים החדשים יותר" search_recent_posts_size: "כמה פוסטים חדשים לשמור באינדקס" + log_search_queries: "רישום שאילתות חיפוש שמתבצעות על ידי משתמשים" + search_query_log_max_size: "מספר מקסימלי של שאילתות חיפוש לשימור" allow_uncategorized_topics: "הרשה לפתוח נושאים ללא קטגוריה. אזהרה: אם יש נושאים ללא קטגוריה, יש לסדר אותם לפני שמבטלים את האופציה. " allow_duplicate_topic_titles: "אשרו נושאים עם כותרות זהות או משוכפלות." unique_posts_mins: "כמה דקות לפני שמשתמש יכול לפרסם את אותו תוכן שוב" @@ -921,7 +930,6 @@ he: notify_mods_when_user_blocked: "אם משתמש נחסם אוטומטית, שילחו הודעה לכל המנחים." flag_sockpuppets: "אם משתמשים חדשים מגיבים לנושא מכתובת IP זהה לזו של מי שהחל את הנושא, סמנו את הפוסטים של שניהם כספאם פוטנציאלי." traditional_markdown_linebreaks: "שימוש בשבירת שורות מסורתית בסימון, מה שדורש שני רווחים עוקבים למעבר שורה." - allow_html_tables: "אפשרו הכנסת טבלאות ב Markdown באמצעות תגיות HTML. התגיות TABLE, THEAD, TD, TR, TH יהיו ברשימה לבנה (מצריך אפייה מחדש של כל הפוסטים הישנים שכוללים טבלאות)" post_undo_action_window_mins: "מספר הדקות בהן מתאפשר למשתמשים לבטל פעולות אחרות בפוסט (לייק, סימון, וכו')." must_approve_users: "על הצוות לאשר את כל המשתמשים החדשים לפני שהם מקבלים גישה לאתר. אזהרה: בחירה זו עבור אתר קיים תשלול גישה ממשתמשים קיימים שאינם מנהלים." pending_users_reminder_delay: "הודיעו למנחים אם משתמשים חדשים ממתינים לאישור למעלה מכמות זו של שעות. קבעו ל -1 כדי לנטרל התראות." @@ -1286,6 +1294,7 @@ he: auto_close_messages_post_count: "מספר פוסטים מקסימלי בהודעה לפני שהיא נסגרת אוטומטית (0 לניטרול)" auto_close_topics_post_count: "מספר מקסימלי של פוסטים בנושא לפני שהוא נסגר אוטומטית (0 לניטרול)" code_formatting_style: "כפתור קוד בדפדפן יציע אוטומטית סגנון קידוד זה" + max_allowed_message_recipients: "מספר מקסימלי של נמענים מותר בהודעה." default_email_digest_frequency: "באיזו תדירות משתמשים יקבלו סיכומי מיילים כברירת מחדל." default_include_tl0_in_digests: "כללו פוסטים ממשתמשים חדשים בדוא\"ל מסכם כברירת מחדל. משתמשים יוכלו לשנות זאת בהעדפות האישיות." default_email_private_messages: "שלח מייל כשמישהו שולח הודעה למשתמש, בתור ברירת מחדל." @@ -1521,41 +1530,9 @@ he: invite_mailer: title: "שולח-מיילים להזמנה" subject_template: "הוזמנת על ידי %{invitee_name} ל '%{topic_title}' ב%{site_domain_name}" - text_body_template: | - %{invitee_name} הזמין/ה אתכם לדיון - - > **%{topic_title}** - > - > %{topic_excerpt} - - ב - - > %{site_title} -- %{site_description} - - אם אתם מעוניינים, לחצו על הקישור למטה: - - %{invite_link} custom_invite_mailer: title: "שולח-מיילים מותאם-אישית להזמנה" subject_template: "%{invitee_name} הזמין/הזמינה אותך לדיון '%{topic_title}' באתר %{site_domain_name}" - text_body_template: | - %{invitee_name} הזמין/ה אתכם לדיון - - > **%{topic_title}** - > - > %{topic_excerpt} - - ב - - > %{site_title} -- %{site_description} - - עם הערה זו - - > %{user_custom_message} - - אם אתם מעוניינים, לחצו על הקישור למטה: - - %{invite_link} invite_forum_mailer: title: "שולח-מיילים להזמנה לפורום" subject_template: "%{invitee_name} הזמינ/ה אתכם להצטרף ל%{site_domain_name}" @@ -1949,24 +1926,14 @@ he: email_reject_topic_not_found: title: "דחיית מייל נושא לא נמצא" subject_template: "[%{email_prefix}] בעיית מייל -- לא נמצא נושא" - text_body_template: | - אנחנו מצטערים, אך הודעת המייל ל %{destination} (עם הכותרת %{former_title}) לא עבדה. - - הנושא שאתם מנסים להגיב אליו כבר לא קיים -- אולי הוא נמחק? אם אתם מאמינים שזו טעות, אנא פנו לחבר צוות. email_reject_topic_closed: title: "דחיית מייל נושא סגור" subject_template: "[%{email_prefix}] בעיית מייל -- נושא נסגר" - text_body_template: | - אנחנו מצטערים, אך הודעת המייל ל %{destination} (עם הכותרת %{former_title}) לא עבדה. - - הנושא שאתם מגיבים אליו סגור כרגע ולא מקבל יותר תגובות. אם אתם מאמינים שזו טעות, אנא פנו לחבר צוות. email_reject_auto_generated: title: "דחיית מייל נוצר אוטומטית" subject_template: "[%{email_prefix}] בעיית מייל -- תגובה נוצרה אוטומטית" - text_body_template: | - אנחנו מצטערים, אבל משהו לא עבד עם הודעת הדוא"ל שלך, שנשלחה אל %{destination} (titled %{former_title}). - - המייל ששלחת סומן כ"נכתב אוטומטית", מה שאומר שהוא נכתב על ידי מחשב ולא הוקלד על ידי אדם; איננו יכולים לקבל מיילים שכאלה. אם אתם מאמינים שזוהי תקלה, אנא צרו קשר עם איש/אשת צוות. + email_reject_unrecognized_error: + subject_template: "[%{email_prefix}] בעיה עם הדוא״ל -- שגיאה לא מוכרת" email_error_notification: title: "התראת בעיית מייל" subject_template: "[%{email_prefix}] בעיית מייל -- בעיית אימות POP" @@ -2088,26 +2055,6 @@ he: visit_link_to_respond: "[בקרו בנושא](%{base_url}%{url}) כדי לענות." visit_link_to_respond_pm: "[בקרו בהודעה](%{base_url}%{url}) כדי לענות." posted_by: "פורסם על ידי %{username} ב %{post_date}" - invited_to_private_message_body: | - %{username} הזמין אותך להודעה - - > **%{topic_title}** - > - > %{topic_excerpt} - - באתר - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} הזמין/הזמינה אותך לדיון - - > **%{topic_title}** - > - > %{topic_excerpt} - - באתר - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "משתמש/ת הוזמן/ה להודעה פרטית" subject_template: "[%{email_prefix}] %{username} הזמין אותך להודעה '%{topic_title}'" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 722c723d09..b6321c1cae 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -171,10 +171,12 @@ it: too_many_links: one: "Spiacenti, i nuovi utenti possono inserire al massimo un collegamento in un messaggio." other: "Spiacenti, i nuovi utenti possono inserire al massimo %{count} collegamenti in un messaggio." + contains_blocked_words: "Il tuo messaggio contiene parole che non sono consentite." spamming_host: "Spiacenti, non puoi inserire un collegamento verso quel dominio." user_is_suspended: "Agli utenti sospesi non è permesso creare messaggi." topic_not_found: "Quancosa non ha funzionato. Forse questo argomento è stato chiuso o cancellato mentre lo leggevi." not_accepting_pms: "Spiacenti, %{username} non accetta messaggi al momento." + max_pm_recepients: "Spiacenti, puoi inviare un messaggio ad un massimo di %{recipients_limit} destinatari." just_posted_that: "è troppo simile a ciò che hai appena pubblicato" invalid_characters: "contiene caratteri non validi" is_invalid: "sembra poco chiaro, è una frase completa?" @@ -360,8 +362,11 @@ it: translation_overrides: attributes: value: - invalid_interpolation_keys: 'Le seguenti chiavi(e) di interpolazione non sono valide: "%{keys}"' missing_interpolation_keys: 'Le seguenti chiavi(e) di interpolazione sono mancanti: "%{keys}"' + watched_word: + attributes: + word: + too_many: "Troppe parole per quest'azione" <<: *errors user_profile: no_info_me: "
    il campo \"Su di me\" del tuo profilo è vuoto, vuoi compilarlo?
    " @@ -424,6 +429,7 @@ it: invalid_email_in: "'%{email}' non è un indirizzo email valido." email_already_used_in_group: "L'indirizzo '%{email}' è stato già usato dal gruppo '%{group_name}'." email_already_used_in_category: "L'indirizzo '%{email}' è stato già usato dalla categoria '%{category_name}'." + description_incomplete: "Il messaggio di descrizione della categoria deve avere almeno un paragrafo." cannot_delete: uncategorized: "Non puoi eliminare la categoria \"Non classificato\"" has_subcategories: "Non puoi cancellare questa categoria perché ha sotto-categorie." @@ -868,6 +874,8 @@ it: search_tokenize_chinese_japanese_korean: "Attiva la tokenizzazione dei caratteri Cinesi/Giapponesi/Coreani nella ricerca anche sui siti non CJK" search_prefer_recent_posts: "Se il tuo forum è corposo e la ricerca è lenta, questa opzione tenta di indicizzare prima i messaggi più recenti" search_recent_posts_size: "Quanti messaggi recenti mantenere nell'indice" + log_search_queries: "Log delle query di ricerca eseguite dagli utenti" + search_query_log_max_size: "Quantità massima di query di ricerca da mantenere" allow_uncategorized_topics: "Permetti la creazione di argomenti senza categoria. ATTENZIONE: se ci sono argomenti senza categoria, devi ricategorizzarli prima di disabilitare questa opzione." allow_duplicate_topic_titles: "Permetti più argomenti con lo stesso identico titolo" unique_posts_mins: "Quanti minuti prima che un utente possa inviare un altro argomento con lo stesso contenuto" @@ -897,6 +905,7 @@ it: show_pinned_excerpt_desktop: "Mostra estratti sugli argomenti appuntati nella vista desktop." post_onebox_maxlength: "Lunghezza massima in caratteri di un messaggio Discourse in Onebox." onebox_domains_blacklist: "Una lista di domini che non verranno mai oneboxed." + inline_onebox_domains_whitelist: "Una lista di domini che saranno oneboxed in forma di miniatura se collegati senza un titolo" max_oneboxes_per_post: "Numero massimo di oneboxe in un messaggio." logo_url: "L'immagine nel logo in alto a sinistra del sito, dovrebbe essere larga e di forma rettangolare. Se non impostata, verrà usato il testo del titolo del sito." digest_logo_url: "Il logo alternativo usato in cima alla email di riepilogo. Dovrebbe essere a forma di un ampio rettangolo. Non dovrebbe essere un'immagine SVG. Se lasciato vuoto, sarà utilizzato il campo `logo_url`. " @@ -934,7 +943,6 @@ it: notify_mods_when_user_blocked: "Se un utente è bloccato automaticamente, manda un messaggio ai moderatori." flag_sockpuppets: "Se un nuovo utente risponde ad un argomento dallo stesso indirizzo IP dell'utente che ha aperto l'argomento stesso, segnala entrambi i messaggi come potenziale spam." traditional_markdown_linebreaks: "Usa l'accapo tradizionale in Markdown, cioè due spazi a fine riga per andare a capo." - allow_html_tables: "Consenti di inserire tabelle in Markdown usando tag HTML. I tag TABLE, THEAD, TD, TR, TH saranno consentiti (richiede un full rebake di tutti i vecchi messaggi contenenti tabelle)" post_undo_action_window_mins: "Numero di minuti durante i quali gli utenti possono annullare le loro azioni recenti su un messaggio (segnalazioni, Mi piace, ecc.)." must_approve_users: "Lo staff deve approvare tutti i nuovi account utente prima che essi possano accedere al sito. ATTENZIONE: abilitare l'opzione per un sito live revocherà l'accesso per tutti gli utenti non-staff esistenti!" pending_users_reminder_delay: "Notifica i moderatori se nuovi utenti sono in attesa di approvazione per più di queste ore. Imposta a -1 per disabilitare le notifiche." @@ -943,6 +951,7 @@ it: ga_domain_name: "OBSOLETO: nome del dominio Google analytics (ga.js), esempio: miosito.com; vedi http://google.com/analytics" ga_universal_tracking_code: "Codice di tracking Google Universal Analytics (analytics.js), es: UA-12345678-9; vedi http://google.com/analytics" ga_universal_domain_name: "Nome di dominio Google Universal Analytics (analytics.js), es: miosito.com; vedi http://google.com/analytics" + ga_universal_auto_link_domains: "Attiva Il monitoraggio interdominio di Google Universal Analytics (analytics.js). Ai collegamenti in uscita a questi domini verrà aggiunto l'id del client. Vedi la guida di Google Cross-domain Tracking." gtm_container_id: "Container id di Google Tag Manager. Es: GTM-ABCDEF" enable_escaped_fragments: "Usa le Ajax-Crawling API di Google se non viene rilevato un webcrawler. Vedi https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Permetti ai moderatori di creare nuove categorie" @@ -1170,12 +1179,15 @@ it: disable_emails: "Evita che Discourse invii qualunque tipo di email" strip_images_from_short_emails: "Elimina le immagini dalle email aventi dimensione inferiore a 2800 Byte." short_email_length: "Lunghezza delle email brevi in Byte" + unsubscribe_via_email: "Consenti agli utenti di annullare la sottoscrizione alle email inviando una email con 'unsubscribe' nell'oggetto o nel corpo" + delete_email_logs_after_days: "Elimina i log delle email dopo (N) giorni. 0 per mantenerli indefinitamente" max_emails_per_day_per_user: "Numero massimo di email da inviare agli utenti per giorno. 0 per disabilitare il limite" enable_staged_users: "Crea automaticamente utenti temporanei quando si elaborano le email in arrivo." maximum_staged_users_per_email: "Numero massimo di utenti temporanei creati quando si elaborano le email in arrivo." auto_generated_whitelist: "Lista degli indirizzi email che non verranno verificati per i contenuti generati automaticamente. Esempio: foo@bar.com|discourse@bar.com" block_auto_generated_emails: "Bloccare le email in arrivo identificate come generate automaticamente." ignore_by_title: "Ignora le email in arrivo in base al loro titolo." + mailgun_api_key: "Chiave API Segreta di Mailgun usata per verificare i messaggi webhook. " attachment_content_type_blacklist: "Elenco delle parole chiave utilizzate per mettere in blacklist gli allegati in base al tipo di contenuto." attachment_filename_blacklist: "Elenco delle parole chiave utilizzate per mettere in blacklist gli allegati in base al nome del file." enable_forwarded_emails: "[BETA] Consenti agli utenti di creare un argomento inviando una email." @@ -1205,22 +1217,85 @@ it: allow_animated_thumbnails: "Genera miniature animate delle gif animate." default_avatars: "URL degli avatar che verranno utilizzati come predefiniti per i nuovi utenti, fintanto che non li cambieranno esplicitamente." automatically_download_gravatars: "Scarica i Gravatars per gli utenti quando viene creato l'account o quando viene modificata l'email" + digest_topics: "Numero massimo di argomenti di successo da mostrare nell'email riepilogativa." + digest_posts: "Numero massimo dei messaggi di successo da mostrare nell'email riepilogativa." + digest_min_excerpt_length: "Lunghezza minima dell'estratto del messaggio nell'email riepilogativa, in caratteri." + suppress_digest_email_after_days: "Sopprimi le email di riepilogo per gli utenti che non visitano il sito per più di (n) giorni." + digest_suppress_categories: "Sopprimi queste categorie dalle email riepilogative." + disable_digest_emails: "Disabilita le email riepilogative per tutti gli utenti." + email_accent_bg_color: "Colore da usare come background per alcuni elementi delle email HTML. Inserire il nome di un colore ('red') o un valore esadecimale ('#FF000')." + email_link_color: "Colore dei collegamenti nelle email HTML. Inserire il nome di un colore ('blue') o un valore esadecimale ('#0000FF')." + max_daily_gravatar_crawls: "Numero massimo di volte al giorno in cui Discourse controllerà Gravatar per gli avatar personalizzati" + public_user_custom_fields: "Una whitelist di campi personalizzati di un utente che possono essere mostrati pubblicamente." + staff_user_custom_fields: "Una whitelist di campi personalizzati di un utente che possono essere mostrati allo staff." + enable_user_directory: "Fornire una directory degli utenti per la navigazione" + enable_group_directory: "Fornire una directory dei gruppi per la navigazione" + allow_anonymous_posting: "Permetti agli utenti di passare alla modalità anonima" + anonymous_posting_min_trust_level: "Livello di esperienza minimo richiesto per abilitare la pubblicazione in modalità anonima" + hide_user_profiles_from_public: "Disabilita schede utenti, profili utenti e directory utenti per gli utenti anonimi." allow_profile_backgrounds: "Permetti agli utenti di caricare immagini di sfondo per il profilo." + sequential_replies_threshold: "Numero di messaggi che un utente deve pubblicare di fila in un argomento prima che gli venga ricordato delle troppe risposte sequenziali." + get_a_room_threshold: "Numero di messaggi che un utente deve inviare alla stessa persona nello stesso argomento prima di essere avvertito." enable_mobile_theme: "I dispositivi mobili usano un tema apposito, con possibilità di passare alla visualizzazione completa. Disabilita questa opzione se vuoi usare un foglio di stile personalizzato che sia completamente reattivo." + dominating_topic_minimum_percent: "Quale percentuale di messaggi un utente deve pubblicare in un argomento prima di essere avvertito che sta eccessivamente dominando l'argomento." + disable_avatar_education_message: "Disabilita il messaggio educativo per cambiare avatar" suppress_uncategorized_badge: "Non mostrare il distintivo per gli argomenti senza categoria nell'elenco degli argomenti." global_notice: "Mostra a tutti i visitatori un avviso con striscione globale URGENTE, EMERGENZA, lascia vuoto per nasconderlo (HTML consentito)." + disable_edit_notifications: "Disabilita le notifiche di modifica dall'utente system quando 'download_remote_images_to_local' è attivo." + automatically_unpin_topics: " Spunta automaticamente gli argomenti quando l'utente arriva in fondo. " + read_time_word_count: "Conteggio di parole al minuto per calcolare il tempo stimato di lettura." + native_app_install_banner: "Domanda ai visitatori ricorrenti di installare l'app nativa di Discourse." + share_anonymized_statistics: "Condividi statistiche anonime di utilizzo." full_name_required: "Il nome completo è un campo obbligatorio del profilo utente." + enable_names: "Mostra il nome completo dell'utente su profilo, scheda utente e email. Disabilita per nascondere il nome completo ovunque." + display_name_on_posts: "Mostra il nome completo dell'utente sui messaggi oltre allo @username." short_progress_text_threshold: "Quando il numero di messaggi di un argomento supera questo valore, la barra di avanzamento mostrerà solo il numero di messaggi attuale. Se modifichi l'ampiezza della barra di avanzamento, potresti dover cambiare questo valore." default_code_lang: "Ai blocchi di codice GitHub viene applicata l'evidenziazione della sintassi del linguaggio di programmazione di default (lang-auto, ruby, python ecc.)" + warn_reviving_old_topic_age: "Quando qualcuno inizia a rispondere ad un argomento in cui l'ultima risposta è più vecchia di tale numero di giorni, verrà visualizzato un avviso. Disabilita impostando su 0." + autohighlight_all_code: "Forza l'evidenziazione della sintassi del codice nei blocchi di testo preformattato, anche se non è stato specificato il linguaggio di programmazione. " + highlighted_languages: "Regole di evidenziazione della sintassi incluse. (Avviso: includere troppe lingue può influire sulle prestazioni) vedi: https://highlightjs.org/static/demo/ per una dimostrazione" embed_by_username: "Nome utente Discourse dell'utente che crea gli argomenti incorporati." embed_truncate: "Tronca i messaggi incorporati." + allowed_href_schemes: "Schemi consentiti nei collegamenti in aggiunta a http e https." embed_post_limit: "Numero massimo di messaggi da incorporare." + embed_username_required: "E' richiesto il nome utente per la creazione di argomenti." + embed_whitelist_selector: "Selettore CSS per gli elementi da permettere negli embed." + embed_blacklist_selector: "Selettore CSS per gli elementi che sono rimossi dagli embed." + notify_about_flags_after: "Se ci sono segnalazioni che non sono state revisionate dopo un tale numero di ore, invia un messaggio privato allo staff. Imposta a 0 per disattivare. " show_create_topics_notice: "Se il sito ha meno di 5 argomenti pubblici, mostra un avviso chiedendo agli amministratori di creare qualche argomento." + delete_drafts_older_than_n_days: Elimina bozze più vecchie di (n) giorni. + bootstrap_mode_min_users: "Numero minimo di utenti richiesto per disabilitare la modalità bootstrap (imposta a 0 per disattivare)" + prevent_anons_from_downloading_files: "Impedire agli utenti anonimi di scaricare gli allegati. ATTENZIONE: questo impedirà a qualsiasi risorsa del sito pubblicata come allegato di funzionare ad eccezione delle immagini." enable_emoji: "Attiva gli emoji" + enforce_square_emoji: "Forza tutte le emoji ad un aspetto di proporzioni quadrate." + approve_post_count: "La quantità di messaggi di un utente nuovo o di base che deve essere approvata" + approve_unless_trust_level: "I messaggi degli utenti al di sotto di questo livello di esperienza devono essere approvati" + approve_new_topics_unless_trust_level: "I nuovi argomenti degli utenti al di sotto di questo livello di esperienza devono essere approvati" + notify_about_queued_posts_after: "Se ci sono messaggi che devono essere revisionati dopo un tale numero di ore, una email verrà inviata a contact_email. Imposta a 0 per disattivare queste email." + auto_close_messages_post_count: "Numero massimo di messaggi consentiti in un messaggio privato prima di essere automaticamente chiuso (0 per disabilitare)" + auto_close_topics_post_count: "Numero massimo di messaggi consentiti in un argomento prima di essere automaticamente chiuso (0 per disabilitare)" + max_allowed_message_recipients: "Numero massimo di destinatari consentiti in un messaggio privato." + default_email_digest_frequency: "Con quale frequenza gli utenti ricevono email riepilogative di default." + default_email_private_messages: " Invia una email quando qualcuno scrive un messaggio ad un utente di default." + default_email_direct: "Invia una email quando qualcuno cita/risponde/menziona un utente di default." + default_email_mailing_list_mode: "Invia una email per ogni nuovo messaggio di default." + default_email_mailing_list_mode_frequency: "Con quale frequenza gli utenti che hanno attivato la modalità mailing list riceveranno email di default." + disable_mailing_list_mode: "Impedisci agli utenti di attivare la modalità mailing list." + default_email_always: "Invia una notifica via email anche quando l'utente è attivo di default." + default_email_previous_replies: "Includi risposte precedenti nelle email di default." + default_email_in_reply_to: "Includi un estratto della risposta ad un messaggio nelle email di default." + default_other_new_topic_duration_minutes: "Condizione globale predefinita per cui un argomento viene considerato nuovo." + default_other_auto_track_topics_after_msecs: "Tempo globale predefinito prima che un argomento venga automaticamente seguito." + default_other_notification_level_when_replying: "Livello di notifica globale predefinito quando un utente risponde ad un argomento." + default_other_external_links_in_new_tab: "Apri i collegamenti esterni in una nuova scheda di default. " + default_other_enable_quoting: "Attiva rispondi citando per il testo evidenziato di default." default_other_disable_jump_reply: "Per default non saltare al messaggio dell'utente dopo la sua risposta." default_categories_watching: "Elenco di categorie osservate per default." default_categories_tracking: "Elenco di categorie seguite per default." default_categories_muted: "Elenco di categorie silenziate per default." + max_user_api_reqs_per_day: "Numero massimo di richieste giornaliere per la chiave API utente " + max_user_api_reqs_per_minute: "Numero massimo di richieste al minuto per la chiave API utente" + max_api_keys_per_user: "Numero massimo di chiavi API utente per utente" tagging_enabled: "Abilitare le etichette per gli argomenti?" min_trust_to_create_tag: "Il livello di esperienza minimo per creare un'etichetta." max_tags_per_topic: "Il numero massimo di etichette che si possono applicare ad un argomento." @@ -1379,10 +1454,31 @@ it: welcome_invite: subject_template: "Benvenuto su %{site_name}!" backup_succeeded: + title: "Backup Riuscito" subject_template: "Backup completato correttamente" + text_body_template: | + Il backup è avvenuto con successo. + + Visita la [sezione admin > backup](%{base_url}/admin/backups) per scaricare il nuovo backup. + + Ecco il log: + + ```text + %{logs} + ``` backup_failed: + title: "Backup Fallito" subject_template: "Backup fallito" + text_body_template: | + Il backup è fallito. + + Ecco il log: + + ```text + %{logs} + ``` restore_succeeded: + title: "Ripristino Riuscito" subject_template: "Il ripristino è avvenuto con successo." text_body_template: | Il ripristino è avvenuto con successo. @@ -1393,6 +1489,7 @@ it: %{logs} ``` restore_failed: + title: "Ripristino Fallito" subject_template: "Ripristino fallito" text_body_template: | Il ripristino è fallito. @@ -1403,24 +1500,27 @@ it: %{logs} ``` bulk_invite_succeeded: + title: "Invito di Massa Riuscito" subject_template: "Invito di massa processato con successo" text_body_template: "Il tuo file con l'invito di massa è stato processato, %{sent} inviti spediti." bulk_invite_failed: + title: "Invito di Massa Fallito" subject_template: "Invito di massa processato con errori" csv_export_succeeded: + title: "Esportazione CSV Riuscita" subject_template: "Esportazione dati completa" + text_body_template: | + L'esportazione dei tuoi dati è avvenuta con successo! :dvd: + + %{file_name} (%{file_size}) + + Il collegamento per il download qui sopra sarà valido per 48 ore. + + I dati vengono compressi come archivio gzip. Se l'archivio non si auto-estrae quando lo si apre, utilizzare gli strumenti consigliati qui: http://www.gzip.org/#faq4 csv_export_failed: + title: "Esportazione CSV Fallita" subject_template: "Esportazione dati fallita" - email_reject_screened_email: - text_body_template: | - Spiacenti, il tuo messaggio email a %{destination} (intitolato %{former_title}) non ha funzionato. - - La tua risposta è stata inviata da un indirizzo email bloccato. Prova a spedire da un altro indirizzo email, oppure contatta un membro dello staff. - email_reject_no_account: - text_body_template: | - Ci dispiace, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. - - Non esiste un account con quell'indirizzo email. Prova ad inviarla a un altro indirizzo o contatta un membro dello staff. + text_body_template: "Siamo spiacenti, ma l'esportazione dei dati è fallita. Per favore controlla i log o [contatta un membro dello staff](%{base_url}/about)." email_reject_bad_destination_address: text_body_template: |+ Spiacenti, ma il tuo messaggio email a %{destination} (intitolato %{former_title}) non è stato inviato. @@ -1439,15 +1539,58 @@ it: Per ulteriori informazioni, ti rimandiamo alle [linee guida della comunità](%{base_url}/guidelines). too_many_tl3_flags: subject_template: "Nuovo account sospeso" + text_body_template: | + Ciao, + + questo è un messaggio automatico da %{site_name} per informarti che il tuo account è stato bloccato a causa di un gran numero di segnalazioni da parte della comunità. + + Come misura precauzionale, il tuo nuovo account non può creare altre risposte o argomenti finché il tuo account non verrà revisionato da un membro dello staff. Ci scusiamo per l'inconveniente + + Per maggiori dettagli, fai riferimento alle [linee guida della comunità](%{base_url}/guidelines). blocked_by_staff: + title: "Bloccato dallo Staff" subject_template: "Account temporaneamente sospeso" + text_body_template: | + Ciao, + + questo è un messaggio automatico da %{site_name} per informarti che il tuo account è stato temporaneamente bloccato come misura precauzionale. + + Per favore continua pure a navigare, anche se non potrai rispondere o creare argomenti fino a che un [membro dello staff](%{base_url}/about) avrà revisionato i tuoi messaggi più recenti. Ci scusiamo per l'inconveniente. + + Per maggiori dettagli, fai riferimento alle [linee guida della comunità](%{base_url}/guidelines). user_automatically_blocked: + title: "Utente Bloccato Automaticamente" subject_template: "Nuovo utente %{username} bloccato dalle segnalazioni della comunità" + text_body_template: | + Questo è un messaggio automatico. + + Il nuovo utente [%{username}](%{user_url}) è stato automaticamente bloccato perché più utenti hanno contrassegnato i messaggi di %{username}. + + Per favore [controlla le segnalazioni](%{base_url}/admin/flags). Se %{username} è stato bloccato per errore, clicca sul tasto di sblocco nella [pagina di amministrazione di questo utente](%{user_url}). + + Questa impostazione può essere modificata tramite l'impostazione del sito `block_new_user`. spam_post_blocked: + title: "Messaggio Spam Bloccato" subject_template: "Il messaggi del nuovo utente %{username} sono stati bloccati a causa dell'invio di ripetuti collegamenti." + text_body_template: | + Questo è un messaggio automatico. + + Il nuovo utente [%{username}](%{user_url}) ha creato diversi messaggi con collegamenti a %{domains}, ma tali messaggi sono stati bloccati per evitare spam. L'utente è ancora abilitato alla creazione di messaggi ma senza collegamenti verso %{domains}. + + Per favore [controlla questo utente](%{user_url}). + + Questa impostazione può essere modificata tramite le impostazioni del sito `newuser_spam_host_threshold` e `white_listed_spam_host_domains`. unblocked: + title: "Sbloccato" subject_template: "Account non più sospeso" + text_body_template: | + Ciao, + + questo è un messaggio automatico da %{site_name} per informarti che il tuo account è stato sbloccato a seguito di revisione da parte dello staff. + + Ora puoi nuovamente creare argomenti e risposte. Grazie della pazienza. pending_users_reminder: + title: "Promemoria Utenti in Sospeso" subject_template: one: "1 utente in attesa di approvazione" other: "%{count} utenti in attesa di approvazione" @@ -1456,10 +1599,33 @@ it: [Per favore valutale nella sezione di amministrazione](%{base_url}/admin/users/list/pending). download_remote_images_disabled: + title: "Scaricamento Immagini Remote Disabilitato" subject_template: "Lo scaricamento delle immagini remote è disabilitato" text_body_template: "L'impostazione `download_remote_images_to_local` è stata disabilitata perché è stato raggiunto il limite di spazio su disco definito in `download_remote_images_threshold`." dashboard_problems: + title: "Problemi Cruscotto" subject_template: "Si sono riscontrati problemi" + text_body_template: | + Alcuni problemi sono stati riportati sul tuo cruscotto di amministratore. + + [Per favore esaminali e risolvili](%{base_url}/admin). + new_user_of_the_month: + title: "Sei il Nuovo Utente del Mese!" + subject_template: "Sei il Nuovo Utente del Mese!" + text_body_template: | + Congratulazioni, ti sei meritato il premio come **Nuovo Utente del Mese per %{month_year}**. :trophy: + + Questo premio viene assegnato solo a due nuovi utenti al mese e sarà visibile permanentemente sulla [tua pagina utente](%{base_url}/my/badges). + + Sei diventato velocemente un membro prezioso della nostra comunità. Grazie per l'adesione e continua a fare un buon lavoro! + unsubscribe_link: | + Per annullare l'iscrizione a queste email, [clicca qui](%{unsubscribe_url}). + unsubscribe_link_and_mail: | + Per annullare l'iscrizione a queste email, [clicca qui](%{unsubscribe_url}). + unsubscribe_mailing_list: | + Stai ricevendo questa email perchè hai abilitato la modalità mailing list. + + Per annullare l'iscrizione a queste email, [clicca qui](%{unsubscribe_url}). subject_re: "R:" subject_pm: "[MP]" user_notifications: @@ -1474,7 +1640,82 @@ it: visit_link_to_respond: "[Visita Argomento](%{base_url}%{url}) per replicare." visit_link_to_respond_pm: "[Visita Messaggio](%{base_url}%{url}) per replicare." posted_by: "Pubblicato da %{username} il %{post_date}" + invited_group_to_private_message_body: | + %{username} ha invitato @%{group_name} a partecipare al messaggio + + > #### %{topic_title} + > + > %{topic_excerpt} + + su + + > %{site_title} -- %{site_description} + invited_to_private_message_body: | + %{username} ti ha invitato a partecipare al messaggio + + > #### %{topic_title} + > + > %{topic_excerpt} + + su + + > %{site_title} -- %{site_description} + invited_to_topic_body: | + %{username} ti ha invitato alla discussione + + > #### %{topic_title} + > + > %{topic_excerpt} + + su + + > %{site_title} -- %{site_description} + user_invited_to_private_message_pm_group: + subject_template: "[%{email_prefix}] %{username} ha invitato @%{group_name} a partecipare al messaggio '%{topic_title}'" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_private_message_pm: + title: "Utente Invitato ad un MP" + subject_template: "[%{email_prefix}] %{username} ti ha invitato a partecipare al messaggio '%{topic_title}'" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_private_message_pm_staged: + subject_template: "[%{email_prefix}] %{username} ti ha invitato a partecipare al messaggio '%{topic_title}'" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_topic: + title: "Utente Invitato ad un Argomento" + subject_template: "[%{email_prefix}] %{username} ti ha invitato a '%{topic_title}'" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_replied: + subject_template: "[%{email_prefix}] %{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} user_replied_pm: + subject_template: "[%{email_prefix}][MP]%{topic_title}" text_body_template: | %{header_instructions} @@ -1484,6 +1725,8 @@ it: %{respond_instructions} user_quoted: + title: "Utente Citato" + subject_template: "[%{email_prefix}]%{topic_title}" text_body_template: | %{header_instructions} @@ -1493,6 +1736,7 @@ it: %{respond_instructions} user_linked: + subject_template: "[%{email_prefix}]%{topic_title}" text_body_template: | %{header_instructions} @@ -1502,6 +1746,8 @@ it: %{respond_instructions} user_mentioned: + title: "Utente Menzionato" + subject_template: "[%{email_prefix}]%{topic_title}" text_body_template: | %{header_instructions} @@ -1511,6 +1757,7 @@ it: %{respond_instructions} user_group_mentioned: + subject_template: "[%{email_prefix}]%{topic_title}" text_body_template: | %{header_instructions} @@ -1520,6 +1767,7 @@ it: %{respond_instructions} user_posted: + subject_template: "[%{email_prefix}]%{topic_title}" text_body_template: | %{header_instructions} @@ -1529,6 +1777,7 @@ it: %{respond_instructions} user_watching_first_post: + subject_template: "[%{email_prefix}]%{topic_title}" text_body_template: | %{header_instructions} @@ -1538,6 +1787,8 @@ it: %{respond_instructions} user_posted_pm: + title: "Pubblicazione MP da Utente" + subject_template: "[%{email_prefix}] [MP] %{topic_title}" text_body_template: | %{header_instructions} @@ -1547,20 +1798,124 @@ it: %{respond_instructions} user_posted_pm_staged: + title: "Pubblicazione MP da Utente Temporaneo" subject_template: "%{optional_re}%{topic_title}" text_body_template: |2 %{message} digest: why: "Un breve sommario di %{site_link} dalla tua ultima visita il %{last_seen_at}" + since_last_visit: "Dalla tua ultima visita" + new_topics: "Nuovi Argomenti" + unread_messages: "Messaggi Non Letti" + unread_notifications: "Notifiche Non Lette" liked_received: "Like ricevuti" + new_posts: "Nuovi Messaggi" new_users: "Nuovi utenti" + popular_topics: "Argomenti Di Successo" follow_topic: "Segui questo topic" + join_the_discussion: "Leggi Altro" + popular_posts: "Messaggi di Successo" + more_new: "Nuovo per te" + subject_template: "[%{email_prefix}] Riepilogo" + unsubscribe: "Questo riepilogo viene inviato da %{site_link} se non ti si vede da un po'. Modifica le tue impostazioni email, o %{unsubscribe_link} annulla l'iscrizione." click_here: "clicca qui" from: "%{site_name} riepilogo" preheader: "Un breve riepilogo dall'ultima tua visita il %{last_seen_at}" + forgot_password: + title: "Password Dimenticata" + subject_template: "[%{email_prefix}] Reimposta password" + text_body_template: | + Qualcuno ha richiesto l'azzeramento della tua password su [%{site_name}](%{base_url}). + + Se non sei stato tu, puoi tranquillamente ignorare questa email. + + Clicca il seguente collegamento per scegliere una nuova password: + %{base_url}/u/password-reset/%{email_token} + set_password: + title: "Imposta Password" + subject_template: "[%{email_prefix}] Imposta Password" + text_body_template: | + Qualcuno ha richiesto di aggiungere una password al tuo account su [%{site_name}](%{base_url}). In alternativa, puoi collegarti usando uno dei servizi online supportati (Google, Facebook, ecc) associato con questo indirizzo email validato. + + Se non hai fatto tu questa richiesta, ignora tranquillamente questa email. + + Clicca sul seguente collegamento per scegliere una password: + %{base_url}/u/password-reset/%{email_token} + admin_login: + title: "Login Amministratore" + subject_template: "[%{email_prefix}] Login" + text_body_template: | + Qualcuno ha richiesto di connettersi con il tuo account su [%{site_name}](%{base_url}). + + Se non sei stato tu a fare questa richiesta, puoi tranquillamente ignorare questa email. + + Clicca sul seguente collegamento per connetterti: + %{base_url}/u/admin-login/%{email_token} + account_created: + title: "Account Creato" + subject_template: "[%{email_prefix}] Il Tuo Nuovo Account" + text_body_template: | + E' stato creato un nuovo account per te su %{site_name} + + Clicca sul seguente collegamento per scegliere una password per il tuo nuovo account: + %{base_url}/u/password-reset/%{email_token} + confirm_new_email: + title: "Conferma Nuova Email" + subject_template: "[%{email_prefix}] Conferma il tuo nuovo indizzo email" + text_body_template: | + Conferma il tuo nuovo indirizzo email su %{site_name} cliccando il seguente collegamento: + + %{base_url}/u/authorize-email/%{email_token} + confirm_old_email: + title: "Conferma Vecchia Email" + subject_template: "[%{email_prefix}] Conferma il tuo attuale indirizzo email" + text_body_template: | + Prima di poter cambiare il tuo indirizzo email abbiamo bisogno che tu confermi di controllare + l'indirizzo email attuale. Dopo aver completato questo passaggio, dovrai confermare + il nuovo indirizzo email. + + Conferma il tuo attuale indirizzo email su %{site_name} cliccando il seguente collegamento: + + %{base_url}/u/authorize-email/%{email_token} + notify_old_email: + title: "Notifica Vecchia Email" + subject_template: "[%{email_prefix}] Il tuo indirizzo email è stato cambiato" + text_body_template: | + Questo è un messaggio automatico per farti sapere che il tuo indirizzo email per + %{site_name} è stato cambiato. Se è stato fatto per errore, per favore contatta un amministratore del sito. + + Il tuo indirizzo email è stato cambiato in: + + %{new_email} signup_after_approval: + title: "Iscrizione Dopo Approvazione" subject_template: "Sei stato ammesso su %{site_name}!" + text_body_template: | + Benvenuto su %{site_name}! + + Un membro dello staff ha approvato il tuo account su %{site_name}. + + Clicca sul seguente collegamento per confermare e attivare il tuo nuovo account: + %{base_url}/u/activate-account/%{email_token} + + Se il collegamento qui sopra non è cliccabile, prova a copiarlo e incollarlo nella barra degli indirizzi del tuo browser. + + %{new_user_tips} + + Noi crediamo da sempre in un [comportamento comunitario civile](%{base_url}/guidelines). + + Buona permanenza! + signup: + title: "Iscrizione" + subject_template: "[%{email_prefix}] Conferma il tuo nuovo account" + text_body_template: | + Benvenuto su %{site_name}! + + Clicca sul collegamento seguente per confermare ed attivare il tuo nuovo account: + %{base_url}/u/activate-account/%{email_token} + + Se il collegamento non è cliccabile, copialo ed incollalo nella barra degli indirizzi del tuo browser. page_not_found: title: "La pagina richiesta non esiste oppure è privata." popular_topics: "Di successo" @@ -1570,24 +1925,33 @@ it: search_google: "Google" login_required: welcome_message: | - #[Benvenuto su %{title}](#welcome) + ## [Benvenuto su %{title}](#welcome) E' richiesto un account. Per continuare, crea un nuovo account oppure connettiti. terms_of_service: title: "Termini di Servizio" signup_form_message: 'Ho letto e accetto i Termini del Servizio.' deleted: 'cancellati' + image: "immagine" upload: edit_reason: "copie locali delle immagini scaricate" unauthorized: "Spiacenti, il file che stai cercando di caricare non è autorizzato (estensioni autorizzate: %{authorized_extensions})." pasted_image_filename: "Immagine incollata" store_failure: "Caricamento n°%{upload_id} fallito per l'utente n°%{user_id}." file_missing: "Spiacenti, devi fornire un file da caricare." + empty: "Spiacenti, ma il file che hai fornito è vuoto." attachments: too_large: "Spiacenti, ma il file che stai cercando di caricare è troppo grande (la dimensione massima è %{max_size_kb}KB)." images: too_large: "Spiacenti, ma l'immagine che stai cercando di caricare è troppo grande (la dimensione massima è %{max_size_kb}KB), per favore ridimensionala e riprova." + larger_than_x_megapixels: "Spiacenti, ma l'immagine che stai cercando di caricare è troppo grande (la dimensione massima è %{max_image_megapixels}-megapixel), per favore ridimensionala e riprova." size_not_found: "Spiacenti, ma non riusciamo a determinare la dimensione dell'immagine. E' forse danneggiata?" + avatar: + missing: "Spiacenti, non troviamo nessun avatar associato a questo indirizzo email. Puoi provare a caricarlo nuovamente?" + flag_reason: + sockpuppet: "Un nuovo utente ha creato un argomento, e un altro nuovo utente ha risposto dallo stesso indirizzo IP (%{ip_address}). Vedi l'impostazione del sito `flag_sockpuppets`." + spam_hosts: "Questo nuovo utente ha provato a creare più messaggi con collegamenti allo stesso dominio (%{domain}). Vedi l'impostazione del sito `newuser_spam_host_threshold`." email_log: + post_user_deleted: "L'utente del messaggio è stato eliminato." no_user: "Non trovo l'utente con l'id %{user_id}" anonymous_user: "L'utente è anonimo" suspended_not_pm: "L'utente è sospeso, non è un messaggio" @@ -1598,12 +1962,20 @@ it: post_deleted: "il messaggio è stato cancellato dal suo autore" user_suspended: "l'utente è stato sospeso" already_read: "l'utente ha già letto questo messaggio" + exceeded_emails_limit: "Ha superato max_emails_per_day_per_user" + exceeded_bounces_limit: "Ha superato bounce_score_threshold" message_blank: "il messaggio è vuoto" message_to_blank: "message.to è vuoto" text_part_body_blank: "text_part.body è vuoto" body_blank: "il corpo è vuoto" + no_echo_mailing_list_mode: "Notifiche della mailing list disattivate per i propri messaggi" color_schemes: base_theme_name: "Base" + default: "Combinazione Chiara" + dark: "Combinazione Scura" + default_theme_name: "Predefinito" + dark_theme_name: "Scuro" + light_theme_name: "Chiaro" about: "Informazioni su" guidelines: "Linee Guida" privacy: "Privacy" @@ -1643,63 +2015,269 @@ it: leader: name: Veterano description: Assegnato modifica globale, puntare, chiudere, archiviare, suddividere e riunire, maggiori "Mi piace" + long_description: | + Questo distintivo è assegnato quando si raggiunge il livello di esperienza 4. Sei un veterano in questa comunità selezionato dallo staff, e qui sei un esempio positivo per il resto della comunità con le tue azioni e parole. Hai la possibilità di modificare tutti i messaggi, agire come un moderatore con azioni comuni quali puntare, chiudere, rendere invisibile, archiviare, dividere e unire, e hai tonnellate di mi piace al giorno. welcome: name: Benvenuto description: Ha ricevuto un "Mi piace" + long_description: | + Questo distintivo è assegnato quando ricevi il tuo primo mi piace su un messaggio. Congratulazioni, hai pubblicato qualcosa che i membri della tua comunità hanno trovato interessante, eccezionale o utile! autobiographer: name: Autobiografo + description: Ha compilato le informazioni sul profilo + long_description: | + Questo distintivo è assegnato per aver compilato il tuo profilo utente e selezionato un'immagine del profilo. Lasciare che la comunità sappia di più su chi sei e a cosa sei interessato rende la comunità migliore e più connessa. Unisciti a noi! anniversary: name: Compleanno description: Membro attivo per un anno, ha scritto almeno una volta long_description: | Questo distintivo è assegnato a chi è membro da un anno con almeno un messaggio scritto durante quell'anno. Grazie per esserci e contribuire alla nostra comunità. Non potremmo farcela senza di te. + nice_post: + name: Risposta Piacevole + description: Ha ricevuto 10 mi piace su una risposta + long_description: | + Questo distintivo è assegnato quando una risposta riceve 10 mi piace. La tua risposta ha fatto davvero impressione sulla comunità e ha aiutato a portare la conversazione avanti! + good_post: + name: Buona Risposta + description: Ha ricevuto 25 mi piace su una risposta + long_description: | + Questo distintivo è assegnato quando una risposta riceve 25 mi piace. La tua risposta è stata eccezionale e ha reso la conversazione migliore per tutti! + great_post: + name: Grande Risposta + description: Ha ricevuto 50 mi piace su una risposta + long_description: | + Questo distintivo è assegnato quando una risposta riceve 50 mi piace. Wow! La tua risposta è stata ispiratrice, affascinante, divertente, o penetrante e la comunità l'ha amata. + nice_topic: + name: Argomento Piacevole + description: Ha ricevuto 10 mi piace su un argomento + long_description: | + Questo distintivo è assegnato quando un argomento riceve 10 mi piace. Hey, hai iniziato una conversazione interessante che la comunità ha apprezzato! + good_topic: + name: Buon Argomento + description: Ha ricevuto 25 mi piace su un argomento + long_description: | + Questo distintivo è assegnato quando il tuo argomento riceve 25 mi piace. Hai lanciato una conversazione vibrante che ha radunato la comunità! great_topic: + name: Grande Argomento description: Ha ricevuto 50 "Mi piace" in un argomento + long_description: | + Questo distintivo è assegnato quando un argomento riceve 50 mi piace. Hai avviato una conversazione affascinante e la comunità ha apprezzato la discussione dinamica che è seguita! + nice_share: + name: Piacevole Condivisione + description: Ha condiviso un messaggio con 25 visitatori unici + long_description: | + Questo distintivo è assegnato per aver condiviso un collegamento che è stato cliccato da 25 visitatori esterni. Grazie per diffondere la parola circa le nostre discussioni e questa comunità. + good_share: + name: Buona Condivisione + description: Ha condiviso un messaggio con 300 visitatori unici + long_description: | + Questo distintivo è assegnato per aver condiviso un collegamento che è stato cliccato da 300 visitatori esterni. Buon lavoro! Hai mostrato una grande discussione ad un gruppo di persone nuove e hai aiutato questa comunità a crescere. + great_share: + name: Grande Condivisione + description: Ha condiviso un messaggio con 1000 visitatori unici + long_description: | + Questo distintivo è assegnato per aver condiviso un collegamento che è stato cliccato da 1000 visitatori esterni. Wow! Hai promosso un'interessante discussione ad un pubblico enorme e ci hai aiutato a far crescere la nostra comunità in grande stile! first_like: name: Primo "Mi piace" description: Ha messo "Mi piace" a un messaggio + long_description: | + Questo distintivo è assegnato la prima volta che metti mi piace ad un messaggio usando il pulsante :heart:. Mettere mi piace a dei messaggi è un ottimo modo per far sapere a tutti i membri della tua comunità che ciò che hanno pubblicato è stato interessante, utile, forte o divertente. Condividi l'amore! first_flag: name: Prima Segnalazione description: Segnalato un messaggio + long_description: | + Questo distintivo è assegnato la prima volta che segnali un messaggio. La segnalazione serve per contribuire a mantenere questo luogo pulito e piacevole per tutti. Se noti dei messaggi che richiedono l'attenzione di un moderatore per qualsiasi motivo, non esitate a contrassegnarli. Puoi anche contrassegnare per inviare messaggi personali ad altri utenti se vedi un problema con i loro messaggi. Se vedi un problema, :flag_black: segnalalo! + promoter: + name: Promotore + description: Ha invitato un utente + long_description: | + Questo distintivo è assegnato quando si invita qualcuno ad unirsi alla comunità tramite il pulsante invita sulla tua pagina utente, o in fondo ad un argomento. Invitare amici che potrebbero essere interessati a discussioni specifiche è un ottimo modo per introdurre nuove persone alla nostra comunità, quindi grazie! + campaigner: + name: Attivista + description: Ha invitato 3 utenti + long_description: | + Questo distintivo è assegnato quando hai invitato 3 persone che hanno successivamente trascorso abbastanza tempo sul sito per diventare utenti base. Una comunità vivace ha bisogno di un'infusione regolare di nuovi arrivati che partecipano regolarmente e aggiungono nuove voci alle conversazioni. + champion: + name: Campione + description: Ha invitato 5 membri + long_description: | + Questo distintivo è assegnato quando hai invitato 5 persone che hanno successivamente trascorso abbastanza tempo sul sito per diventare membri a tutti gli effetti. Wow! Grazie per aver ampliato la diversità della nostra comunità con nuovi membri! first_share: name: Prima Condivisione description: Ha condiviso un messaggio + long_description: | + Questo distintivo è assegnato la prima volta che condividi un collegamento ad una risposta o ad un argomento usando il pulsante condividi. Condividere collegamenti è un ottimo modo per mostrare interessanti discussioni al resto del mondo e far crescere la tua comunità. first_link: name: Primo Collegamento + description: Ha aggiunto un collegamento ad un altro argomento + long_description: | + Questo distintivo è assegnato la prima volta che aggiungi un collegamento ad un altro argomento. Collegare argomenti aiuta i tuoi colleghi lettori a trovare interessanti conversazioni correlate, mostrando le connessioni tra gli argomenti in entrambe le direzioni. Collega liberamente! first_quote: + name: Prima Citazione description: Ha citato un messaggio + long_description: | + Questo distintivo è assegnato la prima volta che citi un messaggio nella tua risposta. Citare le sezioni rilevanti di messaggi precedenti nella tua risposta aiuta a mantenere le discussioni connesse insieme e in argomento. Il modo più semplice per citare è evidenziare una sezione di un messaggio e quindi premere il pulsante rispondi. Cita generosamente! + read_guidelines: + name: Linee Guida + description: Ha letto le linee guida della comunità + long_description: | + Questo distintivo è assegnato per aver letto le linee guida della comunità. Seguendo e condividendo queste semplici linee guida aiuti a costruire una comunità sicura, divertente e sostenibile per tutti. Ricorda sempre che c'è un altro essere umano, molto simile a te, dall'altro lato dello schermo. Sii piacevole! + reader: + name: Lettore + description: Ha letto ogni risposta in un argomento con più di 100 risposte + long_description: | + Questo distintivo è assegnato la prima volta che leggi un argomento lungo con più di 100 risposte. Leggere una conversazione attentamente ti aiuta a seguire la discussione, a comprendere punti di vista diversi, e conduce a conversazioni ancora più interessanti. Quanto più si legge, tanto è migliore la conversazione. Come ci piace dire, la Lettura è Fondamentale! :slight_smile: + popular_link: + name: Collegamento Di Successo + description: Ha pubblicato un collegamento esterno con 50 click + long_description: | + Questo distintivo è assegnato quando un collegamento che hai condiviso riceve 50 click. Grazie per aver pubblicato un collegamento utile che ha aggiunto un contesto interessante alla conversazione! + hot_link: + name: Collegamento Caldo + description: Ha pubblicato un collegamento esterno con 300 click + long_description: | + Questo distintivo è assegnato quando un collegamento che hai condiviso riceve 300 click. Grazie per aver pubblicato un collegamento affascinante che ha portato avanti la conversazione e ha illuminato la discussione! + famous_link: + name: Collegamento Famoso + description: Ha pubblicato un collegamento esterno con 1000 click + long_description: | + Questo distintivo è assegnato quando un collegamento condiviso riceve 1000 click. Wow! Hai inserito un link che ha migliorato significativamente la conversazione aggiungendo dettagli, contesto e informazioni essenziali. Ottimo lavoro! appreciated: + name: Apprezzato description: Ha ricevuto 1 "Mi piace" su 20 messaggi + long_description: | + Questo distintivo è assegnato quando ricevi almeno un mi piace su 20 messaggi differenti. La comunità si sta godendo i tuoi contributi alle conversazioni! respected: + name: Rispettato description: Ha ricevuto 2 "Mi piace" su 100 messaggi + long_description: | + Questo distintivo è assegnato quando ricevi almeno 2 mi piace su 100 messaggi differenti. La comunità sta imparando a rispettare i tuoi numerosi contributi alle conversazioni. admired: + name: Ammirato description: Ha ricevuto 5 "Mi piace" su 300 messaggi + long_description: | + Questo distintivo è assegnato quando ricevi almeno 5 mi piace su 300 messaggi differenti. Wow! La comunità ammira i tuoi contributi frequenti e di alta qualità nelle conversazioni. + out_of_love: + name: Amato + description: Ha messo 50 mi piace in un giorno + long_description: | + Questo distintivo è assegnato quando usi tutti i 50 mi piace che hai a disposizione giornalmente. Ricordando di prenderti un momento per mettere mi piace ai messaggi che ti piacciono e apprezzi, incoraggi i membri della tua comunità a creare discussioni ancora più grandi in futuro. + higher_love: + name: Molto Amato + description: Ha messo 50 mi piace al giorno per 5 volte + long_description: | + Questo distintivo è assegnato quando usi tutti i 50 mi piace giornalieri per 5 giorni. Grazie per aver dedicato del tempo ad incoraggiare le migliori conversazioni ogni giorno! + crazy_in_love: + name: Adorato + description: Ha messo 50 mi piace al giorno per 20 volte + long_description: | + Questo distintivo è assegnato quando usi tutti i 50 mi piace giornalieri per 20 giorni. Wow! Sei un modello di regolarità nel cercare di incoraggiare i membri della tua comunità! + thank_you: + name: Ringraziato + description: Ha ricevuto 20 mi piace sui messaggi e dato 10 mi piace + long_description: | + Questo distintivo è assegnato quando ricevi 20 mi piace sui messaggi e dai 10 o più mi piace in cambio. Quando qualcuno mette mi piace ai tuoi messaggi, troverai il tempo per mettere mi piace a ciò che altri scrivono. + gives_back: + name: Ricambiato + description: Ha ricevuto 100 mi piace sui messaggi e dato 100 mi piace + long_description: | + Questo distintivo è assegnato quando hai ricevuto 100 mi piace sui messaggi e hai dato 100 o più mi piace in cambio. Grazie per aver ricambiato! + empathetic: + name: Empatico + description: Ha ricevuto 500 mi piace sui messaggi e dato 1000 mi piace + long_description: | + Questo distintivo è assegnato quando hai ricevuto 500 mi piace sui messaggi e hai dato 1000 o più mi piace in cambio. Sei un modello di generosità e di apprezzamento reciproco : due_hearts :. first_emoji: + name: Prima Emoji + description: Ha usato una Emoji in un messaggio long_description: | Questo distintivo viene assegnato la prima volta che inserisci un Emoji in un messaggio :thumbsup:. Gli Emoji ti permettono di trasmettere emozioni nei messaggi, dalla felicità :smiley: alla tristezza :anguished: alla rabbia :angry: e tutto ciò che sta nel mezzo :sunglasses:. Digita semplicemente : (carattere due punti) oppure premi il bottone Emoji nella barra degli strumenti dell'editor, per selezionare tra centinaia di possibilità :ok_hand: + first_mention: + name: Prima Menzione + description: Ha menzionato un utente in un messaggio + long_description: Questo distintivo è assegnato la prima volta che menzioni lo @username di qualcuno nel tuo messaggio. Ogni menzione genera una notifica a quella persona, così verrà a conoscenza del tuo messaggio. Inizia a digitare @ (il simbolo) per menzionare qualsiasi utente o, se è consentito, un gruppo - è un modo conveniente per portare qualcosa alla loro attenzione. + first_onebox: + name: Prima Onebox + description: ha inserito un collegamento che ha generato una onebox + long_description: Questo distintivo è assegnato la prima volta che inserisci un collegamento su una riga da solo, il quale viene automaticamente espanso in un'anteprima con un breve riassunto del collegamento, un titolo e (quando disponibile) un'immagine. + first_reply_by_email: + name: Prima Risposta Da Email + description: Ha risposto ad un messaggio tramite email + long_description: | + Questo distintivo è assegnato la prima volta che rispondi ad un messaggio tramite email :e-mail:. + new_user_of_the_month: + name: "Nuovo Utente del Mese" + description: Contributi eccezionali nel loro primo mese + long_description: | + Questo distintivo è assegnato per congratularsi con due nuovi utenti ogni mese per i loro ottimi contributi globali, misurati sulla frequenza dei like ricevuti per i loro messaggi e da chi li ha dati. + badge_title_metadata: "%{display_name}distintivo su %{site_title}" admin_login: success: "email Inviata" error: "Errore!" email_input: "Email Amministratore" submit_button: "Invia Email" + performance_report: + initial_post_raw: Questo argomento include rapporti di rendimento giornalieri per il tuo sito. + initial_topic_title: Rapporti sul rendimento del sito web tags: title: "Etichette" + staff_tag_disallowed: "L'etichetta \"%{tag}\" può essere applicata solo dallo staff." staff_tag_remove_disallowed: "L'etichetta \"%{tag}\" può essere cancellata soltanto dallo staff." rss_by_tag: "Argomenti etichettati %{tag}" + finish_installation: + congratulations: "Congratulazioni, hai installato Discourse!" + register: + button: "Registrare" + title: "Registrare Account Amministratore" + help: "registrare un nuovo account per cominciare" + no_emails: "Purtroppo non è stata definita alcuna email di amministratore durante l'installazione, per cui la configurazione potrebbe risultare difficoltosa." + confirm_email: + title: "Conferma la tua Email" + message: "

    Abbiamo inviato una email di attivazione a %{email}. Segui le istruzioni nella email per attivare il tuo account.

    Se non arriva assicurati di aver impostato correttamente l'email per Discourse e di aver controllato la cartella di spam.

    " + resend_email: + title: "Rispedire Email di Attivazione" + message: "

    Abbiamo rispedito una email di attivazione a %{email}" + safe_mode: + title: "Modalità sicura" + description: "La modalità sicura ti consente di testare il tuo sito senza caricare plugin o personalizzazioni del sito." + no_customizations: "Disabilita il tema corrente" + only_official: "Disabilita i plugin non ufficiali" + no_plugins: "Disabilita tutti i plugin" + enter: "Avvia Modalità Sicura" wizard: + title: "Configurazione Discourse" step: + locale: + title: "Benvenuto sul tuo Discourse!" + fields: + default_locale: + description: "Qual è il linguaggio predefinito della tua comunità?" + forum_title: + title: "Nome" + description: "Il tuo nome è un segno visibile in lontananza, la prima cosa che i visitatori potenziali noteranno sulla tua comunità. Cosa dice il tuo nome e il tuo titolo sulla tua comunità?" + fields: + title: + label: "Il nome della tua comunità" + placeholder: "Il Ritrovo di Jane" + site_description: + label: "Descrivi la tua comunità in una breve frase" + placeholder: "Un posto per Jane e per i suoi amici dove discutere di cose forti" introduction: title: "Introduzione" fields: welcome: label: "Argomento di Benvenuto" + description: "

    Come descriveresti la tua comunità ad un estraneo su un ascensore in circa 1 minuto?

    • Per chi sono queste discussioni?
    • Cosa posso trovare qui?
    • Perché dovrei visitare il sito?

    Il tuo argomento di benvenuto è la prima cosa che i nuovi arrivati leggeranno. Pensalo come al tuo paragrafo 'discorso da ascensore' o 'dichiarazione di intenti'.

    " + one_paragraph: "Restringi il tuo messaggio di benvenuto a un paragrafo." privacy: title: "Accesso" + description: "

    La tua comunità è aperta a tutti o è limitata ai membri, agli inviti o all'approvazione? Se preferisci puoi impostare il sito come privato e poi passare a pubblico in seguito.

    Puoi sempre inviare inviti da argomenti o dalla tua pagina di profilo utente.

    " fields: privacy: choices: open: label: "Pubblico" + description: "Chiunque può accedere a questa comunità e registrare un account" restricted: label: "Privato" description: "Solo le persone che ho invitato o approvato possono accedere a questa comunità" @@ -1709,23 +2287,32 @@ it: contact_email: label: "Email" placeholder: "nome@esempio.com" + description: "Indirizzo email della persona o del gruppo responsabile di questa comunità. Utilizzato per notifiche critiche quali segnalazioni non gestite, aggiornamenti sulla protezione e sulla tua pagina informazioni per un contatto urgente." contact_url: label: "Sito Web" + placeholder: "http://www.example.com/contact-us" + description: "Pagina web di contatto generale per te o per la tua organizzazione. Sarà visualizzato sulla tua pagina informazioni." site_contact: label: "Messaggi Automatici" + description: "Tutti i messaggi personali automatici di Discourse saranno inviati da questo utente, ad esempio il messaggio di benvenuto inviato a ogni nuovo utente." corporate: title: "Società" + description: "Questi nomi verranno inseriti nelle Norme sulla Privacy e nei Termini di servizio, che sono argomenti che è possibile modificare nella categoria Staff. Se non hai un'azienda, non esitare a saltare questo passaggio per ora." fields: company_short_name: label: "Nome Azienda (breve)" + placeholder: "Initech" company_full_name: label: "Nome Azienda (completo)" + placeholder: "Initech, Inc." company_domain: label: "Nome Dominio Azienda" + placeholder: "initech.com" colors: title: "Tema" fields: theme_id: + description: "Preferisci iniziare una combinazione di colori chiara o scura? È possibile personalizzare ulteriormente l'aspetto del tuo sito tramite Amministrazione, Personalizza." choices: default: label: "Semplice Chiaro" @@ -1736,24 +2323,40 @@ it: fields: logo_url: label: "Logo Primario" + description: "L'immagine del logo in alto a sinistra del tuo sito. Usare una forma rettangolare estesa." logo_small_url: label: "Logo Piccolo" + description: "Una versione compatta del tuo logo, mostrata in alto a sinistra del tuo sito, quando lo scorri verso il basso. Utilizzare una forma quadrata." icons: title: "Icone" fields: favicon_url: label: "Icona Piccola" + description: "Immagine dell'icona utilizzata per rappresentare il tuo sito nei browser web che sembra buona a piccole dimensioni, ad esempio 32px per 32px." apple_touch_icon_url: label: "Icona Grande" + description: "Immagine dell'icona utilizzata per rappresentare il tuo sito su dispositivi moderni che sembra buona a dimensioni più grandi. La dimensione consigliata è almeno 144px per 144px." homepage: + description: "Ti consigliamo di mostrare gli ultimi argomenti nella tua homepage, ma puoi anche visualizzare le categorie (gruppi di argomenti) sulla homepage, se preferisci." title: "Homepage" fields: homepage_style: choices: + latest: + label: "Ultimi Argomenti" categories: label: "Categorie" emoji: title: "Emoji" + description: "Quale stile di emoji preferisci per la tua comunità? Puoi aggiungere più Emoji personalizzate in seguito tramite Amministrazione, Personalizza, Emoji." + invites: + title: "Invita lo Staff" + description: "Hai quasi finito! Invita alcuni membri dello staff a contribuire alle discussioni con argomenti e risposte interessanti per far partire la tua comunità." + finished: + title: "Il tuo Discourse è Pronto!" + description: | +

    Se te la senti di cambiare queste impostazioni, visita la tua sezione amministrazione; la trovi accanto all'icona della chiave inglese nel menu hamburger del sito.

    +

    Divertiti e buona fortuna nel costruire la tua nuova comunità!

    activemodel: errors: <<: *errors diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 6435fbb2b4..10fb5de0c6 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -14,6 +14,8 @@ ja: formats: short: "%Y-%d-%m" short_no_year: "%B %-d" + date_only: "%B%-d、%Y" + long: "%B%-d、%Y、%I:%M%P" date: month_names: [null, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] <<: *datetime_formats @@ -29,8 +31,13 @@ ja: purge_reason: "放棄されていたため自動的に削除、アカウントを停止しました。" disable_remote_images_download_reason: "ディスク容量が不足しているため、リモートでの画像ダウンロードは無効になっています。" anonymous: "匿名" + remove_posts_deleted_by_author: "削除者" + themes: + bad_color_scheme: "テーマを更新できないなら、カラースキームを無効にする" + other_error: "サーバーに不具合があるので、テーマを更新してください。" emails: incoming: + default_subject: "このトピックはタイトルが必要です。" show_trimmed_content: "続きを読む" errors: &errors format: '%{attribute} %{message}' @@ -383,9 +390,9 @@ ja: description: "\"%{application_name}\" があなたのアカウントへのアクセスを要求しています: " reports: visits: - title: "訪問ユーザー" + title: "アクセスユーザー" xaxis: "日" - yaxis: "訪問者数" + yaxis: "アクセス数" signups: title: "新規ユーザー" xaxis: "日" @@ -517,9 +524,9 @@ ja: xaxis: "日" yaxis: "合計" mobile_visits: - title: "訪問ユーザー" + title: "アクセスユーザー" xaxis: "日" - yaxis: "訪問者数" + yaxis: "アクセス数" dashboard: rails_env_warning: "サーバは %{env} モードで起動中です。" host_names_warning: "現在 config/database.yml ファイルは、デフォルトの localhost をホスト名として使用しています。あなたのサイトのホスト名に更新してください。" @@ -551,7 +558,6 @@ ja: educate_until_posts: "最初(または複数)の投稿でタイピングを開始したら、ポップアップでガイダンスを表示させるか" title: "サイトの名前です。titleタグで使用されます" site_description: "このサイトについて簡単に説明してください。 descriptionタグで使用されます。" - contact_email: "このサイトへのお問い合わせを行うメールアドレス。お問い合わせフォームからの緊急事項、対応されていない通報など、緊急の通知に使用します" contact_url: "このサイトの問い合わせURL。緊急連絡用問い合わせフォームに使用されます" queue_jobs: "開発者ONLY! 警告! デフォルトでキューは sidekiq により処理されます。これを無効にするとサイトが動作不能になります。" crawl_images: "正しい幅と高さを取得するためにURLから画像を取得する" @@ -945,6 +951,7 @@ ja: deferred: "伝えてくれてありがとうございます。調査しています" deferred_and_deleted: "伝えてくれてありがとうございます。投稿を削除しました" system_messages: + contents_hidden: "内容を確認するためにその投稿にアクセスして下さい。" welcome_user: subject_template: "%{site_name} へようこそ!" welcome_invite: @@ -983,6 +990,9 @@ ja: download_remote_images_disabled: subject_template: "リモート画像のダウンロードを無効化" text_body_template: "'download_remote_images_threshold'の制限に達したため、`download_remote_images_to_local`の設定は無効になりました" + unsubscribe_mailing_list: | + 本メッセージは、メーリングリストモードの設定が有効になっているため配信されました。 + 配信停止をご希望の場合は、[こちらをクリック](%{unsubscribe_url}) subject_re: "Re:" subject_pm: "[プライベートメッセージ]" user_notifications: @@ -990,6 +1000,8 @@ ja: unsubscribe: title: "解除" description: "メールに興味がありませんか? 下のリンクをクリックすると、即座にメール解除ができます:" + reply_by_email: "回答するにはこのメールに返信するか、 (%{base_url}%{url}) に[ブラウザでアクセスしてください]。" + visit_link_to_respond: "回答するには(%{base_url}%{url})に[ブラウザでアクセスしてください]。" posted_by: "%{post_date} に %{username} が投稿" digest: why: "あなたが最後にアクセスした %{last_seen_at} 以降の %{site_link} のまとめです" @@ -1138,19 +1150,53 @@ ja: editor: name: 編集者 description: はじめて投稿を編集する + long_description: | + このバッジは投稿内容の初回編集時におくられます。今後編集のつもりがなくても編集することは良いアイデアです。-投稿を改善したり、細部の間違いを修正したり、何か初期投稿で失敗した時にも内容を追加できます。編集はあなたの投稿をこれまでより良いものにします! + basic_user: + long_description: | + このバッジは信用レベルが1に達した時に授与されます。サイトに出入りして下さったり、我々のコミュニティがどういうものなのかを学ぶためにトピックに目を通して下さり有難うございます。あなたのユーザー条件は次の通りです;全ての必要なコミュニティ要素が許可されました。個人メッセージ、フラグ付け、wiki編集、および複数の画像とリンクの投稿 + regular: + long_description: | + このバッジは信用レベルが3に達した時に授与されます。 + あなたは今最もアクティブな読者の一人であり、我々のコミュニティを素晴らしいものに変えてくれる信用できる貢献者です。カテゴライズしなおしたり、トピック名を付け替えたり、スパムフラグを利用したり、プライベートラウンジエリアにアクセスしたりすることができ、日ごとにいいねが増えるでしょう。 + welcome: + long_description: | + このバッジは投稿に対して初めていいね!された時に授与されます。おめでとうございます、投稿した内容についてあなたのコミュニティメンバーは興味深く、クールで有能です! autobiographer: name: あなたはだれ? description: プロフィールをすべて書く + long_description: | + このバッジはご自身のユーザープロフィールを記入またはプロフィール写真を選択すると授与されます。コミュニティに対してあなたがどういう人物であるか、また改善について興味がある物をもう少し詳しくお知らせ頂き、更に結びつきのあるコミュニティへ。連携しましょう! + anniversary: + long_description: | + このバッジは一年間メンバーで有り続け、少なくともその年に一回は投稿したときに授与されます。サイトに出入りして下さったり、我々のコミュニティに貢献して下さり有難うございます。我々はあなたを必要としています。 nice_post: name: ナイスな返事 + long_description: | + このバッジはあなたの返信が10回いいね!されたときに授与されます。あなたの返信はコミュニティにとってとても印象深く、会話を促進することに役立っています! good_post: name: イカす返事 + long_description: | + このバッジはあなたが25回いいね!されたときに授与されます。あなたの返答は特別で参加者のために会話をよりよいものに変えてくれました! great_post: name: 素晴らしい返事 + long_description: | + このバッジは50回いいね!されたときに授与されます。素晴らしい!あなたの返答は心をつかみ、魅力的で、とても陽気で、洞察力がありそしてコミュニティをとても大切にされていました。 nice_topic: name: ナイスなトピック + long_description: | + このバッジはあなたのトピックが10回いいね!されたときに授与されます。あなたはコミュニティが楽しめるような興味深い会話を開始しました! good_topic: name: イカすトピック + nice_share: + long_description: | + このバッジは共有リンクが25人のサイト訪問者からクリックされた時に授与されます。我々のディスカッションとこのコミュニティについて展開して下さり有難うございます! + good_share: + long_description: | + このバッジは共有リンクが300人のサイト訪問者からクリックされた時に授与されます。素晴らしい出来です!素晴らしいディスカッションで注目を集め、コミュニティの成長を助けてくれました。 + great_share: + long_description: | + このバッジは共有リンクが1000人のサイト訪問者からクリックされた時に授与されます。恐るべし数です!莫大な人数への興味深い討論であなたは昇格しました、そして大胆な手法で我々のコミュニティの成長を助けてくれました! first_flag: name: はじめの通報 description: 通報した投稿 diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 30f6b772cd..2805b2b9ac 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -344,7 +344,6 @@ ko: translation_overrides: attributes: value: - invalid_interpolation_keys: '다음 interpolation key가 유효하지 않습니다: "%{keys}"' missing_interpolation_keys: '다음 interpolation key가 없습니다: "%{keys}"' <<: *errors user_profile: @@ -876,9 +875,7 @@ ko: notify_mods_when_user_blocked: "만약 사용자가 자동 블락되면 중간 운영자에게 메시지 보내기" flag_sockpuppets: "어떤 신규 사용자(예:24이내 가입자)가 글타래를 생성하고 같은 IP주소의 또 다른 신규 사용자가 댓글을 쓰면 자동 스팸 신고" traditional_markdown_linebreaks: "Markdown에서 전통적인 linebreak를 사용, linebreak시 두개의 trailing space를 사용하는 것." - enable_experimental_markdown_it: "(실험) CommonMark를 지원하는 markdown.it 엔진을 사용합니다. 경고: 올바르게 작동하지 않는 플러그인이 있을 수 있습니다." enable_markdown_typographer: "문단의 가독성을 높이기 위해서 기본 타이포그라피 룰을 사용합니다. (c) (tm), 기타 기호를 교체하고 연달아 나오는 물음표의 갯수를 줄입니다." - allow_html_tables: "마크다운 문서에 HTML 테이블을 허용합니다. TABLE, THEAD, TD, TR, TH 태그를 사용할 수 있습니다.(테이블이 포함된 이전 게시물에 적용하려면 rebake 해야 합니다.)" post_undo_action_window_mins: "사용자가 어떤 글에 대해서 수행한 작업(신고 등)을 취소하는 것이 허용되는 시간(초)" must_approve_users: "스태프는 반드시 사이트 엑세스권한을 허용하기 전에 모든 신규가입계정을 승인해야 합니다. 경고: 이것을 활성화하면 기존 스태프 아닌 회원들의 엑세스권한이 회수됩니다." pending_users_reminder_delay: "새로운 사용자가 승인을 기다리는 시간이 여기에 지정된 시간횟수보다 더 길어길경우 운영자에게 알려줍니다. 알림을 해제하려면 -1로 설정하세요." @@ -1046,7 +1043,8 @@ ko: tl3_requires_posts_read_all_time: "VIP 사용자-3 이 되기 위해 꼭 보아야 하는 글타래의 전체 개수" tl3_requires_max_flagged: "회원 레벨3의 자격을 얻으려면, 지난 (tl3 time period)일 동안 x명의 사용자로부터 x개 이상의 포스트를 신고당한 일이 없어야 합니다. x 값을 설정.(0이상)" tl3_promotion_min_duration: "회원등급이 2로 떨어진 후 다시 VIP 사용자-3 이 될 수 있는 최소 일 수" - tl3_requires_likes_given: "회원 레벨3 자격을 얻기 위하여 지난 (tl3 time period)동안 받아야 하는 최소 좋아요 수." + tl3_requires_likes_given: "회원 레벨3 자격을 얻기 위하여 지난 (tl3 time period)동안 줘야 하는 최소 좋아요 수." + tl3_requires_likes_received: "회원 레벨3 자격을 얻기 위하여 지난 (tl3 time period)일 동안 받아야 하는 최소 좋아요 수." tl3_links_no_follow: "VIP 사용자-3 의 글에 있는 링크에서 rel=nofollow 를 제거하지 마시오." min_trust_to_create_topic: "새로운 주제를 생성하기 위한 최소 회원등급" min_trust_to_edit_wiki_post: "위키로 설정된 글 수정할 수 있는 최소 회원등급" @@ -1055,12 +1053,14 @@ ko: min_trust_to_send_messages: "새로운 개인 메시지를 보내기 위한 최소 신뢰도" newuser_max_links: "새로운 사용자가 글에 붙일 수 있는 최대 링크 개수" newuser_max_images: "새로운 사용자가 글에 붙일 수 있는 최대 이미지 개수" - newuser_max_attachments: "새로운 사용자가 포트에 붙일 수 있는 최대 첨부파일 개수" - newuser_max_mentions_per_post: "새료운 사용자가 글에 사용할 수 있는 최대 @name 알림 개수" - newuser_max_replies_per_topic: "다른 사용자가 답글을 달기 전, 새로운 사용자가 개별 글타래에 달 수 있는 최대 답글 개수" + newuser_max_attachments: "새로운 사용자가 포스트에 첨부할 수 있는 최대 파일 개수" + newuser_max_mentions_per_post: "새로운 사용자가 글에서 쓸 수 있는 최대 @아이디 멘션 개수" + newuser_max_replies_per_topic: "다른 사용자가 댓글을 달기 전, 새로운 사용자가 개별 토픽에 달 수 있는 최대 답글 개수" max_mentions_per_post: "글 당 사용할 수 있는 최대 @name 알림의 수" + max_users_notified_per_group_mention: "그룹이 멘션을 받으면 알림을 받는 최대사용자의 수(최대치에 도달하면 알림이 전송되지 않음)" create_thumbnails: "글의 너무 큰 크기의 이미지는 thumnails와 lightbox를 만든다." email_time_window_mins: "알림 메일을 보내기 전 대기 기간(분), 사용자에게 글의 변경하고 완료할 수 있는 기회를 준다." + private_email_time_window_seconds: "사용자가 메시지를 재편집할 기회를 주기 위하여 개인 알림 메일을 보내기 이전에 설정한 시간만큼 대기합니다." email_posts_context: "알림메일의 내용에 추가할 기존 답글 수" flush_timings_secs: "사용자의 이용 시간 데이터를 서버로 보내는 기간(초)" title_max_word_length: "글타래 제목안에 단어들의 최대 길이" @@ -1070,12 +1070,15 @@ ko: title_fancy_entities: "글타래 제목에 일반 ASCII 문자로 만든 기호들을 보기 좋은 HTML로 변환시켜준다. 참고: SmartyPants http://daringfireball.net/projects/smartypants/" min_title_similar_length: "유사 글타래에 대한 체크가 있기 전, 최소 제목 글자 수" min_body_similar_length: "유사 글타래에 대한 체크가 있기 전, 글의 최소 본문 글자 수" + desktop_category_page_style: "/categories 페이지의 비주얼 스타일" category_colors: "허용된 카테고리에 사용될 hexadecimal 색상 값의 리스트" category_style: "카테고리 뱃지 시각 스타일" max_image_size_kb: "최대 이미지 업로드 사이즈(kB). 이 설정은 꼭 nginx / apache와 proxy에도 적용해야 합니다." max_attachment_size_kb: "최대 첨부파일 업로드 사이즈(kB). 이 설정은 꼭 nginx / apache와 proxy에도 적용해야 합니다." authorized_extensions: "파일 업로드에 허용되는 확장자 리스트 ('*'을 사용하면 모든 타입의 파일이 가능합니다.)" + theme_authorized_extensions: "테마 업로드에 허용되는 확장자 목록('*'를 입력하면 모든 확장자 허용)" max_similar_results: "새로운 글타래를 작성할 때, 에디터 위에 보여줄 비슷한 글타래들의 개수. 제목과 본문을 바탕으로 비교합니다." + max_image_megapixels: "이미지의 최대 메가픽셀수" title_prettify: "일반적인 제목의 오타 및 오류를 수정해준다. 모두 대문자로 쓰거나, 첫자가 소문자이거나(영문), 복수의 !, ? 혹은 마침표(.)가 중복으로 들어간 것 등" topic_views_heat_low: "글타래가 연하게 하이라이트 되기 위한 조회수" topic_views_heat_medium: "글타래가 적당하게 하이라이트 되기 위한 조회수" @@ -1092,8 +1095,11 @@ ko: faq_url: "FAQ주소가 있으면 전체 URL을 적어주세요." tos_url: "이용약관이 있으면 전체 URL을 적어주세요." privacy_policy_url: "개인정보 보호가 있으면 전체 URL을 적어주세요." + newuser_spam_host_threshold: "새로운 사용자가 동일한 호스트의 링크를 포스팅하더라도 스팸으로 간주되지 않는 최대 포스팅 수 `newuser_spam_host_threshold`" white_listed_spam_host_domains: "스팸 호스트 테스트로부터 제외할 도메인 리스트. 새로운 사용자는 새로운 사용자는 헤당 도메인에 대한 링크를 포함하는 글를 생성하는데 제한되지 않는다." staff_like_weight: "스태프가 좋아요를 눌렀을 때 부여할 가산점" + topic_view_duration_hours: "N 시간마다 IP/User 별로 새 토픽 조회수를 셉니다." + user_profile_view_duration_hours: "N 시간마다 IP/User 별로 새 프로필 조회수를 셉니다." levenshtein_distance_spammer_emails: "스패머 메일을 체크할 때, 허용할 다른 글자 개수(fuzzy match)" max_new_accounts_per_registration_ip: "(n) 회원등급의 0개의 계정(스태프도 tl2 이상도 아닌 계정만)이 이 IP에 있으면, 새로운 회원가입 방지." min_ban_entries_for_roll_up: "Roll up 버튼을 눌렀을 때, 적어도 (N)개의 엔트리가 있다면 새 subnet ban 엔트리를 만듭니다." @@ -1101,16 +1107,40 @@ ko: max_age_unmatched_ips: "(N)일 뒤에 안 맞는 막힌 IP 접근을 지웁니다." num_flaggers_to_close_topic: "추후 중재를 위해 자동으로 글타래를 일시정지시킬 최소 신고자 수" num_flags_to_close_topic: "추후 중재를 위해 자동으로 글타래를 일시정지시킬 최소 신고 횟수" + num_hours_to_close_topic: "운영진 중재를 위하여 토픽을 중지시킬 시간" auto_respond_to_flag_actions: "신고 기각 시 자동 답변 활성화" + min_first_post_typing_time: "사용자가 첫번째 포스트를 작성할 때 타이핑에 소요해야 하는 밀리초. 설정값에 도달하지 않았을 때는 자동으로 승인 큐에 포함됩니다. 승인 0으로 설정하면 해제됩니다.(해제는 권장하지 않습니다)" + auto_block_fast_typers_on_first_post: " min_first_post_typing_time에 도달하지 않은 사용자를 자동으로 차단" auto_block_fast_typers_max_trust_level: "빠른 타이핑이 감지되면 자동 차단을 적용할 최대 신뢰도" + auto_block_first_post_regex: "첫번째 포스트를 검사하여 통과하면 사용자를 차단하고 승인 대기열에 포함시키는 대소문자 구분이 없는 정규표현식. 예: raging|a[bc]a, 는 raging, aba, aca를 포함한 첫번째 포스트를 차단합니다. 첫번째 포스트에만 적용됩니다." reply_by_email_enabled: "이메일을 통해 주제에 답글을 달 수 있음." reply_by_email_address: "이메일 주소로 답글을 다는 템플릿. 예: %{reply_key}@reply.myforum.com" + alternative_reply_by_email_addresses: "수신된 이메일에 대한 답장메일의 대체 템플릿 목록. 예: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" incoming_email_prefer_html: "수신 이메일에 text 대신 HTML을 사용" disable_emails: "Discourse가 어떤 메일로 보내지 못하도록 합니다." strip_images_from_short_emails: "2800바이트, 즉 2.73 KB 이내 사이즈를 가지도록 이미지를 빼주세요." short_email_length: "짦은 이메일 바이트 길이" display_name_on_email_from: "이메일 보낸사람 필드에 전체 이름을 표시" + unsubscribe_via_email: "사용자가 이메일 제목이나 내용에 'unsubscribe' 를 포함시켜서 답장하면 이메일 구독을 해지가 되도록 허용" + unsubscribe_via_email_footer: "이메일의 하단에 이메일 mailto: 링크 방식의 구독해지 링크를 첨부하기" + delete_email_logs_after_days: "(N) 일이 지나면 이메일 로그 삭제하기. 0으로 설정하면 삭제하지 않음" + max_emails_per_day_per_user: "사용자에게 보낼 이메일의 하루 최대치. 0으로 설정하면 제한없음." + enable_staged_users: "수신 이메일 처리 중일 때 자동으로 격리된 회원 생성." + maximum_staged_users_per_email: "수신된 이메일을 처리하는 동안 생성할 최대 격리회원 수" + auto_generated_whitelist: "자동 생성 메일로 체크되지 않는 이메일 주소의 목록. 예: foo@bar.com|discourse@bar.com" + block_auto_generated_emails: "자동 생성된 이메일로 확인된 수신 이메일 차단." + ignore_by_title: "제목을 기준으로 수신 메일 무시." + mailgun_api_key: "webhook 메시지를 확인할 Mailgun Secret API 키" + soft_bounce_score: "임시 반송이 일어났을 때 사용자에게 반송 스코어 추가." + hard_bounce_score: "영구 반송이 일어났을 때 사용자에게 반송 스코어 추가." + bounce_score_threshold: "사용자에게 메일을 보낼 최대 반송 스코어." + bounce_score_threshold_deactivate: "사용자를 비활성화 할 최대 반송 스코어." + reset_bounce_score_after_days: "X 일 이후 반송 스코어 자동 초기화." + attachment_content_type_blacklist: "콘텐츠 타입을 기준으로 첨부를 차단할 키워드 블랙리스트." + attachment_filename_blacklist: "파일 이름을 기준으로 첨부를 차단할 키워드 블랙리스트." enable_forwarded_emails: "[베타] 사용자가 이메일 포워딩으로 토픽을 생성할 수 있도록 허용" + always_show_trimmed_content: "항상 수신 메일의 다듬어진 부분을 표시. 경고: 메일 주소가 드러날 수 있습니다." + private_email: "개인 정보 보호를 위하여 이메일에 포스트나 토픽의 내용 미포함." manual_polling_enabled: "이메일 답장을 위한 API를 사용하여 이메일 푸시" pop3_polling_enabled: "POP3를 이용한 이메일 설문조사" pop3_polling_ssl: "SSL로 POP3 서버를 연결합니다. (추천)" @@ -1125,29 +1155,52 @@ ko: email_in_min_trust: "이메일을 통해 새 글타래를 포스팅 할 수 있는 최소 사용자 회원등급" email_prefix: "이메일 제목에 쓰일 [라벨]. 설정하지 않으면 기본적으로 'title(필수 설정의)' 이 된다." email_site_title: "사이트에서 전송되는 이메일의 보내는 이를 사이트 이름으로 설정. 설정하지 않으면 'title(필수 설정의)'이 된다. 만약 'title'에 보내는 이에 허용되지 않는 글자가 포함되어 있으면 이 설정이 사용된다." + find_related_post_with_key: "응답 포스트를 찾을 때는 reply 키만 사용.(Amazon SES 사용 시 권장)" minimum_topics_similar: "새로운 글타래를 작성할 때, 유사한 글타래들을 보여주기 위해 존재해야 할 최소 글타래 개수" relative_date_duration: "절대적 날짜(9월 3일) 대신 상대적 날짜(7일 전)가 사용될 일 수 (글타래가 작성된 이후부터)" delete_user_max_post_age: "(x)일 이전의 첫 글가 있는 유저는 삭제할 수 없음." delete_all_posts_max: "전체 글 지우기 버튼을 통해 한번에 삭제할 수 있는 최대 글 수. 만약 사용자가 이것보다 많은 글을 가지고 있으면 한번에 삭제 할 수 없다." username_change_period: "등록 후 사용자 이름 최소 유지 일 수(0은 사용자 이름 변경을 막음)" email_editable: "가입 후 이메일 주소 변경 허용" + logout_redirect: "로그아웃 후에 리디렉션할 위치 (예: http://somesite.com/logout)" + allow_uploaded_avatars: "사용자가 커스텀 프로필 사진을 올릴 수 있도록 허용." + allow_animated_avatars: "사용자가 프로필 사진으로 애니메이션 GIF 파일을 올릴 수 있도록 허용. 경고: 이 설정을 바꾼 후 avatars:refresh rake task 를 실행." allow_animated_thumbnails: "움직이는 gif로 썸네일을 만듭니다." default_avatars: "신규가입자가 받게될 기본 아바타 URL" automatically_download_gravatars: "사용자가 계정을 만들거나 이메일을 변경하자마자 Gravatar를 다운로드합니다." + digest_topics: "요약 이메일에 표시할 인기 토픽의 최대 개수." + digest_posts: "요약 이메일에 표시할 인기 포스트의 최대 개수." + digest_other_topics: "요약 이메일의 '신규 토픽과 팔로우하는 카테고리'에 표시되는 토픽의 최대 개수." + digest_min_excerpt_length: "요약 이메일에서 발췌된 포스트의 최소 표시 글자 수." + suppress_digest_email_after_days: "(n) 일 이상 사이트를 방문하지 않은 사용자에게는 요약 이메일 보내지 않기." + digest_suppress_categories: "요약 이메일에서 이 카테고리 숨기기." disable_digest_emails: "모든 사용자에게 요약 이메일 전송 해제하기" + email_accent_bg_color: "강조 색상은 HTML 이메일의 몇몇 요소의 배경색상으로 사용됩니다. 색상명('red')이나 hex 값('#FF0000')을 입력하세요." + email_accent_fg_color: "텍스트 컬러는 HTML 이메일의 배경색상으로 렌더링됩니다. 색상명('white')나 hex값('#FFFFFF')을 입력하세요." + email_link_color: "HTML 이메일의 링크 색상. 색상명('blue')나 hex값('#0000FF')를 입력하세요." + detect_custom_avatars: "사용자가 커스텀 프로필 사진을 올렸는지의 체크 여부." max_daily_gravatar_crawls: "하루에 Discourse가 커스텀 아파타를 위해 Gravatar를 체크하는 최대 횟수" public_user_custom_fields: "유저가 쓸 수 있는 공개 커스텀 필드 목록" staff_user_custom_fields: "스태프가 쓸 수 있는 공개 커스텀 필드 목록" - enable_user_directory: "전체 유저 둘러보기 목록 제공" + enable_user_directory: "브라우징을 위한 유저의 디렉토리 제공" + enable_group_directory: "브라우징을 위한 그룹의 디렉토리 제공" allow_anonymous_posting: "익명 모드 허용" anonymous_posting_min_trust_level: "익명 게시 할 수 있는 최소 회원등급" anonymous_account_duration_minutes: "한 익명이 익명계정을 만들 때 빨리 만드는 걸 막을 분 단위 시간 예: 600으로 정해놓으면 마지막 게시하고 익명 전환 후로 600분이 지나는 즉시 새 계정이 만들어 집니다." + hide_user_profiles_from_public: "익명사용자의 사용자 카드, 사용자 프로필, 사용자 디렉토리 해제." + user_website_domains_whitelist: "이 목록에 속한 도메인으로는 개인 웹사이트 등록 불가함. ` | `를 구분자로 한 목록." allow_profile_backgrounds: "사용자에게 프로필 배경 이미지 업로드를 허용합니다." + sequential_replies_threshold: "너무 많은 연속 댓글을 달고 있다고 경고를 받지 않고 사용자가 연달아 쓸 수 있는 포스트 수." + get_a_room_threshold: "경고를 받지 않고, 같은 토픽에서 같은 사람에게 보낼 수 있는 허용 포스트 수." enable_mobile_theme: "모바일 디바이스는 모바일 환경에 친화적인 테마를 사용합니다, 그리고 PC용 화면으로 전환할 수 있습니다. 만약 커스텀 스타일 시트를 사용한다면 이것을 비활성화 시키세요." dominating_topic_minimum_percent: "한 글타래에서 한 사용자의 영향력을 결정하는 글 수의 퍼센트" + disable_avatar_education_message: "아바타 변경 교육 메시지 해제." suppress_uncategorized_badge: "글타래 리스트에서 카테고리가 없는 글타래에 대한 훈장을 보여주지 않는다." + permalink_normalizations: "퍼마링크를 매칭하기 전에 다음 정규표현식을 적용. 예: /(topic.*)\\?.*/\\1 를 적용하면 토픽 루트에서 퀴리 스트링을 뽑아냅니다. 캡쳐된 값에 접근하려면 정규표현식+스트링 형식에 \\1 등을 사용하면 됩니다." global_notice: "긴급, 비상 전체 배너를 모든 방문자에게 표시합니다. 숨기려면 빈칸으로 바꾸면 됩니다(HTML 허용)." disable_edit_notifications: "'download_remote_images_to_local'가 활성화되있으면 시스템 사용자에 의한 수정 알림을 비활성화합니다." + automatically_unpin_topics: "사용자가 하단에 도달하면 토픽 고정을 자동 해제." + read_time_word_count: "읽은 시간 추정치 계산을 위한 1분 당 워드 카운트." share_anonymized_statistics: "익명화된 사용 통계 공유하기" full_name_required: "사용자 프로필에서 실명을 필수 입력 항목으로 설정" enable_names: "사용자의 실명을 프로필, 사용자카드, 이메일에서 표시함. 실명을 표시하지 않으려면 체크를 해제하세요." diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index c28ee211c7..828a8c074b 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -895,7 +895,6 @@ nl: notify_mods_when_user_blocked: "Als een gebruiker automatisch geblokkeerd is, stuur dan een bericht naar alle moderatoren." flag_sockpuppets: "Als een nieuwe gebruiker antwoord op een topic vanaf hetzelfde ip-adres als de nieuwe gebruiker die het topic opende, markeer dan beide berichten als potentiële spam." traditional_markdown_linebreaks: "Gebruik traditionele regeleinden in Markdown, welke 2 spaties aan het einde van een regel nodig heeft voor een regeleinde." - allow_html_tables: "Sta toe dat tabellen in Markdown mogen worden ingevoerd met behulp van HTML-tags. TABLE, TD, TR, TH zullen aan de whitelist worden toegevoegd (vereist volledig herbouwen van alle oude berichten met tabellen)" post_undo_action_window_mins: "Het aantal minuten waarin gebruikers hun recente acties op een bericht nog terug kunnen draaien (liken, markeren, etc)." must_approve_users: "Stafleden moeten alle nieuwe gebruikersaccounts goedkeuren voordat ze de site mogen bezoeken. OPGELET: als dit wordt aangezet voor een actieve site wordt alle toegang voor bestaande niet stafleden ingetrokken." pending_users_reminder_delay: "Moderators informeren als nieuwe gebruikers al langer dan dit aantal uren op goedkeuring wachten. Stel dit in op -1 om meldingen uit te schakelen." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 67ddc8bdf6..84074ab6c9 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -956,7 +956,6 @@ pl_PL: notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." flag_sockpuppets: "Jeśli nowy użytkownik odpowiada na dany temat z tego samego adresu IP co nowy użytkownik, który założył temat, oznacz ich posty jako potencjalny spam." traditional_markdown_linebreaks: "Używaj tradycyjnych znaków końca linii w Markdown, to znaczy dwóch spacji na końcu linii." - allow_html_tables: "Pozwalaj tabelom być zamieszczanym w Markdown przy użyciu tagów HTML. TABLE, THEAD, TD, TR, TH będą dozwolone (wymaga pełnego rebake na wszystkich starych postach zawierających tabele)." post_undo_action_window_mins: "Przez tyle minut użytkownicy mogą cofnąć swoje ostatnie działania przy danym poście (lajki, flagowanie, itd.)." must_approve_users: "Zespół musi zaakceptować wszystkie nowe konta zanim uzyskają dostęp do serwisu. UWAGA: włączenie tego dla już udostępnionej strony sprawi, że zostanie odebrany dostęp wszystkim istniejącym użytkownikom spoza zespołu." pending_users_reminder_delay: "Powiadomić moderatorów jeżeli nowi użytkownicy czekali na zatwierdzenie dłużej niż his mamy godzin. Ustaw -1 aby wyłączyć powiadomienia. " @@ -1349,40 +1348,8 @@ pl_PL: subject_template: "Potwierdź, że nie chcesz już otrzymywać powiadomień mailowych ze strony %{site_title}" invite_mailer: subject_template: "%{invitee_name} zaprosił cię do '%{topic_title}' na %{site_domain_name}" - text_body_template: | - %{invitee_name} zaprosił cię do dyskusji - - > **%{topic_title}** - > - > %{topic_excerpt} - - w serwisie - - > %{site_title} -- %{site_description} - - Jeśli jesteś zainteresowany, kliknij w link: - - %{invite_link} custom_invite_mailer: subject_template: "%{invitee_name} zaprosił cię do dyskusji '%{topic_title}' w serwisie %{site_domain_name}" - text_body_template: | - %{invitee_name} zaprosił cię do dyskusji - - > **%{topic_title}** - > - > %{topic_excerpt} - - w serwisie - - > %{site_title} -- %{site_description} - - Informacja od zapraszającego: - - > %{user_custom_message} - - Jeśli jesteś zainteresowany, kliknij w link: - - %{invite_link} invite_forum_mailer: subject_template: "%{invitee_name} zaprosił cię do dołączenia do %{site_domain_name}" text_body_template: | @@ -1657,25 +1624,6 @@ pl_PL: visit_link_to_respond: "[Zobacz Temat](%{base_url}%{url}) aby odpowiedzieć." visit_link_to_respond_pm: "[Zobacz wiadomość](%{base_url}%{url}) aby odpowiedzieć." posted_by: "Dodany przez %{username} w dniu %{post_date}" - invited_to_private_message_body: | - %{username} zaprosił cię do wiadomści - - > **%{topic_title}** - > - > %{topic_excerpt} - - w - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} zaprosił cię do dyskusji - > **%{topic_title}** - > - > %{topic_excerpt} - - w - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "Użytkownik zaprosił do PW" text_body_template: | diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index a89dda5a2d..7968d800ba 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -27,29 +27,35 @@ pt: topics: "Tópicos" posts: "mensagens" loading: "A carregar" + powered_by_html: 'Desenvolvido por Discourse, e melhor visualização com o JavaScript ativado' log_in: "Iniciar Sessão" - purge_reason: "Conta removida automaticamente como conta abandonada, desactivada." - disable_remote_images_download_reason: "O download remoto de imagens foi desativado por não haver espaço disponível no disco." + purge_reason: "Eliminada automaticamente como abandonada, conta desativada" + disable_remote_images_download_reason: "A transferência remota de imagens foi desativada por não haver espaço disponível no disco." anonymous: "Anónimo" + remove_posts_deleted_by_author: "Eliminado pelo autor" + themes: + bad_color_scheme: "Não é possível atualizar o tema, esquema de cor inválido" + other_error: "Ocorreu algo de errado ao atualizar o tema" emails: incoming: + default_subject: "Este tópico precisa de um título" show_trimmed_content: "Mostrar conteúdo aparado" - maximum_staged_user_per_email_reached: "Foi atingido o número máximo de utilizadores temporários por email." + maximum_staged_user_per_email_reached: "Foi atingido o número máximo de utilizadores temporários criados por e-mail." errors: empty_email_error: "Acontece quando a informação não processada do email recebido veio em branco." - no_message_id_error: "Acontece quando o email recebido não tem 'Message-Id' no cabeçalho da mensagem." + no_message_id_error: "Acontece quando o e-mail não tem 'Id. de Mensagem' no cabeçalho." auto_generated_email_error: "Acontece quando o cabeçalho 'precedence' é definida como: 'list', 'junk' ou 'auto_reply', ou quando algum cabeçalho contém: 'auto-submitted', 'auto-replied' ou 'auto-generated'." - no_body_detected_error: "Acontece quando conseguimos obter um corpo de mensagem e não existem anexos." - inactive_user_error: "Acontece quando o remetente não está activo." - blocked_user_error: "Acontece quando o remetente está bloqueado." + no_body_detected_error: "Acontece quando nós não conseguimos extrair um corpo e não existiam anexos." + inactive_user_error: "Acontece quando o remetente não está ativo." + blocked_user_error: "Acontece quando o remetente foi bloqueado." bad_destination_address: "Acontece quando nenhum dos endereços de email nos campos para/cc/bcc coincide com um endereço de email configurado." - strangers_not_allowed_error: "Acontece quando um utilizador tenta criar um tópico numa categoria da qual não é membro." - insufficient_trust_level_error: "Acontece quando um utilizador tentar criar um tópico numa categoria para a qual não tem o nível de confiança necessário." - reply_user_not_matching_error: "Acontece quando uma resposta veio de um endereço de email diferente do destinatário da notificação." - topic_not_found_error: "Acontece quando a resposta veio de um tópico relacionado mas o tópico relacionado foi apagado." - topic_closed_error: "Acontece quando uma resposta chegou mas o tópico relacionado foi fechado." - bounced_email_error: "O email é um relatório de email redirecionado." - screened_email_error: "Acontece quando a morada de email da pessoa que o enviou já foi confirmada." + strangers_not_allowed_error: "Acontece quando um utilizador tentou criar um novo tópico numa categoria em que não é membro." + insufficient_trust_level_error: "Acontece quando um utilizador tentou criar um novo tópico numa categoria em que não tem o nível de confiança necessário." + reply_user_not_matching_error: "Acontece quando uma resposta veio de um endereço de e-mail diferente de onde a notificação foi enviada." + topic_not_found_error: "Acontece quando é recebida uma resposta, mas que o tópico relacionado foi eliminado." + topic_closed_error: "Acontece quando é recebida uma resposta, mas que o tópico relacionado foi encerrado." + bounced_email_error: "A mensagem é um relatório de mensagem redirecionado." + screened_email_error: "Acontece quando o endereço de e-mail do remetente já foi confirmado." errors: &errors format: '%{attribute} %{message}' messages: @@ -103,6 +109,8 @@ pt: max_username_length_range: "Não pode definir o máximo abaixo do mínimo." default_categories_already_selected: "Não pode selecionar uma categoria usada noutra lista." s3_upload_bucket_is_required: "Não pode ativar carregamentos para o S3 excepto se tiver fornecido o 's3_upload_bucket'." + invite: + not_found: "O seu código de convite é inválido. Por favor, contacte o administrador do site." bulk_invite: file_should_be_csv: "O ficheiro carregado deve ser do formato CSV." error: "Ocorreu um erro ao carregar esse ficheiro. Por favor tente mais tarde." @@ -125,6 +133,8 @@ pt: embed: start_discussion: "Iniciar Discussão" continue: "Continuar Discussão" + no_hosts: "Não foram configurados anfitriões para incorporar." + configure: "Configurar Incorporação" more_replies: one: "mais 1 resposta" other: "mais %{count} respostas" @@ -158,6 +168,7 @@ pt: spamming_host: "Pedimos desculpa, não pode colocar uma hiperligação para esse servidor." user_is_suspended: "Utilizadores suspensos não têm permissão para publicar." topic_not_found: "Algo de errado ocorreu. Talvez este tópico tenha sido fechado ou eliminado enquanto olhava para ele?" + not_accepting_pms: "Desculpe, de momento, %{username} não está a aceitar mensagens." just_posted_that: "é demasiado semelhante ao que publicaste recentemente" invalid_characters: "contem caracteres inválidos" is_invalid: "parece pouco claro, é uma frase completa?" @@ -177,6 +188,12 @@ pt: latest: "Tópicos recentes" hot: "Tópicos quentes" top: "Melhores tópicos" + top_all: "Melhores tópicos de todos os tempos" + top_yearly: "Melhores tópicos do ano" + top_quarterly: "Melhores tópicos do trimestre" + top_monthly: "Melhores tópicos do mês" + top_weekly: "Melhores tópicos da semana" + top_daily: "Melhores tópicos do dia" posts: "Últimas mensagens" private_posts: "Mensagens privadas recentes" group_posts: "Mensagens recentes em %{group_name}" @@ -190,6 +207,8 @@ pt: queue: delete_reason: "Eliminado através da fila de moderação de mensagens" groups: + success: + bulk_add: "%{users_added}utilizadores foram adicionados ao grupo." errors: can_not_modify_automatic: "Não pode modificar um grupo automático" member_already_exist: "'%{username}' já é membro deste grupo." @@ -197,6 +216,7 @@ pt: invalid_incoming_email: "'%{email}' não é um endereço de email válido." email_already_used_in_group: "'%{email}' já é utilizado pelo grupo '%{group_name}'." email_already_used_in_category: "'%{email}' já é utilizado pela categoria '%{category_name}'." + cant_allow_membership_requests: "Não pode permitir pedidos de adesão para um grupo sem quaisquer donos." default_names: everyone: "todos" admins: "administradores" @@ -207,6 +227,9 @@ pt: trust_level_2: "nivel_de_confianca_2" trust_level_3: "nivel_de_confianca_3" trust_level_4: "nivel_de_confianca_4" + request_membership_pm: + title: "Pedido de Adesão para @%{group_name}" + body: "Eu gostaria de de solicitar a adesão em @%{group_name}." education: until_posts: one: "1 mensagem" @@ -261,16 +284,28 @@ pt: same_as_username: "é a mesma que o seu nome de utilizador. Por favor utilize uma palavra-passe mais segura." same_as_email: "é a mesma que o seu email. Por favor utilize uma palavra-passe mais segura." same_as_current: "é a mesma que a sua palavra-passe atual." + unique_characters: "tem demasiados carateres repetidos. Por favor, utilize uma palavra-passe mais segura." ip_address: signup_not_allowed: "A inscrição não é permitida através desta conta." color_scheme_color: attributes: hex: invalid: "não é uma cor válida" + post_reply: + base: + different_topic: "A publicação e a resposta devem pertencer ao mesmo tópico." web_hook: attributes: payload_url: invalid: "URL inválido. O URL deve incluir http:// ou https://. E espaços não são permitidos." + custom_emoji: + attributes: + name: + taken: já está a ser utilizado por outro emoji + topic_timer: + attributes: + execute_at: + in_the_past: "deve estar no futuro." <<: *errors user_profile: no_info_other: "
    %{name} ainda não colocou nada no campo Sobre Mim
    " @@ -280,6 +315,9 @@ pt: meta_category_description: "Discussão sobre este sítio, a sua organização, como funciona, e como podemos melhorá-lo." staff_category_name: "Pessoal" staff_category_description: "Categoria privada para discussões do pessoal. Os tópicos estão apenas visíveis para administradores e moderadores." + assets_topic_title: "Ativos para o desenho do site" + discourse_welcome_topic: + title: "Bem-vindo ao Discourse" lounge_welcome: title: "Bem-vindo ao Salão" body: |2+ @@ -433,6 +471,8 @@ pt: other: "há quase %{count} anos atrás" password_reset: no_token: "Desculpe, essa hiperligação para alterar a palavra-passe é muito antiga. Selecione o botão Iniciar Sessão e utilize 'Esqueci a minha palavra-passe' para obter uma nova hiperligação." + choose_new: "Escolha uma nova palavra-passe" + choose: "Escolha uma palavra-passe" update: 'Atualizar Palavra-passe' save: 'Definir Palavra-passe' title: 'Redefinir Palavra-passe' @@ -455,43 +495,58 @@ pt: welcome_to: "Bem-vindo a %{site_name}!" approval_required: "Um moderador tem que aprovar a sua conta antes de poder aceder a este fórum. Irá receber um email quando a sua conta for aprovada!" missing_session: "Não conseguimos detectar se a sua conta foi criada, pelo que pedimos que confirme que tem os cookies permitidos." + activated: "Desculpe, esta conta já foi ativada." + admin_confirm: + title: "Confirmar Conta de Administrador" + description: "Tem a certeza que o %{target_username} seja um administrador?" + grant: "Conceder Acesso de Administrador" + complete: "%{target_username} é agora um administrador." + back_to: "Voltar a %{title}" post_action_types: off_topic: title: 'Fora de Contexto' description: 'Esta mensagem não é relevante para a discussão corrente, conforme definido pelo título e pela primeira mensagem, e provavelmente deverá ser transferida para outro tópico.' + short_description: 'Não é relevante para a discussão' long_form: 'sinalizou isto como fora de contexto' spam: title: 'Spam' + short_description: 'É um anúncio ou vandalismo' long_form: 'sinalizou isto como spam' email_title: '"%{title}" foi sinalizado como spam' email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inapropriado' description: 'Esta mensagem contém conteúdo que uma pessoa sensata iria considerar ofensivo, abusivo, ou como uma violação das orientações da nossa comunidade.' + short_description: 'Uma violação das nossas orientações da comunidade' long_form: 'sinalizou isto como inapropriado' notify_user: title: 'Enviar uma mensagem a @{{username}}' description: 'Quero falar com esta pessoa diretamente e em privado acerca da sua mensagem.' + short_description: 'Eu quero falar com esta pessoa diretamente e em privado sobre a sua publicação.' long_form: 'utilizador com mensagens' email_title: 'A sua mensagem em "%{title}"' email_body: "%{link}\n\n%{message}" notify_moderators: title: "Algo Mais" description: 'Esta mensagem requer a atenção do pessoal por outra razão não listada acima.' + short_description: 'Requer a atenção da equipa por outra razão' long_form: 'marcou isto para ser alvo da atenção do pessoal' email_title: 'Uma mensagem em "%{title}" requer a atenção do pessoal' email_body: "%{link}\n\n%{message}" bookmark: title: 'Adicionar Marcador' description: 'Adicionar um marcador a esta mensagem' + short_description: 'Adicionar esta publicação aos marcadores' long_form: 'adicionou um marcador a esta mensagem' like: title: 'Gostar' description: 'Gostar desta mensagem' + short_description: 'Gostar desta publicação' long_form: 'gostou disto' vote: title: 'Votar' description: 'Votar nesta mensagem' + short_description: 'Votar para esta publicação' long_form: 'votou nesta mensagem' user_activity: no_bookmarks: @@ -802,7 +857,6 @@ pt: notify_mods_when_user_blocked: "Se um utilizador for bloqueado de forma automática, enviar uma mensagem a todos os moderadores." flag_sockpuppets: "Se um novo utilizador responde a um tópico a partir do mesmo endereço IP do novo utilizador que iniciou o tópico, sinalizar ambas as mensagens como potencial spam." traditional_markdown_linebreaks: "Utilize tradicionais quebras de linha no Markdown, que requer dois espaços no final para uma quebra de linha." - allow_html_tables: "Permitir inserção de tabelas em Markdown utilizando tags HTML. TABLE,THEAD, TD, TR,TH fazem parte da lista branca (requer que todas as mensagens antigas que contém tabelas sejam refeitas)" post_undo_action_window_mins: "Número de minutos durante o qual os utilizadores têm permissão para desfazer ações numa mensagem (gostos, sinalizações, etc)." must_approve_users: "O pessoal deve aprovar todas as novas contas de utilizador antes destas terem permissão para aceder ao sítio. AVISO: ativar isto para um sítio ativo irá revogar o acesso aos utilizadores existentes que não fazem parte do pessoal!" pending_users_reminder_delay: "Notificar moderadores se novos utilizadores estiverem à espera de aprovação por mais que esta quantidade de horas. Configurar com -1 para desativar notificações." @@ -1709,29 +1763,6 @@ pt: visit_link_to_respond: "[Visitar Tópico](%{base_url}%{url}) para responder." visit_link_to_respond_pm: "[Visitar Mensagem](%{base_url}%{url}) para responder." posted_by: "Publicado por %{username} em %{post_date}" - invited_to_private_message_body: |+ - %{username} convidou-o para uma mensagem - - > **%{topic_title}** - > - > %{topic_excerpt} - - at - - > %{site_title} -- %{site_description} - - invited_to_topic_body: |+ - %{username} convidou-o para uma discussão - - > **%{topic_title}** - > - > %{topic_excerpt} - - em - - > %{site_title} -- %{site_description} - - user_invited_to_private_message_pm: text_body_template: | %{header_instructions} diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 1450f873a7..873cf7e69a 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -1339,16 +1339,6 @@ pt_BR: visit_link_to_respond: "[Visite o Tópico](%{base_url}%{url}) para responder." visit_link_to_respond_pm: "[Visite o Mensagem](%{base_url}%{url}) para responder." posted_by: "Postado por %{username} em %{post_date}" - invited_to_topic_body: | - %{username} convidou você para uma discussão - - > **%{topic_title}** - > - > %{topic_excerpt} - - em - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: text_body_template: | %{header_instructions} diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 8082e409bf..7bb1d31006 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -878,7 +878,6 @@ ro: notify_mods_when_user_blocked: "Dacă un utilizator este blocat automat, trimite un mesaj tuturor moderatorilor." flag_sockpuppets: "Dacă un utilizator nou răspunde unui subiect de la același IP ca utilizatorul ce a pornit subiectul, marchează ambele postări ca potențial spam." traditional_markdown_linebreaks: "Folosește întreruperi de rând tradiționale în Markdown, ceea ce necesită două spații pentru un capăt de rând. " - allow_html_tables: "Permite introducerea de tabele în Markdown prin folosirea de etichete HTML. HEAD, TD, TR, TH vor fi autorizate (necesită un rebake pe toate postările vechi ce conțin tabele)" post_undo_action_window_mins: "Numărul de minute în care utilizatorii pot anula acțiunile recente asupra unei postări (aprecieri, marcări cu marcaje de avertizare, etc)." must_approve_users: "Membrii echipei trebuie să aprobe toate conturile noilor utilizatori înainte ca aceștia să poată accesa site-ul. ATENȚIE: activarea acestei opțiuni pentru un site în producție va revoca accesul tuturor utilizatorilor care nu sunt membri ai echipei!" pending_users_reminder_delay: "Notifică moderatorii dacă noii utilizatori sunt în așteptarea aprobării de mai mult de atâtea ore. Setează la -1 pentru a dezactiva notificările." @@ -1800,26 +1799,6 @@ ro: visit_link_to_respond: "[Vizitează subiect](%{base_url}%{url}) pentru a răspunde." visit_link_to_respond_pm: "[Vizitează subiect](%{base_url}%{url}) pentru a răspunde." posted_by: "Postat de %{username} pe data %{post_date}" - invited_to_private_message_body: | - %{username} te-a invitat la un mesaj - - > **%{topic_title}** - > - > %{topic_excerpt} - - pe - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} te-a invitat la o discuție - - > **%{topic_title}** - > - > %{topic_excerpt} - - pe - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: text_body_template: | %{header_instructions} diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index c624ff9a90..c826a3b6c4 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -28,30 +28,33 @@ ru: posts: "сообщения" loading: "Загрузка..." log_in: "Войти" - purge_reason: "Деактивированная учетная запись будет автоматически удалена как заброшенная" - disable_remote_images_download_reason: "Загрузка картинок была отключена из-за недостаточности места на диске." + purge_reason: "Аккаунт удалён автоматически как заброшенный и неактивированный." + disable_remote_images_download_reason: "Загрузка картинок была отключена из-за недостатка места на диске." anonymous: "Гость" remove_posts_deleted_by_author: "Удалено автором" + themes: + bad_color_scheme: "При обновлении темы оформления обнаружена неверная цветовая схема." + other_error: "При обновлении темы оформления что-то пошло не так." emails: incoming: - default_subject: "Для темы необходим заголовок" - show_trimmed_content: "Показывать урезанное содержимое" + default_subject: "Тема входящего письма по умолчанию." + show_trimmed_content: "Показывать сокращённый текст" maximum_staged_user_per_email_reached: "Достигнуто максимальное количество постановочных пользователей, созданных по электронной почте." errors: empty_email_error: "Происходит, когда мы получаем пустое письмо." no_message_id_error: "Происходит, когда письмо не имеет заголовка 'Message-Id'." - auto_generated_email_error: "Случается, когда заголовок 'Приоритет' установлен в: список, мусор, навалочных или автоматический ответ, или когда какой-либо другой заголовок содержит: автоматический представленный, автоматический ответ или автоматически генерируется." - no_body_detected_error: "Случается, когда мы не могли извлечь тело и не было никаких вложений." + auto_generated_email_error: "Происходит когда заголовок \"приоритет\" установлен в: 'bulk', 'list', 'auto_reply', 'junk' или когда другой заголовок содержит флаги 'auto-submitted', 'auto-replied' или 'auto-generated'." + no_body_detected_error: "Случается, когда мы не смогли извлечь текст письма и не было никаких вложений." inactive_user_error: "Происходит, когда отправитель неактивен." blocked_user_error: "Происходит, когда отправитель заблокирован." - bad_destination_address: "Случается, когда ни один из адресов электронной почты в To / Cc / Bcc поля не совпадающего с настройки входящей адрес электронной почты." - strangers_not_allowed_error: "Возникает,когда пользователь пытается создать новую тему в разделе,членом которой он не является" - insufficient_trust_level_error: "Случается, когда пользователь пытается создать новую тему в категории они не имеют необходимого уровня доверия для." - reply_user_not_matching_error: "Случается, когда ответ пришел с другой адрес электронной почты уведомление было направлено." - topic_not_found_error: "Случается, когда ответ пришел, но связанная тема уже удалена." - topic_closed_error: "Случается, когда ответ пришел, но связанная тема уже закрыта." - bounced_email_error: "Электронная почта является подпрыгнул отчет по электронной почте." - screened_email_error: "Случается, когда адрес электронной почты отправителя был уже экранированы." + bad_destination_address: "Происходит, когда ни один из адресатов, указанных в полях To/Cc/Bcc не совпадает со входящим адресом электронной почты." + strangers_not_allowed_error: "Происходит когда пользователь пытается создать новую тему в разделе, участником которого он не является." + insufficient_trust_level_error: "Происходит когда пользователь пытается создать новую тему в категории, недоступной ему по уровню доступа." + reply_user_not_matching_error: "Происходит, когда ответ приходит с адреса, отличного от того, на который было направлено уведомление." + topic_not_found_error: "Происходит, когда ответ пришел, но связанная тема уже удалена." + topic_closed_error: "Происходит когда ответ пришел, но связанная тема уже закрыта." + bounced_email_error: "Возврат недоставленного сообщения электронной почты." + screened_email_error: "Происходит, когда адрес отправителя электронной почты уже верифицирован (проверен)." errors: &errors format: '%{attribute} %{message}' messages: @@ -72,8 +75,8 @@ ru: inclusion: не включен в список invalid: неверный is_invalid: "Не совсем ясно, это предложение закончено?" - contains_censored_words: "содержит следующие нецензурные слова %{censored_words}" - matches_censored_pattern: "содержит нецензурные слова, которые соответствуют шаблону %{censored_words}" + contains_censored_words: "содержит следующие запрещенные слова %{censored_words}" + matches_censored_pattern: "содержит запрещенные на сайте выражения, которые соответствуют шаблону %{censored_words}" less_than: должен быть меньше %{count} less_than_or_equal_to: должен быть меньше или равен %{count} not_a_number: не число @@ -113,6 +116,7 @@ ru: s3_upload_bucket_is_required: "Нельзя включить загрузки на S3, пока не задана настройка 's3_upload_bucket'." invite: not_found: "Неверный код приглашения. Пожалуйста, свяжитесь с администратором сайта." + user_exists: "Нет необходимости приглашать %{email}, у него уже есть аккаунт!" bulk_invite: file_should_be_csv: "Загружаемый файл должен быть в csv-формате." error: "Произошла ошибка при загрузке файла. Пожалуйста, повторите попытку позже." @@ -123,23 +127,23 @@ ru: backup_file_should_be_tar_gz: "Файл резервной копии должен быть архивом в формате .TAR.GZ." not_enough_space_on_disk: "Не хватает места на диске сервера для загрузки этой резервной копии." invalid_filename: "Резервная копия файла содержит недопустимые символы. Допустимые символы A-Z 0-9. - _." - not_logged_in: "Для этого действия, пожалуйста авторизуйтесь." + not_logged_in: "Для этого действия необходимо зарегистрироваться." not_found: "Запрашиваемая страница или ресурс не найден." invalid_access: "У вас нет прав для просмотра запрашиваемого ресурса." - read_only_mode_enabled: "Сайт в режиме только для чтения. Взаимодействия отключены." + read_only_mode_enabled: "Сайт в режиме только для чтения. Какие-либо иные действия запрещены. " reading_time: "Время на прочтение" likes: "Лайков" too_many_replies: one: "Извините, новые пользователи могут оставлять только один ответ к теме." few: "Извините, новые пользователи могут оставлять только %{count} ответов к одной теме." many: "Извините, новые пользователи могут оставлять только %{count} ответов к одной теме." - other: "Извините, новые пользователи могут оставлять только %{count} ответов к одной теме." + other: "Извините, новые пользователи могут оставлять только %{count} ответ(ов) к одной теме." embed: start_discussion: "Начать обсуждение" continue: "Продолжить обсуждение" - error: "Встраивание Ошибки" - referer: "Отсылающий:" - mismatch: "Ссылающийся не совпал с каким-либо из следующих хостов:" + error: "Внутренняя ошибка" + referer: "Источник:" + mismatch: "Источник не совпал с каким-либо из следующих хостов:" no_hosts: "Никаких хостов не настроено для встраивания." configure: "Настроить Встраивание" more_replies: @@ -189,8 +193,9 @@ ru: spamming_host: "Извините, вы не можете разместить ссылку в этом сообщении." user_is_suspended: "Заблокированным пользователям запрещено писать." topic_not_found: "Что-то пошло не так. Возможно, эта тема была закрыта или заархивирована, пока вы ее читали?" - not_accepting_pms: "К сожалению, %{username} не принимает сообщения в данный момент." - just_posted_that: "слишком схоже с уже опубликованным Вами сообщением" + not_accepting_pms: "К сожалению, %{username} в данный момент не принимает сообщения." + max_pm_recepients: "Извините, вы можете послать сообщение максимум %{recipients_limit}получателю." + just_posted_that: "слишком схоже с уже опубликованным вами сообщением" invalid_characters: "содержит недопустимые символы" is_invalid: "Не совсем ясно, это предложение закончено?" next_page: "следующая страница →" @@ -228,6 +233,8 @@ ru: queue: delete_reason: "Удалено по запросу модератора." groups: + success: + bulk_add: "%{users_added}пользователей было добавлено в группу." errors: can_not_modify_automatic: "Вы не можете изменить автоматическую группу." member_already_exist: "'%{username}' уже является членом этой группы." @@ -246,6 +253,9 @@ ru: trust_level_2: "уровень доверия 2" trust_level_3: "уровень доверия 3" trust_level_4: "уровень доверия 4" + request_membership_pm: + title: "Запрос на участие в группе %{group_name}" + body: "Я хотел бы подать заявку на участие в группе %{group_name}" education: until_posts: one: "1 сообщение" @@ -259,17 +269,15 @@ ru: - Интересен ли придуманный заголовок? Достаточно ли точно он описывает то, что вы хотите обсудить в вашей теме? - - О чём ваша тема? Кому она будет интересна? Почему она важна для обсуждения? Какой, по-вашему мнению, будет реакция общества на тему? + - О чём ваша тема? Кому она будет интересна? Почему она важна для обсуждения? Какой, по-вашему мнению, будет реакция сообщества на тему? - - Постарайтесь включить в текст сообщения подходящие поисковые слова, по которым другие смогут *найти* тему. Выберите раздел, к которому ваша тема подходит по смыслу. + - Постарайтесь включить в текст сообщения подходящие теги, по которым другие смогут *найти* тему. Выберите раздел, к которому ваша тема подходит по смыслу. Больше информации вы найдете в нашем [руководстве пользователя](/guidelines). Данное окно будет показываться, пока вы создаете свои первые %{education_posts_text}. new-reply: | Добро пожаловать на сайт %{site_name} — **Спасибо за участие в обсуждении!** - Составляя свой ответ в теме, учитывайте следующее: - - - Улучшит ли ваш ответ обсуждение в целом, хотя бы немного? + - Как вы думаете, улучшит ли ваш ответ обсуждение в целом, хотя бы немного? - Будьте дружелюбны к участникам разговора. @@ -278,11 +286,11 @@ ru: Больше информации вы найдете в нашем [руководстве пользователя](/guidelines). Данное окно будет показываться пока вы создаете свои первые %{education_posts_text}. avatar: | ### Как насчет изображения для вашей учетной записи? - Вы опубликовали несколько тем и ответы, но ваше изображение профиля не уникален как вы - это просто письмо. + Вы опубликовали несколько тем и ответы, но ваше изображение профиля не уникально - это просто первая буква вашего имени. - Рассматривали ли вы **[посетить свой профиль пользователя] (%{profile_path})** и загрузить изображение, которое представляет вас? + Может быть вам стоит **[посетить свой профиль пользователя] (%{profile_path})** и загрузить аватарку? - Это легче следить за ходом обсуждений и найти интересных людей в разговорах, когда каждый человек имеет уникальный профиль картину! + Когда каждый пользователь имеет уникальную аватарку, следить за обсуждениями становится значительно легче! sequential_replies: | ### Лучше совмещать несколько ответов в один @@ -320,6 +328,7 @@ ru: same_as_username: "совпадает с вашим псевдонимом. Пожалуйста, придумайте более надежный пароль." same_as_email: "совпадает с вашим e-mail. Пожалуйста, придумайте более надежный пароль." same_as_current: "такая же, как текущий пароль." + unique_characters: "слишком много повторяющихся символов. Используйте пожалуйста более сложный пароль." ip_address: signup_not_allowed: "Регистрация с данной учетной записью запрещена." color_scheme_color: @@ -978,7 +987,7 @@ ru: allow_all_attachments_for_group_messages: "Разрешить все почтовые вложения для групповых сообщений." enable_flash_video_onebox: "Разрешить умную вставку ссылок sqf и flv (Adobe Flash). ВНИМАНИЕ: повышает риски безопасности сайта." default_invitee_trust_level: "Уровень доверия приглашенных пользователей по-умолчанию (от 0 до 4)." - default_trust_level: "Уровень доверия по умолчанию для всех новых пользователей (0-4). ВНИМАНИЕ! Повышая уровень доверия для новых пользователей, Вы можете столкнуться с большим колличеством спама." + default_trust_level: "Уровень доверия по умолчанию для всех новых пользователей (0-4). ВНИМАНИЕ! Повышая уровень доверия для новых пользователей, вы можете столкнуться с большим количеством спама." tl1_requires_topics_entered: "В скольких темах новый пользователь должен побывать для продвижения до уровня доверия 1." tl1_requires_read_posts: "Сколько сообщений новый пользователь должен прочитать для продвижения до уровня доверия 1." tl1_requires_time_spent_mins: "Сколько минут новый пользователь должен читать сообщения на форуме для продвижения до уровня доверия 1." @@ -1261,6 +1270,7 @@ ru: incorrect_username_email_or_password: "Неверное имя пользователя, адрес электронной почты или пароль" wait_approval: "Спасибо за регистрацию. Мы оповестим вас, когда ваша учетная запись будет одобрена." active: "Ваша учетная запись активирована и готова к использованию." + activate_email: "

    Почти готово! Мы выслали письмо на %{email}. Пожалуйста, следуйте инструкциям в этом письме для активации вашей учетной записи.

    Если письмо не пришло, пожалуйста, проверьте папку \"спам\", или попробуйте войти еще раз, чтобы выслать активационное письмо повторно.

    " not_activated: "Вы пока что не можете войти на сайт. Пожалуйста, следуйте инструкциям по активации учетной записи, которые мы отправили вам по электронной почтой." not_allowed_from_ip_address: "Вход с этого IP в качестве пользователя %{username} запрещен." admin_not_allowed_from_ip_address: "Вход с этого IP в качестве администратора запрещен." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 71cd2e89a3..912d3d5da0 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -761,7 +761,6 @@ sk: notify_mods_when_user_blocked: "Ak je používateľ automaticky zablokovaný, pošli správu všetkým moderátorom." flag_sockpuppets: "Ak nový používateľ odpovedá na tému z rovnakej IP adresy, ako nový používateľ, ktorý danú tému vytvoril, označ oba ich príspevky ako potencionálny spam." traditional_markdown_linebreaks: "V Markdown použiť tradičné oddeľovače riadkov, čo vyžaduje dve koncové medzery ako oddeľovač riadku." - allow_html_tables: "V Markdown umožniť použitie tabuliek pomocou HTML značiek. TABLE, THEAD, TD, TR, TH budú umožnené (vyžaduje \"full rebake\" na všetkých starých príspevkoch ktoré obsahujú tabuľky)" post_undo_action_window_mins: "Počet minút počas ktorých môžu používatelia zrušiť poslednú akciu na príspevku (\"Páči sa\", označenie, atď..)." must_approve_users: "Obsluha musí povoliť účty všetkým novým používateľom skôr než im bude povolený prístup na stránku. UPOZORNENIE: zapnutie na živej stránke spôsobí zrušenie prístupu pre existujúcich používateľov, okrem obsluhy!" pending_users_reminder_delay: "Upozorni moderátora ak nový používateľ čaká na schválenie dlhšie ako tento počet hodín. Nastavte -1 pre vypnutie upozornenia." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 2e2d4eb90a..93510e25f4 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -63,6 +63,8 @@ sq: load_from_remote: "Postimi nuk mundi të ngarkohet. Riprovoheni!" site_settings: default_categories_already_selected: "Nuk mund të zgjidhni një kategori të përdorur në një tjetër listë." + invite: + user_exists: "Nuk keni nevojë të ftoni %{email}, ata kanë krijuar një llogari në faqe!" not_logged_in: "Ju duhet të identifikoheni për këtë veprim." invalid_access: "Ju nuk jeni të drejta për të parë burimin e kërkuar." reading_time: "Koha e leximit" @@ -387,6 +389,7 @@ sq: title: "Çregjistrohuni" stop_watching_topic: "Ndaloni së ndjekuri këtë temë, %{link}" unwatch_category: "Ndaloni së ndjekuri të gjitha temat në %{category}" + disable_digest_emails: "Mos më dërgo më mesazhe email me përmbledhjet" all: "Mos më dërgoni më email nga %{sitename}" user_api_key: title: "Lejo këtë aplikimi të ketë akses në faqe" @@ -571,7 +574,7 @@ sq: fixed_category_positions_on_create: "If checked, category ordering will be maintained on topic creation dialog (requires fixed_category_positions)." add_rel_nofollow_to_user_content: "Add rel nofollow to all submitted user content, except for internal links (including parent domains). If you change this, you must rebake all posts with: \"rake posts:rebake\"" exclude_rel_nofollow_domains: "A list of domains where nofollow should not be added to links. tld.com will automatically allow sub.tld.com as well. As a minimum, you should add the top-level domain of this site to help web crawlers find all content. If other parts of your website are at other domains, add those too." - post_excerpt_maxlength: "Maximum length of a post excerpt / summary." + post_excerpt_maxlength: "Gjatësia maksimale e një përmbledhjeje të një postimi. " post_onebox_maxlength: "Maximum length of a oneboxed Discourse post in characters." logo_url: "The logo image at the top left of your site, should be a wide rectangle shape. If left blank site title text will be shown." logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down. If left blank a home glyph will be shown." @@ -1040,26 +1043,6 @@ sq: visit_link_to_respond: "[Shiko Temën](%{base_url}%{url}) për tu përgjigjur." visit_link_to_respond_pm: "[Shiko Mesazhin](%{base_url}%/{url}) për tu përgjigjur." posted_by: "Postuar nga %{username} më %{post_date}" - invited_to_private_message_body: | - %{username} ju ftoi tek mesazhi - - > **%{topic_title}** - > - > %{topic_excerpt} - - në - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} ju ftoi në një bisedë - - > **%{topic_title}** - > - > %{topic_excerpt} - - tek - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: text_body_template: | %{header_instructions} @@ -1169,8 +1152,11 @@ sq: %{message} digest: why: "Një përmbledhje e shkurtër e temave tek %{site_link} që prej vizitës tuaj të fundit më %{last_seen_at}" + subject_template: "[%{email_prefix}] Përmbledhja" + unsubscribe: "Kjo përmbledhje dërgohet nga %{site_link} ku nuk ju kemi parë në faqe për ca kohë. Ndryshoni preferencat tuaja, ose %{unsubscribe_link} për të mos i marrë më këto mesazhe." click_here: "klikoni këtu" - from: "%{site_name} - Një përmbledhje e shkurtër" + from: "%{site_name} - përmbledhja" + preheader: "Një përmbledhe e shkurtër e faqes që nga vizita juaj e fundit më %{last_seen_at}" notify_old_email: text_body_template: | Ky është një mesazh automatik që ju njofton se adresa juaj email për @@ -1189,7 +1175,7 @@ sq: search_google: "Google" login_required: welcome_message: | - #[Welcome to %{title}](#welcome) + ## [Welcome to %{title}](#welcome) An account is required. Please create an account or log in to continue. terms_of_service: title: "Kushtet e shërbimit" diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 6ac8c593d3..9387c78ea4 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -818,7 +818,6 @@ sv: notify_mods_when_user_blocked: "Om en användare blockeras automatiskt, skicka ett meddelande till alla moderatorer." flag_sockpuppets: "Flagga båda användarnas inlägg som potentiell skräppost om en ny användare svarar på ett ämne från samma IP-adress som den andra nya användaren som skapade ämnet." traditional_markdown_linebreaks: "Använd vanliga radmatningar i Markdown, vilka kräver 2 avslutande mellanslag för en radmatning." - allow_html_tables: "Tillåt tabeller att läggas in i Markdown genom användning av HTML-taggar. TABLE, THEAD, TD, TR, TH kommer att vitlistas (kräver full uppdatering/rebake av alla gamla inlägg som innehåller tabeller)" post_undo_action_window_mins: "Antal minuter som en användare tillåts att upphäva handlingar på ett inlägg som gjorts nyligen (gillning, flaggning osv)." must_approve_users: "Personal måste godkänna alla nya användarkonton innan de tillåts använda webbplatsen. VARNING: om det tillåts när webbplatsen är live så kommer det att upphäva tillgång för alla existerande användare som inte är personal!" pending_users_reminder_delay: "Notifiera moderatorer om nya användare har väntat på godkännande längre än så här många timmar. Ange -1 för att inaktivera notifikationer. " @@ -1650,26 +1649,6 @@ sv: visit_link_to_respond: "[Besök ämnet](%{base_url}%{url}) för att ge din respons. " visit_link_to_respond_pm: "[Besök meddelanden](%{base_url}%{url}) för att ge din respons." posted_by: "Postat av %{username} den %{post_date}" - invited_to_private_message_body: | - %{username} bjöd in dit till ett meddelande - - > **%{topic_title}** - > - > %{topic_excerpt} - - vid - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} bjöd in dig till en diskussion - - > **%{topic_title}** - > - > %{topic_excerpt} - - vid - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "Användare invitera personligt" text_body_template: | diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 0430d47a47..30d63f631a 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -705,7 +705,6 @@ tr_TR: notify_mods_when_user_blocked: "Eğer bir kullanıcı otomatik olarak engellendiyse, tüm moderatörlere ileti yolla." flag_sockpuppets: "Eğer, yeni kullanıcı konuya, konuyu başlatan yeni kullanıcı ile aynı IP adresinden cevap yazarsa, her iki gönderiyi de potansiyel istenmeyen olarak bildir. " traditional_markdown_linebreaks: "Markdown'da, satır sonundan önce yazının sağında iki tane boşluk gerektiren, geleneksel satır sonu metodunu kullan." - allow_html_tables: "Tabloların HTML etiketleri kullanılarak Markdown ile oluşturulmasına izin verin. TABLE, THEAD, TD, TR, TH kabul edilir (tablo içeren tüm eski gönderilerin yenilenmesini gerektirir) " post_undo_action_window_mins: "Bir gönderide yapılan yeni eylemlerin (beğenme, bildirme vb) geri alınabileceği zaman, dakika olarak" must_approve_users: "Siteye erişimlerine izin verilmeden önce tüm yeni kullanıcı hesaplarının görevliler tarafından onaylanması gerekir. UYARI: yayındaki bir site için bunu etkinleştirmek görevli olmayan hesapların erişimini iptal edecek." pending_users_reminder_delay: "Belirtilen saatten daha uzun bir süredir onay bekleyen yeni kullanıcılar mevcutsa moderatörleri bilgilendir. Bilgilendirmeyi devre dışı bırakmak için -1 girin." @@ -1325,26 +1324,6 @@ tr_TR: visit_link_to_respond: "Cevaplamak için [konuyu ziyaret edin](%{base_url}%{url})." visit_link_to_respond_pm: "Cevaplamak için [iletiyi ziyaret edin](%{base_url}%{url})." posted_by: "%{post_date} tarihinde %{username} tarafından gönderildi" - invited_to_private_message_body: | - %{username} sizi bir iletiye davet etti - - > **%{topic_title}** - > - > %{topic_excerpt} - - şurada - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} sizi bir tartışmaya davet etti - - > **%{topic_title}** - > - > %{topic_excerpt} - - şurada - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: text_body_template: | %{header_instructions} diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index 62f13094af..8445258d57 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -681,7 +681,6 @@ vi: notify_mods_when_user_blocked: "Nếu một thành viên được khóa tự động, gửi tin nhắn đến tất cả các điều hành viên." flag_sockpuppets: "Nếu thành viên mới trả lời chủ đề có cùng địa chỉ IP với thành viên mới tạo chủ đề, đánh dấu các bài viết của họ là spam tiềm năng." traditional_markdown_linebreaks: "Sử dụng ngắt dòng truyền thống trong Markdown, đòi hỏi hai khoảng trống kế tiếp cho một ngắt dòng." - allow_html_tables: "Cho phép nhập bảng trong Markdown sử dụng các thẻ HTML. TABLE, THEAD, TD, TR, TH sẽ được sử dụng (đòi hỏi thực hiện lại cho các bài viết cũ có chứa bảng)" post_undo_action_window_mins: "Số phút thành viên được phép làm lại các hành động gần đây với bài viết (like, đánh dấu...)." must_approve_users: "Quản trị viên phải duyệt tất cả các tài khoản thành viên mới trước khi họ có quyền truy cập website. LƯU Ý: bật tính năng này trên site đang hoạt động sẽ hủy bỏ quyền truy cập đối với các tài khoản thành viên hiện tại!" pending_users_reminder_delay: "Thông báo cho quản trị viên nếu thành viên mới đã chờ duyệt lâu hơn số giờ được thiết lập ở đây, đặt là -1 để tắt thông báo." @@ -1286,26 +1285,6 @@ vi: description: "Bạn không thích nhận mail giống mail này? Nhấn vào bỏ theo dõi để bỏ đăng ký ngay lập tức:" only_reply_by_email: "Trả lời email này để phản hồi." posted_by: "Đăng bởi %{{username} ngày %{post_date}" - invited_to_private_message_body: | - %{username} mời bạn xem - - > **%{topic_title}** - > - > %{topic_excerpt} - - tại - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} mời bạn thảo luận - - > **%{topic_title}** - > - > %{topic_excerpt} - - tại - - > %{site_title} -- %{site_description} user_posted_pm_staged: subject_template: "%{optional_re}%{topic_title}" digest: diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 297f1c3e33..27008d68fa 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -347,7 +347,7 @@ zh_CN: meta_category_description: "讨论该站点的站务、组织、如何运作和如何改善它。" staff_category_name: "管理人员" staff_category_description: "管理人员可见的分类。只有管理员和版主才能阅览主题。" - assets_topic_title: "站点设计资料" + assets_topic_title: "站点设计资源" discourse_welcome_topic: title: "欢迎来到 Discourse" body: |2 @@ -881,7 +881,6 @@ zh_CN: notify_mods_when_user_blocked: "如果一个用户被自动封禁了,发送一个私信给所有管理员。" flag_sockpuppets: "如果一个新用户开始了一个主题,并且同时另一个新用户以同一个 IP 在该主题回复,他们所有的帖子都将被自动标记为垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用传统换行符,即用两个尾随空格来换行" - allow_html_tables: "允许在输入 Markdown 文本时使用表格 HTML 标签。标签 TABLE、THEAD、TD、TR、TH 将被允许使用,即白名单这些标签(需要重置所有包含表格的老帖子的 HTML)" post_undo_action_window_mins: "允许用户在帖子上进行撤销操作(赞、标记等)所需等待的间隔分钟数" must_approve_users: "新用户在被允许访问站点前需要由管理人员批准。警告:在运行的站点中启用将解除所有非管理人员用户的访问权限!" pending_users_reminder_delay: "如果新用户等待批准时间超过此小时设置则通知版主。设置 -1 关闭通知。" @@ -1462,37 +1461,9 @@ zh_CN: invite_mailer: title: "要求发件人" subject_template: "%{invitee_name} 邀请你参与 %{site_domain_name} 主题 '%{topic_title}' " - text_body_template: | - %{invitee_name}邀请你参与讨论: - - > **%{topic_title}** - > - > %{topic_excerpt} - - 社区: - - > %{site_title} -- %{site_description} - - 如果你有兴趣,点击下方的链接: - - %{invite_link} custom_invite_mailer: title: "自定义邀请发件人" subject_template: "%{invitee_name}邀请你'%{topic_title}'在%{site_domain_name}" - text_body_template: | - %{invitee_name}邀请你参与讨论: - - > **%{topic_title}** - > - > %{topic_excerpt} - - 社区: - - > %{site_title} -- %{site_description} - - 如果你有兴趣,点击下方的链接: - - %{invite_link} invite_forum_mailer: title: "邀请论坛发件人" subject_template: "%{invitee_name} 邀请你加入 %{site_domain_name}" @@ -2033,26 +2004,6 @@ zh_CN: visit_link_to_respond: "[访问主题](%{base_url}%{url})以回复." visit_link_to_respond_pm: "[访问私信](%{base_url}%{url})以回复." posted_by: "%{username}发表于%{post_date}" - invited_to_private_message_body: | - %{username} 邀请你至私信交流: - - > **%{topic_title}** - > - > %{topic_excerpt} - - 论坛: - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} 邀请你参与讨论: - - > **%{topic_title}** - > - > %{topic_excerpt} - - 论坛: - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "邀请用户至私信" subject_template: "[%{email_prefix}] %{username}邀请你至私信“%{topic_title}”" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index ebd13f4af5..bd0659b579 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -832,7 +832,6 @@ zh_TW: notify_mods_when_user_blocked: "若有用戶被自動封鎖,將發送訊息給所有板主。" flag_sockpuppets: "如果一個新用戶開始了一個主題,並且同時另一個新用戶以同一個 IP 在該主題回復,他們所有的帖子都將被自動標記為垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用傳統的換行符號,即用兩個行末空格來換行" - allow_html_tables: "允許在輸入 Markdown 文本時使用表格 HTML 標籤。標籤 TABLE、THEAD、TD、TR、TH 將被允許使用,即白名單這些標籤(需要重置所有包含表格的老帖子的 HTML)" post_undo_action_window_mins: "允許用戶在帖子上進行撤銷操作(讚、標記等)所需等待的時間分隔(分鐘)" must_approve_users: "新用戶在被允許訪問站點前需要由管理人員批准。警告:在運行的站點中啟用將解除所有非管理人員用戶的訪問權限!" pending_users_reminder_delay: "如果新用戶等待批准時間超過此小時設置則通知版主。設置 -1 關閉通知。" @@ -1375,41 +1374,9 @@ zh_TW: invite_mailer: title: "邀請 Mailer" subject_template: "%{invitee_name} 邀請你參與在 %{site_domain_name} 討論的話題 '%{topic_title}'" - text_body_template: | - %{invitee_name} 邀請你參與位於 - - > %{site_title} -- %{site_description} - - 的以下討論 - - > **%{topic_title}** - > - > %{topic_excerpt} - - 如果你有興趣,請點擊下面的連結: - - %{invite_link} custom_invite_mailer: title: "客製邀請 Mailer" subject_template: "%{invitee_name}邀請你'%{topic_title}'在%{site_domain_name}" - text_body_template: | - %{invitee_name} 留下以下留言 - - > %{user_custom_message} - - 邀請你參與位於 - - > %{site_title} -- %{site_description} - - 的討論 - - > **%{topic_title}** - > - > %{topic_excerpt} - - 如果你有興趣,請點擊下面的連結: - - %{invite_link} invite_forum_mailer: title: "邀請論壇 Mailer" subject_template: "%{invitee_name} 邀請你加入 %{site_domain_name}" @@ -1858,26 +1825,6 @@ zh_TW: visit_link_to_respond: "[訪問主題](%{base_url}%{url})以回覆。" visit_link_to_respond_pm: "[訪問\"私訊\"](%{base_url}%{url})以回覆。" posted_by: "由 %{username} 張貼於 %{post_date}" - invited_to_private_message_body: | - %{username} 邀請你至消息交流: - - > **%{topic_title}** - > - > %{topic_excerpt} - - 論壇: - - > %{site_title} -- %{site_description} - invited_to_topic_body: | - %{username} 邀請你參與討論: - - > **%{topic_title}** - > - > %{topic_excerpt} - - 論壇: - - > %{site_title} -- %{site_description} user_invited_to_private_message_pm: title: "用戶被邀請至私人訊息" text_body_template: | diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index e7741c1eb3..65b631aec3 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -121,7 +121,7 @@ server { } # cache emojis - location ~ /_?emoji.*\.(png|gif|jpg|jpeg)$/ { + location ~ /images/emoji/ { expires 1y; add_header Cache-Control public,immutable; } diff --git a/config/puma.rb b/config/puma.rb index d472c7278f..85f38250ec 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,4 +1,4 @@ -if ENV['RAILS_ENV']=='production' +if ENV['RAILS_ENV'] == 'production' # First, you need to change these below to your situation. APP_ROOT = '/home/discourse/discourse' @@ -6,11 +6,11 @@ if ENV['RAILS_ENV']=='production' # Second, you can choose how many threads that you are going to run at same time. workers "#{num_workers}" - threads 8,32 + threads 8, 32 # Unless you know what you are changing, do not change them. - bind "unix://#{APP_ROOT}/tmp/sockets/puma.sock" - stdout_redirect "#{APP_ROOT}/log/puma.log","#{APP_ROOT}/log/puma.err.log" + bind "unix://#{APP_ROOT}/tmp/sockets/puma.sock" + stdout_redirect "#{APP_ROOT}/log/puma.log", "#{APP_ROOT}/log/puma.err.log" pidfile "#{APP_ROOT}/tmp/pids/puma.pid" state_path "#{APP_ROOT}/tmp/pids/puma.state" daemonize true diff --git a/config/routes.rb b/config/routes.rb index ebc92dc8df..553bbc9e5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -130,12 +130,11 @@ Discourse::Application.routes.draw do put "anonymize" post "reset_bounce_score" end - get "users/:id.json" => 'users#show', defaults: {format: 'json'} - get 'users/:id/:username' => 'users#show', constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:id.json" => 'users#show', defaults: { format: 'json' } + get 'users/:id/:username' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT } get 'users/:id/:username/badges' => 'users#show' get 'users/:id/:username/tl3_requirements' => 'users#show' - post "users/sync_sso" => "users#sync_sso", constraints: AdminConstraint.new post "users/invite_admin" => "users#invite_admin", constraints: AdminConstraint.new @@ -169,6 +168,12 @@ Discourse::Application.routes.draw do end end resources :screened_urls, only: [:index] + resources :watched_words, only: [:index, :create, :update, :destroy] do + collection do + get "action/:id" => "watched_words#index" + end + end + post "watched_words/upload" => "watched_words#upload" end get "/logs" => "staff_action_logs#index" @@ -266,10 +271,9 @@ Discourse::Application.routes.draw do end end - get "memory_stats"=> "diagnostics#memory_stats", constraints: AdminConstraint.new - get "dump_heap"=> "diagnostics#dump_heap", constraints: AdminConstraint.new - get "dump_statement_cache"=> "diagnostics#dump_statement_cache", constraints: AdminConstraint.new - + get "memory_stats" => "diagnostics#memory_stats", constraints: AdminConstraint.new + get "dump_heap" => "diagnostics#dump_heap", constraints: AdminConstraint.new + get "dump_statement_cache" => "diagnostics#dump_statement_cache", constraints: AdminConstraint.new end # admin namespace get "email_preferences" => "email#preferences_redirect", :as => "email_preferences_redirect" @@ -327,7 +331,7 @@ Discourse::Application.routes.draw do post "#{root_path}/read-faq" => "users#read_faq" get "#{root_path}/search/users" => "users#search_users" - get({ "#{root_path}/account-created/" => "users#account_created" }.merge(index == 1 ? { as: :users_account_created } : {as: :old_account_created})) + get({ "#{root_path}/account-created/" => "users#account_created" }.merge(index == 1 ? { as: :users_account_created } : { as: :old_account_created })) get "#{root_path}/account-created/resent" => "users#account_created" get "#{root_path}/account-created/edit-email" => "users#account_created" @@ -342,63 +346,63 @@ Discourse::Application.routes.draw do constraints: { token: /[0-9a-f]+/ } }.merge(index == 1 ? { as: 'confirm_admin' } : {})) post "#{root_path}/confirm-admin/:token" => "users#confirm_admin", constraints: { token: /[0-9a-f]+/ } - get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username.json" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: {format: :json} + get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username.json" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT }, defaults: { format: :json } get({ "#{root_path}/:username" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT, format: /(json|html)/ } }.merge(index == 1 ? { as: 'user' } : {})) - put "#{root_path}/:username" => "users#update", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: { format: :json } - get "#{root_path}/:username/emails" => "users#check_emails", constraints: {username: USERNAME_ROUTE_FORMAT} + put "#{root_path}/:username" => "users#update", constraints: { username: USERNAME_ROUTE_FORMAT }, defaults: { format: :json } + get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: USERNAME_ROUTE_FORMAT } get({ "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } }.merge(index == 1 ? { as: :email_preferences } : {})) - get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT} - put "#{root_path}/:username/preferences/username" => "users#username", constraints: {username: USERNAME_ROUTE_FORMAT} - delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: {username: USERNAME_ROUTE_FORMAT} - put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: {username: USERNAME_ROUTE_FORMAT} - put "#{root_path}/:username/preferences/card-badge" => "users#update_card_badge", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/summary" => "users#summary", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/invited" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/about-me" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: USERNAME_ROUTE_FORMAT } + put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: USERNAME_ROUTE_FORMAT } + delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: USERNAME_ROUTE_FORMAT } + put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: { username: USERNAME_ROUTE_FORMAT } + put "#{root_path}/:username/preferences/card-badge" => "users#update_card_badge", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/summary" => "users#summary", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/invited" => "users#invited", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: USERNAME_ROUTE_FORMAT } post "#{root_path}/action/send_activation_email" => "users#send_activation_email" - get "#{root_path}/:username/summary" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/notifications" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/activity/pending" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - delete "#{root_path}/:username" => "users#destroy", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/by-external/:external_id" => "users#show", constraints: {external_id: /[^\/]+/} - get "#{root_path}/:username/flagged-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/deleted-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: {username: USERNAME_ROUTE_FORMAT} + get "#{root_path}/:username/summary" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/activity" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/badges" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/notifications" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/activity/pending" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + delete "#{root_path}/:username" => "users#destroy", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/by-external/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ } + get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: USERNAME_ROUTE_FORMAT } + get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: USERNAME_ROUTE_FORMAT } end - get "user-badges/:username.json" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: {format: :json} - get "user-badges/:username" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT} + get "user-badges/:username.json" => "user_badges#username", constraints: { username: USERNAME_ROUTE_FORMAT }, defaults: { format: :json } + get "user-badges/:username" => "user_badges#username", constraints: { username: USERNAME_ROUTE_FORMAT } - post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: {username: USERNAME_ROUTE_FORMAT} - get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT} + post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: USERNAME_ROUTE_FORMAT } + get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT } get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT } # in most production settings this is bypassed @@ -422,8 +426,8 @@ Discourse::Application.routes.draw do get "private-posts" => "posts#latest", id: "private_posts" get "posts/by_number/:topic_id/:post_number" => "posts#by_number" get "posts/:id/reply-history" => "posts#reply_history" - get "posts/:username/deleted" => "posts#deleted_posts", constraints: {username: USERNAME_ROUTE_FORMAT} - get "posts/:username/flagged" => "posts#flagged_posts", constraints: {username: USERNAME_ROUTE_FORMAT} + get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: USERNAME_ROUTE_FORMAT } + get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: USERNAME_ROUTE_FORMAT } resources :groups, id: USERNAME_ROUTE_FORMAT do get "posts.rss" => "groups#posts_feed", format: :rss @@ -440,6 +444,10 @@ Discourse::Application.routes.draw do get 'mentionable' get 'logs' => 'groups#histories' + collection do + get "search" => "groups#search" + end + member do put "members" => "groups#add_members" delete "members" => "groups#remove_member" @@ -501,10 +509,9 @@ Discourse::Application.routes.draw do get "/badges/:id(/:slug)" => "badges#show" resources :user_badges, only: [:index, :create, :destroy] - get '/c', to: redirect('/categories') - resources :categories, :except => :show + resources :categories, except: :show post "category/:category_id/move" => "categories#move" post "categories/reorder" => "categories#reorder" post "category/:category_id/notifications" => "categories#set_notifications" @@ -550,6 +557,7 @@ Discourse::Application.routes.draw do get "top" => "list#top" get "search/query" => "search#query" get "search" => "search#show" + post "search/click" => "search#click" # Topics resource get "t/:id" => "topics#show" @@ -566,11 +574,11 @@ Discourse::Application.routes.draw do resources :similar_topics get "topics/feature_stats" - get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT} - get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} - get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: {username: USERNAME_ROUTE_FORMAT} - get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: {username: USERNAME_ROUTE_FORMAT} - get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: {username: USERNAME_ROUTE_FORMAT} + get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: { username: USERNAME_ROUTE_FORMAT } + get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: { username: USERNAME_ROUTE_FORMAT } + get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: { username: USERNAME_ROUTE_FORMAT } + get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: { username: USERNAME_ROUTE_FORMAT } + get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: { username: USERNAME_ROUTE_FORMAT } get "topics/private-messages-group/:username/:group_name.json" => "list#private_messages_group", as: "topics_private_messages_group", constraints: { username: USERNAME_ROUTE_FORMAT, group_name: USERNAME_ROUTE_FORMAT @@ -590,49 +598,49 @@ Discourse::Application.routes.draw do # Topic routes get "t/id_for/:slug" => "topics#id_for_slug" - get "t/:slug/:topic_id/print" => "topics#show", format: :html, print: true, constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/} - get "t/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id/summary" => "topics#show", defaults: {summary: true}, constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id/unsubscribe" => "topics#unsubscribe", constraints: {topic_id: /\d+/} - get "t/:topic_id/unsubscribe" => "topics#unsubscribe", constraints: {topic_id: /\d+/} - get "t/:topic_id/summary" => "topics#show", constraints: {topic_id: /\d+/} - put "t/:slug/:topic_id" => "topics#update", constraints: {topic_id: /\d+/} - put "t/:slug/:topic_id/star" => "topics#star", constraints: {topic_id: /\d+/} - put "t/:topic_id/star" => "topics#star", constraints: {topic_id: /\d+/} - put "t/:slug/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/} - put "t/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/} - put "t/:topic_id/clear-pin" => "topics#clear_pin", constraints: {topic_id: /\d+/} - put "t/:topic_id/re-pin" => "topics#re_pin", constraints: {topic_id: /\d+/} - put "t/:topic_id/mute" => "topics#mute", constraints: {topic_id: /\d+/} - put "t/:topic_id/unmute" => "topics#unmute", constraints: {topic_id: /\d+/} - post "t/:topic_id/timer" => "topics#timer", constraints: {topic_id: /\d+/} - put "t/:topic_id/make-banner" => "topics#make_banner", constraints: {topic_id: /\d+/} - put "t/:topic_id/remove-banner" => "topics#remove_banner", constraints: {topic_id: /\d+/} - put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: {topic_id: /\d+/} - put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group", constraints: {topic_id: /\d+/} - put "t/:topic_id/recover" => "topics#recover", constraints: {topic_id: /\d+/} - get "t/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/} - get "t/:topic_id/last" => "topics#show", post_number: 99999999, constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id.rss" => "topics#feed", format: :rss, constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id" => "topics#show", constraints: {topic_id: /\d+/} - get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/} - get "t/:slug/:topic_id/last" => "topics#show", post_number: 99999999, constraints: {topic_id: /\d+/} - get "t/:topic_id/posts" => "topics#posts", constraints: {topic_id: /\d+/}, format: :json - get "t/:topic_id/excerpts" => "topics#excerpts", constraints: {topic_id: /\d+/}, format: :json - post "t/:topic_id/timings" => "topics#timings", constraints: {topic_id: /\d+/} - post "t/:topic_id/invite" => "topics#invite", constraints: {topic_id: /\d+/} - post "t/:topic_id/invite-group" => "topics#invite_group", constraints: {topic_id: /\d+/} - post "t/:topic_id/move-posts" => "topics#move_posts", constraints: {topic_id: /\d+/} - post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: {topic_id: /\d+/} - post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: {topic_id: /\d+/} - put "t/:topic_id/change-timestamp" => "topics#change_timestamps", constraints: {topic_id: /\d+/} - delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: {topic_id: /\d+/} - put "t/:topic_id/bookmark" => "topics#bookmark", constraints: {topic_id: /\d+/} - put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: {topic_id: /\d+/} + get "t/:slug/:topic_id/print" => "topics#show", format: :html, print: true, constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: { topic_id: /\d+/ } + get "t/:topic_id/wordpress" => "topics#wordpress", constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id/summary" => "topics#show", defaults: { summary: true }, constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id/unsubscribe" => "topics#unsubscribe", constraints: { topic_id: /\d+/ } + get "t/:topic_id/unsubscribe" => "topics#unsubscribe", constraints: { topic_id: /\d+/ } + get "t/:topic_id/summary" => "topics#show", constraints: { topic_id: /\d+/ } + put "t/:slug/:topic_id" => "topics#update", constraints: { topic_id: /\d+/ } + put "t/:slug/:topic_id/star" => "topics#star", constraints: { topic_id: /\d+/ } + put "t/:topic_id/star" => "topics#star", constraints: { topic_id: /\d+/ } + put "t/:slug/:topic_id/status" => "topics#status", constraints: { topic_id: /\d+/ } + put "t/:topic_id/status" => "topics#status", constraints: { topic_id: /\d+/ } + put "t/:topic_id/clear-pin" => "topics#clear_pin", constraints: { topic_id: /\d+/ } + put "t/:topic_id/re-pin" => "topics#re_pin", constraints: { topic_id: /\d+/ } + put "t/:topic_id/mute" => "topics#mute", constraints: { topic_id: /\d+/ } + put "t/:topic_id/unmute" => "topics#unmute", constraints: { topic_id: /\d+/ } + post "t/:topic_id/timer" => "topics#timer", constraints: { topic_id: /\d+/ } + put "t/:topic_id/make-banner" => "topics#make_banner", constraints: { topic_id: /\d+/ } + put "t/:topic_id/remove-banner" => "topics#remove_banner", constraints: { topic_id: /\d+/ } + put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: { topic_id: /\d+/ } + put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group", constraints: { topic_id: /\d+/ } + put "t/:topic_id/recover" => "topics#recover", constraints: { topic_id: /\d+/ } + get "t/:topic_id/:post_number" => "topics#show", constraints: { topic_id: /\d+/, post_number: /\d+/ } + get "t/:topic_id/last" => "topics#show", post_number: 99999999, constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id.rss" => "topics#feed", format: :rss, constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id" => "topics#show", constraints: { topic_id: /\d+/ } + get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: { topic_id: /\d+/, post_number: /\d+/ } + get "t/:slug/:topic_id/last" => "topics#show", post_number: 99999999, constraints: { topic_id: /\d+/ } + get "t/:topic_id/posts" => "topics#posts", constraints: { topic_id: /\d+/ }, format: :json + get "t/:topic_id/excerpts" => "topics#excerpts", constraints: { topic_id: /\d+/ }, format: :json + post "t/:topic_id/timings" => "topics#timings", constraints: { topic_id: /\d+/ } + post "t/:topic_id/invite" => "topics#invite", constraints: { topic_id: /\d+/ } + post "t/:topic_id/invite-group" => "topics#invite_group", constraints: { topic_id: /\d+/ } + post "t/:topic_id/move-posts" => "topics#move_posts", constraints: { topic_id: /\d+/ } + post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: { topic_id: /\d+/ } + post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: { topic_id: /\d+/ } + put "t/:topic_id/change-timestamp" => "topics#change_timestamps", constraints: { topic_id: /\d+/ } + delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: { topic_id: /\d+/ } + put "t/:topic_id/bookmark" => "topics#bookmark", constraints: { topic_id: /\d+/ } + put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: { topic_id: /\d+/ } - post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: {topic_id: /\d+/} + post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: { topic_id: /\d+/ } get "p/:post_id(/:user_id)" => "posts#short_link" get "/posts/:id/cooked" => "posts#cooked" @@ -663,6 +671,7 @@ Discourse::Application.routes.draw do end get "onebox" => "onebox#show" + get "inline-onebox" => "inline_onebox#show" get "exception" => "list#latest" @@ -712,12 +721,12 @@ Discourse::Application.routes.draw do end Discourse.filters.each do |filter| - root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), :as => "list_#{filter}" + root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), as: "list_#{filter}" end # special case for categories - root to: "categories#index", constraints: HomePageConstraint.new("categories"), :as => "categories_index" + root to: "categories#index", constraints: HomePageConstraint.new("categories"), as: "categories_index" # special case for top - root to: "list#top", constraints: HomePageConstraint.new("top"), :as => "top_lists" + root to: "list#top", constraints: HomePageConstraint.new("top"), as: "top_lists" root to: 'finish_installation#index', constraints: HomePageConstraint.new("finish_installation"), as: 'installation_redirect' diff --git a/config/site_settings.yml b/config/site_settings.yml index fd487d97a6..ed2147124e 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -103,6 +103,9 @@ basic: ga_universal_domain_name: client: true default: 'auto' + ga_universal_auto_link_domains: + default: '' + type: list ga_tracking_code: client: true default: '' @@ -492,18 +495,12 @@ posting: delete_removed_posts_after: client: true default: 24 - enable_experimental_markdown_it: - client: true - default: false traditional_markdown_linebreaks: client: true default: false enable_markdown_typographer: client: true - default: false - allow_html_tables: - client: true - default: false + default: true suppress_reply_directly_below: client: true default: true @@ -558,11 +555,6 @@ posting: type: list client: true delete_old_hidden_posts: true - censored_words: - client: true - default: '' - refresh: true - type: list censored_pattern: client: true default: '' @@ -641,7 +633,7 @@ email: default: '' validator: "AlternativeReplyByEmailAddressesValidator" find_related_post_with_key: - default: false + default: true manual_polling_enabled: default: false pop3_polling_enabled: @@ -814,6 +806,7 @@ files: allow_staff_to_upload_any_file_in_pm: default: true client: true + strip_image_metadata: true trust: default_trust_level: @@ -923,6 +916,9 @@ onebox: max_oneboxes_per_post: default: 50 client: true + inline_onebox_domains_whitelist: + default: '' + type: list spam: add_rel_nofollow_to_user_content: true @@ -1113,7 +1109,7 @@ legal: backups: readonly_mode_during_backup: - default: true + default: false allow_restore: default: false maximum_backups: @@ -1150,6 +1146,8 @@ search: search_tokenize_chinese_japanese_korean: false search_prefer_recent_posts: false search_recent_posts_size: 100000 + log_search_queries: true + search_query_log_max_size: 1000000 uncategorized: version_checks: diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index 716c96cc98..e0d0894cec 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -126,9 +126,9 @@ before_fork do |server, worker| def max_rss rss = `ps -eo rss,args | grep sidekiq | grep -v grep | awk '{print $1}'` - .split("\n") - .map(&:to_i) - .max + .split("\n") + .map(&:to_i) + .max rss ||= 0 @@ -146,9 +146,9 @@ before_fork do |server, worker| def force_kill_rogue_sidekiq info = `ps -eo pid,rss,args | grep sidekiq | grep -v grep | awk '{print $1,$2}'` info.split("\n").each do |row| - pid,mem = row.split(" ").map(&:to_i) - if pid > 0 && (mem*1024) > max_allowed_size - Rails.logger.warn "Detected rogue Sidekiq pid #{pid} mem #{mem*1024}, killing" + pid, mem = row.split(" ").map(&:to_i) + if pid > 0 && (mem * 1024) > max_allowed_size + Rails.logger.warn "Detected rogue Sidekiq pid #{pid} mem #{mem * 1024}, killing" Process.kill("KILL", pid) rescue nil end end @@ -199,7 +199,6 @@ before_fork do |server, worker| ActiveRecord::Base.connection.disconnect! $redis.client.disconnect - # Throttle the master from forking too quickly by sleeping. Due # to the implementation of standard Unix signal handlers, this # helps (but does not completely) prevent identical, repeated signals diff --git a/db/fixtures/001_categories.rb b/db/fixtures/001_categories.rb index 1a0282b84e..4dc13268ac 100644 --- a/db/fixtures/001_categories.rb +++ b/db/fixtures/001_categories.rb @@ -30,7 +30,7 @@ ColumnDropper.drop( table: 'categories', after_migration: 'AddUploadsToCategories', columns: ['logo_url', 'background_url'], - on_drop: ->(){ + on_drop: ->() { STDERR.puts 'Removing superflous categories columns!' } ) diff --git a/db/fixtures/002_groups.rb b/db/fixtures/002_groups.rb index 68912df848..d7ae62c944 100644 --- a/db/fixtures/002_groups.rb +++ b/db/fixtures/002_groups.rb @@ -7,9 +7,9 @@ Group.where(name: 'everyone').update_all(visibility_level: Group.visibility_leve ColumnDropper.drop( table: 'groups', - after_migration: 'AddVisibleBackToGroups', - columns: %w[visible], - on_drop: ->(){ + after_migration: 'RemovePublicFromGroups', + columns: %w[visible public], + on_drop: ->() { STDERR.puts 'Removing superflous visible group column!' } ) diff --git a/db/fixtures/006_badges.rb b/db/fixtures/006_badges.rb index a8f55dd6f8..22cf2c7714 100644 --- a/db/fixtures/006_badges.rb +++ b/db/fixtures/006_badges.rb @@ -142,9 +142,9 @@ Badge.seed do |b| end [ - [Badge::Promoter,"Promoter",BadgeType::Bronze,1,0], - [Badge::Campaigner,"Campaigner",BadgeType::Silver,3,1], - [Badge::Champion,"Champion",BadgeType::Gold,5,2], + [Badge::Promoter, "Promoter", BadgeType::Bronze, 1, 0], + [Badge::Campaigner, "Campaigner", BadgeType::Silver, 3, 1], + [Badge::Champion, "Champion", BadgeType::Gold, 5, 2], ].each do |id, name, type, count, trust_level| Badge.seed do |b| b.id = id @@ -154,7 +154,7 @@ end b.multiple_grant = false b.target_posts = false b.show_posts = false - b.query = BadgeQueries.invite_badge(count,trust_level) + b.query = BadgeQueries.invite_badge(count, trust_level) b.default_badge_grouping_id = BadgeGrouping::Community # daily is good enough b.trigger = Badge::Trigger::None @@ -307,7 +307,6 @@ end end end - [ [Badge::ThankYou, "Thank You", BadgeType::Bronze, 20, 10], [Badge::GivesBack, "Gives Back", BadgeType::Silver, 100, 100], diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index d725d78a41..937faa20cf 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -6,12 +6,18 @@ if user user.save end +UserEmail.seed do |ue| + ue.id = -1 + ue.email = "no_email" + ue.primary = true + ue.user_id = -1 +end + User.seed do |u| u.id = -1 u.name = "system" u.username = "system" u.username_lower = "system" - u.email = "no_email" u.password = SecureRandom.hex u.active = true u.admin = true @@ -27,7 +33,6 @@ UserOption.where(user_id: -1).update_all( Group.user_trust_level_change!(-1, TrustLevel[4]) - ColumnDropper.drop( table: 'users', after_migration: 'AddUserAuthTokens', @@ -49,19 +54,25 @@ ColumnDropper.drop( last_redirected_to_top_at auth_token auth_token_updated_at ], - on_drop: ->(){ + on_drop: ->() { STDERR.puts 'Removing superflous users columns!' } ) # User for the smoke tests if ENV["SMOKE"] == "1" + UserEmail.seed do |ue| + ue.id = 0 + ue.email = "smoke_user@discourse.org" + ue.primary = true + ue.user_id = 0 + end + smoke_user = User.seed do |u| u.id = 0 u.name = "smoke_user" u.username = "smoke_user" u.username_lower = "smoke_user" - u.email = "smoke_user@discourse.org" u.password = "P4ssw0rd" u.active = true u.approved = true @@ -77,4 +88,3 @@ if ENV["SMOKE"] == "1" EmailToken.where(user_id: smoke_user.id).update_all(confirmed: true) end - diff --git a/db/fixtures/501_meta_category.rb b/db/fixtures/501_meta_category.rb index 9996b2abc8..38ed96aaea 100644 --- a/db/fixtures/501_meta_category.rb +++ b/db/fixtures/501_meta_category.rb @@ -17,7 +17,7 @@ unless Rails.env.test? raise "Failed meta topic" end - meta.set_permissions(:everyone => :full) + meta.set_permissions(everyone: :full) meta.topic_id = post.topic.id unless meta.save puts meta.errors.full_messages diff --git a/db/fixtures/502_staff_category.rb b/db/fixtures/502_staff_category.rb index f1b62c1bf2..54faa6385b 100644 --- a/db/fixtures/502_staff_category.rb +++ b/db/fixtures/502_staff_category.rb @@ -1,6 +1,6 @@ unless Rails.env.test? staff = Category.find_by(id: SiteSetting.staff_category_id) - if staff and !staff.group_ids.include?(Group[:staff].id) + if staff && !staff.group_ids.include?(Group[:staff].id) # Add permissions and a description to the Staff category. diff --git a/db/fixtures/600_themes.rb b/db/fixtures/600_themes.rb index 0ec6e274d2..5b141c57ad 100644 --- a/db/fixtures/600_themes.rb +++ b/db/fixtures/600_themes.rb @@ -7,13 +7,13 @@ if !Theme.exists? dark_scheme ||= ColorScheme.create_from_base(name: name, via_wizard: true, base_scheme_id: "dark") name = I18n.t('color_schemes.dark_theme_name') - _dark_theme = Theme.create(name: name, user_id: -1, - color_scheme_id: dark_scheme.id, - user_selectable: true) + _dark_theme = Theme.create(name: name, user_id: -1, + color_scheme_id: dark_scheme.id, + user_selectable: true) name = I18n.t('color_schemes.default_theme_name') - default_theme = Theme.create(name: name, user_id: -1, - user_selectable: true) + default_theme = Theme.create(name: name, user_id: -1, + user_selectable: true) default_theme.set_default! end @@ -22,7 +22,7 @@ ColumnDropper.drop( table: 'theme_fields', after_migration: 'AddUploadIdToThemeFields', columns: ['target'], - on_drop: ->(){ + on_drop: ->() { STDERR.puts 'Removing superflous theme_fields target column!' } ) diff --git a/db/fixtures/999_delayed.rb b/db/fixtures/999_delayed.rb index 1141e13e64..4d69a58267 100644 --- a/db/fixtures/999_delayed.rb +++ b/db/fixtures/999_delayed.rb @@ -6,7 +6,7 @@ TableMigrationHelper.delayed_drop( old_name: 'topic_status_updates', new_name: 'topic_timers', after_migration: 'RenameTopicStatusUpdatesToTopicTimers', - on_drop: ->(){ + on_drop: ->() { STDERR.puts "Dropping topic_status_updates. It was moved to topic_timers." } ) diff --git a/db/fixtures/999_topics.rb b/db/fixtures/999_topics.rb index 541cacad12..25958a8c69 100644 --- a/db/fixtures/999_topics.rb +++ b/db/fixtures/999_topics.rb @@ -6,9 +6,9 @@ staff = Category.find_by(id: SiteSetting.staff_category_id) seed_welcome_topics = (Topic.where('id NOT IN (SELECT topic_id from categories where topic_id is not null)').count == 0 && !Rails.env.test?) unless Rails.env.test? - def create_static_page_topic(site_setting_key, title_key, body_key, body_override, category, description, params={}) + def create_static_page_topic(site_setting_key, title_key, body_key, body_override, category, description, params = {}) unless SiteSetting.send(site_setting_key) > 0 - creator = PostCreator.new( Discourse.system_user, + creator = PostCreator.new(Discourse.system_user, title: I18n.t(title_key, default: I18n.t(title_key, locale: :en)), raw: body_override.present? ? body_override : I18n.t(body_key, params.merge(default: I18n.t(body_key, params.merge(locale: :en)))), skip_validations: true, @@ -19,18 +19,16 @@ unless Rails.env.test? SiteSetting.send("#{site_setting_key}=", post.topic_id) - _reply = PostCreator.create( Discourse.system_user, + _reply = PostCreator.create(Discourse.system_user, raw: I18n.t('static_topic_first_reply', page_name: I18n.t(title_key, default: I18n.t(title_key, locale: :en))), skip_validations: true, - topic_id: post.topic_id ) + topic_id: post.topic_id) end end - create_static_page_topic('tos_topic_id', 'tos_topic.title', "tos_topic.body", nil, staff, "terms of service", { - company_domain: "company_domain", - company_full_name: "company_full_name", - company_name: "company_short_name" - }) + create_static_page_topic('tos_topic_id', 'tos_topic.title', "tos_topic.body", nil, staff, "terms of service", company_domain: "company_domain", + company_full_name: "company_full_name", + company_name: "company_short_name") create_static_page_topic('guidelines_topic_id', 'guidelines_topic.title', "guidelines_topic.body", nil, staff, "guidelines") @@ -57,15 +55,13 @@ if seed_welcome_topics end welcome = File.read(filename) - PostCreator.create( Discourse.system_user, + PostCreator.create(Discourse.system_user, raw: welcome, title: DiscoursePluginRegistry.seed_data["admin_quick_start_title"] || "READ ME FIRST: Admin Quick Start Guide", skip_validations: true, category: staff ? staff.name : nil) end - - # run this later, cause we need to make sure new application controller resilience is in place first ColumnDropper.drop( @@ -74,7 +70,7 @@ ColumnDropper.drop( columns: %w{ first_topic_unread_at }, - on_drop: ->(){ + on_drop: ->() { STDERR.puts "Removing superflous user stats columns!" ActiveRecord::Base.exec_sql "DROP FUNCTION IF EXISTS first_unread_topic_for(int)" } @@ -91,7 +87,7 @@ ColumnDropper.drop( notify_user_count last_unread_at }, - on_drop: ->(){ + on_drop: ->() { STDERR.puts "Removing superflous topic columns!" } ) @@ -106,7 +102,7 @@ ColumnDropper.drop( auto_close_based_on_last_post auto_close_hours }, - on_drop: ->(){ + on_drop: ->() { STDERR.puts "Removing superflous topic columns!" }, delay: 3600 diff --git a/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb b/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb index 9f7daa2742..28e3371745 100644 --- a/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb +++ b/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb @@ -3,7 +3,6 @@ class AddLastPostUserIdToForumThreads < ActiveRecord::Migration def up add_column :forum_threads, :last_post_user_id, :integer - execute "update forum_threads t set last_post_user_id = (select user_id from posts where forum_thread_id = t.Id order by post_number desc limit 1)" diff --git a/db/migrate/20120629150253_denormalize_expressions.rb b/db/migrate/20120629150253_denormalize_expressions.rb index 1b8a659db4..8a38664885 100644 --- a/db/migrate/20120629150253_denormalize_expressions.rb +++ b/db/migrate/20120629150253_denormalize_expressions.rb @@ -15,7 +15,6 @@ class DenormalizeExpressions < ActiveRecord::Migration add_column :forum_threads, :expression4_count, :integer, null: false, default: 0 add_column :forum_threads, :expression5_count, :integer, null: false, default: 0 - (1..5).each do |i| execute "update posts set expression#{i}_count = (select count(*) from expressions where parent_id = posts.id and expression_type_id = #{i})" execute "update forum_threads set expression#{i}_count = (select sum(expression#{i}_count) from posts where forum_thread_id = forum_threads.id)" diff --git a/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb b/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb index bd1e05bea2..4bd5e9b733 100644 --- a/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb +++ b/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb @@ -4,7 +4,7 @@ class AddEmailHashedPasswordNameSaltToUsers < ActiveRecord::Migration execute "update users set email= md5(random()::text) || 'domain.com'" - change_column :users, :email, :string, limit:256, null: false + change_column :users, :email, :string, limit: 256, null: false add_index :users, [:email], unique: true rename_column :users, :display_username, :name @@ -12,7 +12,7 @@ class AddEmailHashedPasswordNameSaltToUsers < ActiveRecord::Migration add_column :users, :password_hash, :string, limit: 64 add_column :users, :salt, :string, limit: 32 add_column :users, :active, :boolean - add_column :users, :activation_key,:string, limit: 32 + add_column :users, :activation_key, :string, limit: 32 add_column :user_open_ids, :active, :boolean, null: false diff --git a/db/migrate/20120720013733_add_username_lower_to_users.rb b/db/migrate/20120720013733_add_username_lower_to_users.rb index 764fae375d..38602aa2aa 100644 --- a/db/migrate/20120720013733_add_username_lower_to_users.rb +++ b/db/migrate/20120720013733_add_username_lower_to_users.rb @@ -3,7 +3,7 @@ class AddUsernameLowerToUsers < ActiveRecord::Migration add_column :users, :username_lower, :string, limit: 20 execute "update users set username_lower = lower(username)" add_index :users, [:username_lower], unique: true - change_column :users, :username_lower, :string, limit: 20, null:false + change_column :users, :username_lower, :string, limit: 20, null: false end def down remove_column :users, :username_lower diff --git a/db/migrate/20120807223020_create_actions.rb b/db/migrate/20120807223020_create_actions.rb index 623bda5929..ace5939785 100644 --- a/db/migrate/20120807223020_create_actions.rb +++ b/db/migrate/20120807223020_create_actions.rb @@ -8,7 +8,6 @@ class CreateActions < ActiveRecord::Migration # There is a notificiation table as well that covers much of this, # but this table is wider and is intended for non-notifying actions as well - t.integer :action_type, null: false t.integer :user_id, null: false t.integer :target_forum_thread_id diff --git a/db/migrate/20120809020415_remove_site_id.rb b/db/migrate/20120809020415_remove_site_id.rb index 86727e86fd..c9904d4a70 100644 --- a/db/migrate/20120809020415_remove_site_id.rb +++ b/db/migrate/20120809020415_remove_site_id.rb @@ -9,7 +9,7 @@ class RemoveSiteId < ActiveRecord::Migration remove_index 'expression_types', name: 'index_expression_types_on_site_id_and_expression_index' remove_index 'expression_types', name: 'index_expression_types_on_site_id_and_name' - remove_column 'expression_types','site_id' + remove_column 'expression_types', 'site_id' add_index "expression_types", ["expression_index"], unique: true add_index "expression_types", ["name"], unique: true diff --git a/db/migrate/20120809053414_correct_indexing_on_posts.rb b/db/migrate/20120809053414_correct_indexing_on_posts.rb index b3777e093a..13ff4507d6 100644 --- a/db/migrate/20120809053414_correct_indexing_on_posts.rb +++ b/db/migrate/20120809053414_correct_indexing_on_posts.rb @@ -1,20 +1,20 @@ class CorrectIndexingOnPosts < ActiveRecord::Migration def up - execute "update posts pp + execute "update posts pp set post_number = c.real_number from ( select p1.id, count(*) real_number from posts p1 join posts p2 on p1.forum_thread_id = p2.forum_thread_id where p2.id <= p1.id and p1.forum_thread_id = p2.forum_thread_id - group by p1.id + group by p1.id ) as c where pp.id = c.id and pp.post_number <> c.real_number" - remove_index "posts", ["forum_thread_id","post_number"] + remove_index "posts", ["forum_thread_id", "post_number"] # this needs to be unique if it is not we can not use post_number to identify a post - add_index "posts", ["forum_thread_id","post_number"], unique: true + add_index "posts", ["forum_thread_id", "post_number"], unique: true end diff --git a/db/migrate/20120809154750_remove_index_for_now.rb b/db/migrate/20120809154750_remove_index_for_now.rb index 148cbe70a0..ea20ceb152 100644 --- a/db/migrate/20120809154750_remove_index_for_now.rb +++ b/db/migrate/20120809154750_remove_index_for_now.rb @@ -1,11 +1,11 @@ class RemoveIndexForNow < ActiveRecord::Migration def up - remove_index "posts", ["forum_thread_id","post_number"] - add_index "posts", ["forum_thread_id","post_number"], unique: false + remove_index "posts", ["forum_thread_id", "post_number"] + add_index "posts", ["forum_thread_id", "post_number"], unique: false end def down - remove_index "posts", ["forum_thread_id","post_number"] - add_index "posts", ["forum_thread_id","post_number"], unique: true + remove_index "posts", ["forum_thread_id", "post_number"] + add_index "posts", ["forum_thread_id", "post_number"], unique: true end end diff --git a/db/migrate/20120809174649_create_post_actions.rb b/db/migrate/20120809174649_create_post_actions.rb index ddcf4d4632..7a8e5d9f44 100644 --- a/db/migrate/20120809174649_create_post_actions.rb +++ b/db/migrate/20120809174649_create_post_actions.rb @@ -3,7 +3,7 @@ class CreatePostActions < ActiveRecord::Migration create_table :post_actions do |t| t.integer :post_id, null: false t.integer :user_id, null: false - t.integer :post_action_type_id, null:false + t.integer :post_action_type_id, null: false t.datetime :deleted_at t.timestamps end diff --git a/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb b/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb index 5a8ea4af87..6d79402d34 100644 --- a/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb +++ b/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb @@ -1,5 +1,5 @@ class AddUniqueConstraintToUserActions < ActiveRecord::Migration def change - add_index :user_actions, ['action_type','user_id', 'target_forum_thread_id', 'target_post_id', 'acting_user_id'], name: "idx_unique_rows", unique: true + add_index :user_actions, ['action_type', 'user_id', 'target_forum_thread_id', 'target_post_id', 'acting_user_id'], name: "idx_unique_rows", unique: true end end diff --git a/db/migrate/20120821191616_add_bumped_at_to_forum_threads.rb b/db/migrate/20120821191616_add_bumped_at_to_forum_threads.rb index 584c62c9fb..9420f79a8d 100644 --- a/db/migrate/20120821191616_add_bumped_at_to_forum_threads.rb +++ b/db/migrate/20120821191616_add_bumped_at_to_forum_threads.rb @@ -5,6 +5,6 @@ class AddBumpedAtToForumThreads < ActiveRecord::Migration change_column :forum_threads, :bumped_at, :datetime, null: false remove_index :forum_threads, :last_posted_at - add_index :forum_threads, :bumped_at, order: {bumped_at: :desc} + add_index :forum_threads, :bumped_at, order: { bumped_at: :desc } end end diff --git a/db/migrate/20121011155904_create_email_logs.rb b/db/migrate/20121011155904_create_email_logs.rb index 2d92e00db6..abfea55e76 100644 --- a/db/migrate/20121011155904_create_email_logs.rb +++ b/db/migrate/20121011155904_create_email_logs.rb @@ -7,7 +7,7 @@ class CreateEmailLogs < ActiveRecord::Migration t.timestamps end - add_index :email_logs, :created_at, order: {created_at: :desc} - add_index :email_logs, [:user_id, :created_at], order: {created_at: :desc} + add_index :email_logs, :created_at, order: { created_at: :desc } + add_index :email_logs, [:user_id, :created_at], order: { created_at: :desc } end end diff --git a/db/migrate/20121018103721_rename_forum_thread_tables.rb b/db/migrate/20121018103721_rename_forum_thread_tables.rb index d513d1b0e2..84980494da 100644 --- a/db/migrate/20121018103721_rename_forum_thread_tables.rb +++ b/db/migrate/20121018103721_rename_forum_thread_tables.rb @@ -14,7 +14,6 @@ class RenameForumThreadTables < ActiveRecord::Migration rename_column 'categories', 'threads_month', 'topics_month' rename_column 'categories', 'threads_week', 'topics_week' - rename_column 'category_featured_topics', 'forum_thread_id', 'topic_id' rename_column 'topic_link_clicks', 'forum_thread_link_id', 'topic_link_id' @@ -22,7 +21,6 @@ class RenameForumThreadTables < ActiveRecord::Migration rename_column 'topic_links', 'forum_thread_id', 'topic_id' rename_column 'topic_links', 'link_forum_thread_id', 'link_topic_id' - rename_column 'topic_users', 'forum_thread_id', 'topic_id' rename_column 'incoming_links', 'forum_thread_id', 'topic_id' diff --git a/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb b/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb index 1432d8ba70..34ee5edbce 100644 --- a/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb +++ b/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb @@ -6,7 +6,7 @@ class AddAutoTrackAfterSecondsAndBanningAndDobToUser < ActiveRecord::Migration add_column :users, :auto_track_topics_after_msecs, :integer add_column :users, :views, :integer, null: false, default: 0 - remove_column :users, :auto_track_topics + remove_column :users, :auto_track_topics add_column :topic_users, :total_msecs_viewed, :integer, null: false, default: 0 diff --git a/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb b/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb index 3f537dfdc9..1eb09763de 100644 --- a/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb +++ b/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb @@ -3,8 +3,8 @@ class AddReplyToUserIdToPost < ActiveRecord::Migration # caching this column makes the topic page WAY faster add_column :posts, :reply_to_user_id, :integer execute 'UPDATE posts p SET reply_to_user_id = ( - SELECT u.id from users u - JOIN posts p2 ON p2.user_id = u.id AND + SELECT u.id from users u + JOIN posts p2 ON p2.user_id = u.id AND p2.post_number = p.reply_to_post_number AND p2.topic_id = p.topic_id )' diff --git a/db/migrate/20130320012100_add_user_indexes_to_posts_and_topics.rb b/db/migrate/20130320012100_add_user_indexes_to_posts_and_topics.rb index 3fffe74303..89bdb3aee4 100644 --- a/db/migrate/20130320012100_add_user_indexes_to_posts_and_topics.rb +++ b/db/migrate/20130320012100_add_user_indexes_to_posts_and_topics.rb @@ -1,9 +1,9 @@ class AddUserIndexesToPostsAndTopics < ActiveRecord::Migration def up - execute "CREATE INDEX idx_posts_user_id_deleted_at + execute "CREATE INDEX idx_posts_user_id_deleted_at ON posts(user_id) WHERE deleted_at IS NULL" - - execute "CREATE INDEX idx_topics_user_id_deleted_at + + execute "CREATE INDEX idx_topics_user_id_deleted_at ON topics(user_id) WHERE deleted_at IS NULL" end diff --git a/db/migrate/20130404232558_add_user_extras.rb b/db/migrate/20130404232558_add_user_extras.rb index 73d1add55d..70c75482e5 100644 --- a/db/migrate/20130404232558_add_user_extras.rb +++ b/db/migrate/20130404232558_add_user_extras.rb @@ -7,7 +7,6 @@ class AddUserExtras < ActiveRecord::Migration add_column :users, :likes_received, :integer, null: false, default: 0 add_column :users, :topic_reply_count, :integer, null: false, default: 0 - # NOTE: to keep migrations working through refactorings we avoid externalizing this stuff. # even though a helper method may make sense execute <> 0 & 255) );" - change_column :topic_link_clicks, :ip_address, :inet, { :null => false } + change_column :topic_link_clicks, :ip_address, :inet, null: false remove_column :topic_link_clicks, :ip end diff --git a/db/migrate/20130625022454_change_ip_to_inet_in_views.rb b/db/migrate/20130625022454_change_ip_to_inet_in_views.rb index 30a0e8390b..e4e01f5629 100644 --- a/db/migrate/20130625022454_change_ip_to_inet_in_views.rb +++ b/db/migrate/20130625022454_change_ip_to_inet_in_views.rb @@ -12,7 +12,7 @@ class ChangeIpToInetInViews < ActiveRecord::Migration (ip >> 0 & 255) );" - change_column table, :ip_address, :inet, { :null => false } + change_column table, :ip_address, :inet, null: false remove_column table, :ip end diff --git a/db/migrate/20130625170842_remove_access_password.rb b/db/migrate/20130625170842_remove_access_password.rb index 31c5a83d4a..d2b87bc71b 100644 --- a/db/migrate/20130625170842_remove_access_password.rb +++ b/db/migrate/20130625170842_remove_access_password.rb @@ -1,7 +1,7 @@ class RemoveAccessPassword < ActiveRecord::Migration def up result = execute("SELECT count(*) FROM site_settings where name='access_password' and char_length(value) > 0") - if result[0] and result[0]["count"].to_i > 0 + if result[0] && result[0]["count"].to_i > (0) execute "DELETE FROM site_settings where name='access_password'" SiteSetting.login_required = true SiteSetting.must_approve_users = true diff --git a/db/migrate/20130911182437_create_user_stats.rb b/db/migrate/20130911182437_create_user_stats.rb index 66f5370743..ff3f78ab18 100644 --- a/db/migrate/20130911182437_create_user_stats.rb +++ b/db/migrate/20130911182437_create_user_stats.rb @@ -1,6 +1,6 @@ class CreateUserStats < ActiveRecord::Migration def up - create_table :user_stats, :id => false do |t| + create_table :user_stats, id: false do |t| t.references :user, null: false t.boolean :has_custom_avatar, default: false, null: false end diff --git a/db/migrate/20130912185218_acting_user_null.rb b/db/migrate/20130912185218_acting_user_null.rb index fe99e3783c..d03eabe527 100644 --- a/db/migrate/20130912185218_acting_user_null.rb +++ b/db/migrate/20130912185218_acting_user_null.rb @@ -1,10 +1,10 @@ class ActingUserNull < ActiveRecord::Migration def up - change_column :user_histories, :acting_user_id, :integer, :null => true + change_column :user_histories, :acting_user_id, :integer, null: true end def down execute "DELETE FROM user_histories WHERE acting_user_id IS NULL" - change_column :user_histories, :acting_user_id, :integer, :null => false + change_column :user_histories, :acting_user_id, :integer, null: false end end diff --git a/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb b/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb index b1a20098d6..3055c712a8 100644 --- a/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb +++ b/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb @@ -1,5 +1,5 @@ class AddUserIdParentTypeIndexOnViews < ActiveRecord::Migration def change - add_index :views, [:user_id,:parent_type,:parent_id] + add_index :views, [:user_id, :parent_type, :parent_id] end end diff --git a/db/migrate/20131022045114_add_uncategorized_category.rb b/db/migrate/20131022045114_add_uncategorized_category.rb index 953960fd66..347bc2320c 100644 --- a/db/migrate/20131022045114_add_uncategorized_category.rb +++ b/db/migrate/20131022045114_add_uncategorized_category.rb @@ -17,7 +17,6 @@ class AddUncategorizedCategory < ActiveRecord::Migration execute "INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES ('uncategorized_category_id', 3, #{category_id}, now(), now())" - execute "DELETE from site_settings where name in ('uncategorized_name', 'uncategorized_text_color', 'uncategorized_color')" execute "UPDATE topics SET category_id = #{category_id} WHERE archetype = 'regular' AND category_id IS NULL" diff --git a/db/migrate/20131023163509_add_parent_category_id_to_categories.rb b/db/migrate/20131023163509_add_parent_category_id_to_categories.rb index f4b86f5041..759895f5fe 100644 --- a/db/migrate/20131023163509_add_parent_category_id_to_categories.rb +++ b/db/migrate/20131023163509_add_parent_category_id_to_categories.rb @@ -3,4 +3,3 @@ class AddParentCategoryIdToCategories < ActiveRecord::Migration add_column :categories, :parent_category_id, :integer end end - diff --git a/db/migrate/20131210181901_migrate_word_counts.rb b/db/migrate/20131210181901_migrate_word_counts.rb index 64e05aae99..e783725bb8 100644 --- a/db/migrate/20131210181901_migrate_word_counts.rb +++ b/db/migrate/20131210181901_migrate_word_counts.rb @@ -2,7 +2,7 @@ class MigrateWordCounts < ActiveRecord::Migration disable_ddl_transaction! def up - post_ids = execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map {|r| r['id'].to_i } + post_ids = execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } while post_ids.length > 0 3.times do begin @@ -12,10 +12,10 @@ class MigrateWordCounts < ActiveRecord::Migration # Deadlock. Try again, up to 3 times. end end - post_ids = execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map {|r| r['id'].to_i } + post_ids = execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } end - topic_ids = execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map {|r| r['id'].to_i } + topic_ids = execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } while topic_ids.length > 0 3.times do begin @@ -25,7 +25,7 @@ class MigrateWordCounts < ActiveRecord::Migration # Deadlock. Try again, up to 3 times. end end - topic_ids = execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map {|r| r['id'].to_i } + topic_ids = execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } end end diff --git a/db/migrate/20140211230222_move_cas_settings.rb b/db/migrate/20140211230222_move_cas_settings.rb index 0de988fbfa..b832385a3b 100644 --- a/db/migrate/20140211230222_move_cas_settings.rb +++ b/db/migrate/20140211230222_move_cas_settings.rb @@ -4,23 +4,23 @@ class MoveCasSettings < ActiveRecord::Migration #convert the data over to be used by the plugin. cas_hostname = SiteSetting.find_by(name: "cas_hostname") cas_sso_hostname = SiteSetting.find_by(name: "cas_sso_hostname") - if cas_hostname && ! cas_sso_hostname + if cas_hostname && ! cas_sso_hostname #convert the setting over for use by the plugin cas_hostname.update_attribute(:name, 'cas_sso_hostname') - elsif cas_hostname && cas_sso_hostname + elsif cas_hostname && cas_sso_hostname #copy the setting over for use by the plugin and delete the original setting - cas_sso_hostname.update_attribute(:value,cas_hostname.value) + cas_sso_hostname.update_attribute(:value, cas_hostname.value) cas_hostname.destroy end cas_domainname = SiteSetting.find_by(name: "cas_domainname") cas_sso_email_domain = SiteSetting.find_by(name: "cas_sso_email_domain") - if cas_domainname && ! cas_sso_email_domain + if cas_domainname && ! cas_sso_email_domain #convert the setting over for use by the plugin cas_domainname.update_attribute(:name, 'cas_sso_email_domain') - elsif cas_domainname && cas_sso_email_domain + elsif cas_domainname && cas_sso_email_domain #copy the setting over for use by the plugin and delete the original setting - cas_sso_email_domain.update_attribute(:value,cas_domainname.value) + cas_sso_email_domain.update_attribute(:value, cas_domainname.value) cas_domainname.destroy end @@ -29,7 +29,7 @@ class MoveCasSettings < ActiveRecord::Migration cas_logins.destroy end - #remove the unused table + #remove the unused table drop_table :cas_user_infos end diff --git a/db/migrate/20140227104930_add_custom_email_in_to_categories.rb b/db/migrate/20140227104930_add_custom_email_in_to_categories.rb index 7d3cac2c5f..fa31829caf 100644 --- a/db/migrate/20140227104930_add_custom_email_in_to_categories.rb +++ b/db/migrate/20140227104930_add_custom_email_in_to_categories.rb @@ -1,12 +1,12 @@ class AddCustomEmailInToCategories < ActiveRecord::Migration def up - add_column :categories, :email_in, :string, null: true - add_column :categories, :email_in_allow_strangers, :boolean, default: false - add_index :categories, :email_in, unique: true + add_column :categories, :email_in, :string, null: true + add_column :categories, :email_in_allow_strangers, :boolean, default: false + add_index :categories, :email_in, unique: true end def down - remove_column :categories, :email_in - remove_column :categories, :email_in_allow_strangers - remove_index :categories, :email_in + remove_column :categories, :email_in + remove_column :categories, :email_in_allow_strangers + remove_index :categories, :email_in end end diff --git a/db/migrate/20140318150412_add_excerpt_to_topics.rb b/db/migrate/20140318150412_add_excerpt_to_topics.rb index 35283cb47b..513906d900 100644 --- a/db/migrate/20140318150412_add_excerpt_to_topics.rb +++ b/db/migrate/20140318150412_add_excerpt_to_topics.rb @@ -2,7 +2,7 @@ class AddExcerptToTopics < ActiveRecord::Migration def up add_column :topics, :excerpt, :string, limit: 1000 - topic_ids = execute("SELECT id FROM topics WHERE pinned_at IS NOT NULL").map {|r| r['id'].to_i } + topic_ids = execute("SELECT id FROM topics WHERE pinned_at IS NOT NULL").map { |r| r['id'].to_i } topic_ids.each do |topic_id| cooked = execute("SELECT cooked FROM posts WHERE topic_id = #{topic_id} ORDER BY post_number ASC LIMIT 1")[0]['cooked'] if cooked diff --git a/db/migrate/20140402201432_make_content_sha1_nullable.rb b/db/migrate/20140402201432_make_content_sha1_nullable.rb index 29b7572edc..fd2a60ebd9 100644 --- a/db/migrate/20140402201432_make_content_sha1_nullable.rb +++ b/db/migrate/20140402201432_make_content_sha1_nullable.rb @@ -1,5 +1,5 @@ class MakeContentSha1Nullable < ActiveRecord::Migration def change - change_column :topic_embeds, :content_sha1, :string, :limit => 40, :null => true + change_column :topic_embeds, :content_sha1, :string, limit: 40, null: true end end diff --git a/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb b/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb index ae4a8f364a..82e90e7153 100644 --- a/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb +++ b/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb @@ -9,7 +9,7 @@ class RemoveSystemAvatarsFromUserAvatars < ActiveRecord::Migration # normally we dont reach into the object model, but we have to here. # otherwise we will wait a real long time for uploads to go away skip = -1 - while skip=destroy_system_avatar_batch(skip) do + while skip = destroy_system_avatar_batch(skip) do puts "Destroyed up to id: #{skip}" end @@ -21,9 +21,9 @@ class RemoveSystemAvatarsFromUserAvatars < ActiveRecord::Migration initial = skip Upload.where('id IN (SELECT system_upload_id FROM user_avatars) AND id > ?', skip) - .order(:id) - .limit(500) - .each do |upload| + .order(:id) + .limit(500) + .each do |upload| skip = upload.id begin upload.destroy diff --git a/db/migrate/20140715055242_add_quoted_posts.rb b/db/migrate/20140715055242_add_quoted_posts.rb index 587d894799..5f6c79cd3e 100644 --- a/db/migrate/20140715055242_add_quoted_posts.rb +++ b/db/migrate/20140715055242_add_quoted_posts.rb @@ -9,7 +9,6 @@ class AddQuotedPosts < ActiveRecord::Migration add_index :quoted_posts, [:post_id, :quoted_post_id], unique: true add_index :quoted_posts, [:quoted_post_id, :post_id], unique: true - # NOTE this can be done in pg but too much of a headache id = 0 while id = backfill_batch(id, 1000); end @@ -37,9 +36,8 @@ SQL topic_id = a['data-topic'].to_i post_number = a['data-post'].to_i - next if uniq[[topic_id,post_number]] - uniq[[topic_id,post_number]] = true - + next if uniq[[topic_id, post_number]] + uniq[[topic_id, post_number]] = true execute "INSERT INTO quoted_posts(post_id, quoted_post_id, created_at, updated_at) SELECT #{post_id}, id, created_at, updated_at diff --git a/db/migrate/20140804010803_incoming_link_normalization.rb b/db/migrate/20140804010803_incoming_link_normalization.rb index 2c3743541a..171451dff5 100644 --- a/db/migrate/20140804010803_incoming_link_normalization.rb +++ b/db/migrate/20140804010803_incoming_link_normalization.rb @@ -36,7 +36,6 @@ class IncomingLinkNormalization < ActiveRecord::Migration ) X WHERE a[2] IS NOT NULL" - execute "UPDATE incoming_links l SET incoming_referer_id = r.id FROM incoming_referers r @@ -59,7 +58,6 @@ class IncomingLinkNormalization < ActiveRecord::Migration FROM incoming_domains d WHERE d.name = l.domain AND d.https = l.https AND d.port = l.port" - remove_column :incoming_referers, :domain remove_column :incoming_referers, :port remove_column :incoming_referers, :https diff --git a/db/migrate/20140804075613_normalize_topic_view_data_and_index.rb b/db/migrate/20140804075613_normalize_topic_view_data_and_index.rb index 4f6e6f1b2e..67b9d784b5 100644 --- a/db/migrate/20140804075613_normalize_topic_view_data_and_index.rb +++ b/db/migrate/20140804075613_normalize_topic_view_data_and_index.rb @@ -29,7 +29,6 @@ class NormalizeTopicViewDataAndIndex < ActiveRecord::Migration SELECT NULL, topic_id, ip_address, viewed_at FROM tmp_views_ip ' - execute 'CREATE UNIQUE INDEX user_id_topic_id_topic_views ON topic_views(user_id, topic_id) WHERE user_id IS NOT NULL' execute 'CREATE UNIQUE INDEX ip_address_topic_id_topic_views ON topic_views(ip_address, topic_id) WHERE user_id IS NULL' diff --git a/db/migrate/20140815215618_add_name_lower_to_categories.rb b/db/migrate/20140815215618_add_name_lower_to_categories.rb index 97c1685337..20f6ef8c37 100644 --- a/db/migrate/20140815215618_add_name_lower_to_categories.rb +++ b/db/migrate/20140815215618_add_name_lower_to_categories.rb @@ -3,7 +3,7 @@ class AddNameLowerToCategories < ActiveRecord::Migration def up add_column :categories, :name_lower, :string, limit: 50 execute "update categories set name_lower = lower(name)" - change_column :categories, :name_lower, :string, limit: 50, null:false + change_column :categories, :name_lower, :string, limit: 50, null: false end def down diff --git a/db/migrate/20140913192733_add_trust_level_locked_column.rb b/db/migrate/20140913192733_add_trust_level_locked_column.rb index 8ac5b0b989..c086f4a2fd 100644 --- a/db/migrate/20140913192733_add_trust_level_locked_column.rb +++ b/db/migrate/20140913192733_add_trust_level_locked_column.rb @@ -1,6 +1,6 @@ class AddTrustLevelLockedColumn < ActiveRecord::Migration def change - add_column :users, :trust_level_locked, :boolean, { default: false, null: false} + add_column :users, :trust_level_locked, :boolean, default: false, null: false reversible do |dir| dir.up do diff --git a/db/migrate/20140929204155_migrate_tos_setting.rb b/db/migrate/20140929204155_migrate_tos_setting.rb index 4e313c0373..6ecac45bb8 100644 --- a/db/migrate/20140929204155_migrate_tos_setting.rb +++ b/db/migrate/20140929204155_migrate_tos_setting.rb @@ -13,7 +13,6 @@ class MigrateTosSetting < ActiveRecord::Migration label = res[0]['value'] end - label = PG::Connection.escape_string(label) execute("INSERT INTO user_fields (name, field_type, editable) VALUES ('#{label}', 'confirm', false)") end diff --git a/db/migrate/20141216112341_resolve_duplicate_group_names.rb b/db/migrate/20141216112341_resolve_duplicate_group_names.rb index 63a49e4b60..dc8fa1fdb0 100644 --- a/db/migrate/20141216112341_resolve_duplicate_group_names.rb +++ b/db/migrate/20141216112341_resolve_duplicate_group_names.rb @@ -11,7 +11,7 @@ class ResolveDuplicateGroupNames < ActiveRecord::Migration groups = Group.where id: results.map { |r| r['id'] } groups.group_by { |g| g.name.downcase }.each do |key, value| value.each_with_index do |dup, index| - dup.update! name: "#{dup.name[0..18]}_#{index+1}" if index > 0 + dup.update! name: "#{dup.name[0..18]}_#{index + 1}" if index > 0 end end end diff --git a/db/migrate/20150108002354_add_liked_and_bookmarked_to_topic_user.rb b/db/migrate/20150108002354_add_liked_and_bookmarked_to_topic_user.rb index 2dcdd47314..58b5f37831 100644 --- a/db/migrate/20150108002354_add_liked_and_bookmarked_to_topic_user.rb +++ b/db/migrate/20150108002354_add_liked_and_bookmarked_to_topic_user.rb @@ -4,7 +4,7 @@ class AddLikedAndBookmarkedToTopicUser < ActiveRecord::Migration add_column :topic_users, :bookmarked, :boolean, default: false # likes and bookmarks PostActionType.types[:like] and :bookmark which should not be used in a migration - {liked: 2, bookmarked: 1}.each do |name, type| + { liked: 2, bookmarked: 1 }.each do |name, type| execute "UPDATE topic_users SET #{name} = true WHERE EXISTS (SELECT 1 FROM post_actions pa diff --git a/db/migrate/20150713203955_enlarge_users_email_field.rb b/db/migrate/20150713203955_enlarge_users_email_field.rb index 15856c0f04..9b4238f4b5 100644 --- a/db/migrate/20150713203955_enlarge_users_email_field.rb +++ b/db/migrate/20150713203955_enlarge_users_email_field.rb @@ -1,8 +1,8 @@ class EnlargeUsersEmailField < ActiveRecord::Migration def up - change_column :users, :email, :string, :limit => 513 + change_column :users, :email, :string, limit: 513 end def down - change_column :users, :email, :string, :limit => 128 + change_column :users, :email, :string, limit: 128 end end diff --git a/db/migrate/20150729150523_migrate_auto_close_posts.rb b/db/migrate/20150729150523_migrate_auto_close_posts.rb index d00ec9623a..1a4d6bc0e7 100644 --- a/db/migrate/20150729150523_migrate_auto_close_posts.rb +++ b/db/migrate/20150729150523_migrate_auto_close_posts.rb @@ -9,7 +9,7 @@ class MigrateAutoClosePosts < ActiveRecord::Migration sql = "UPDATE posts SET action_code = 'autoclosed.enabled', post_type = 3 " sql << "WHERE post_type = 2 AND (" - sql << strings.map {|s| "raw ~* #{ActiveRecord::Base.connection.quote(s)}" }.join(' OR ') + sql << strings.map { |s| "raw ~* #{ActiveRecord::Base.connection.quote(s)}" }.join(' OR ') sql << ")" execute sql diff --git a/db/migrate/20160108051129_fix_incorrect_user_history.rb b/db/migrate/20160108051129_fix_incorrect_user_history.rb index 2a9cbdf0fb..e7b44a9e76 100644 --- a/db/migrate/20160108051129_fix_incorrect_user_history.rb +++ b/db/migrate/20160108051129_fix_incorrect_user_history.rb @@ -7,7 +7,6 @@ class FixIncorrectUserHistory < ActiveRecord::Migration # # This migration hunts for date stuff started going wrong and date it started being good and corrects the data - # this is a :auto_trust_level_change mislabled as :check_email # impersonate that was actually delete topic condition = < 0 + if result[0] && result[0]["count"].to_i > (0) execute "UPDATE categories SET show_subcategory_list = true WHERE parent_category_id IS NULL" end end diff --git a/db/migrate/20170313192741_add_themes.rb b/db/migrate/20170313192741_add_themes.rb index ab2e3abc3b..52905c86b7 100644 --- a/db/migrate/20170313192741_add_themes.rb +++ b/db/migrate/20170313192741_add_themes.rb @@ -20,16 +20,15 @@ class AddThemes < ActiveRecord::Migration remove_column :color_schemes, :versioned_id enabled_theme_count = execute("SELECT count(*) FROM themes WHERE enabled") - .to_a[0]["count"].to_i - + .to_a[0]["count"].to_i enabled_scheme_id = execute("SELECT id FROM color_schemes WHERE enabled") - .to_a[0]&.fetch("id") + .to_a[0]&.fetch("id") theme_key, theme_id = execute("SELECT key, id FROM themes WHERE enabled").to_a[0]&.values - if (enabled_theme_count == 0 && enabled_scheme_id) || enabled_theme_count > 1 + if (enabled_theme_count == 0 && enabled_scheme_id) || enabled_theme_count > 1 puts "Creating a new default theme!" @@ -67,8 +66,6 @@ SQL execute(sql) end - - remove_column :themes, :enabled remove_column :color_schemes, :enabled end diff --git a/db/migrate/20170609115401_add_extension_to_topic_links.rb b/db/migrate/20170609115401_add_extension_to_topic_links.rb new file mode 100644 index 0000000000..7c05a12d1a --- /dev/null +++ b/db/migrate/20170609115401_add_extension_to_topic_links.rb @@ -0,0 +1,6 @@ +class AddExtensionToTopicLinks < ActiveRecord::Migration + def change + add_column :topic_links, :extension, :string, limit: 10 + add_index :topic_links, :extension + end +end diff --git a/db/migrate/20170628152322_create_watched_words.rb b/db/migrate/20170628152322_create_watched_words.rb new file mode 100644 index 0000000000..212a3d2b9f --- /dev/null +++ b/db/migrate/20170628152322_create_watched_words.rb @@ -0,0 +1,11 @@ +class CreateWatchedWords < ActiveRecord::Migration + def change + create_table :watched_words do |t| + t.string :word, null: false + t.integer :action, null: false + t.timestamps + end + + add_index :watched_words, [:action, :word], unique: true + end +end diff --git a/db/migrate/20170703115216_add_extension_to_uploads.rb b/db/migrate/20170703115216_add_extension_to_uploads.rb new file mode 100644 index 0000000000..76aa08fa11 --- /dev/null +++ b/db/migrate/20170703115216_add_extension_to_uploads.rb @@ -0,0 +1,11 @@ +class AddExtensionToUploads < ActiveRecord::Migration + def up + add_column :uploads, :extension, :string, limit: 10 + execute "CREATE INDEX index_uploads_on_extension ON uploads(lower(extension))" + end + + def down + remove_column :uploads, :extension + execute "DROP INDEX index_uploads_on_extension" + end +end diff --git a/db/migrate/20170713164357_create_search_logs.rb b/db/migrate/20170713164357_create_search_logs.rb new file mode 100644 index 0000000000..26d6f1a425 --- /dev/null +++ b/db/migrate/20170713164357_create_search_logs.rb @@ -0,0 +1,12 @@ +class CreateSearchLogs < ActiveRecord::Migration + def change + create_table :search_logs do |t| + t.string :term, null: false + t.integer :user_id, null: true + t.inet :ip_address, null: false + t.integer :clicked_topic_id, null: true + t.integer :search_type, null: false + t.datetime :created_at, null: false + end + end +end diff --git a/db/migrate/20170717084947_create_user_emails.rb b/db/migrate/20170717084947_create_user_emails.rb new file mode 100644 index 0000000000..eb6dc4980d --- /dev/null +++ b/db/migrate/20170717084947_create_user_emails.rb @@ -0,0 +1,42 @@ +require_dependency 'column_dropper' + +class CreateUserEmails < ActiveRecord::Migration + def up + create_table :user_emails do |t| + t.integer :user_id, null: false + t.string :email, limit: 513, null: false + t.boolean :primary, default: false, null: false + t.timestamps + end + + add_index :user_emails, :user_id + add_index :user_emails, [:user_id, :primary], unique: true + + execute "CREATE UNIQUE INDEX index_user_emails_on_email ON user_emails (lower(email));" + + execute <<~SQL + INSERT INTO user_emails ( + id, + user_id, + email, + "primary", + created_at, + updated_at + ) SELECT + id, + id, + email, + 'TRUE', + created_at, + updated_at + FROM users + SQL + + change_column_null :users, :email, true + ColumnDropper.mark_readonly(:users, :email) + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20170728012754_split_public_in_groups.rb b/db/migrate/20170728012754_split_public_in_groups.rb new file mode 100644 index 0000000000..31303986c5 --- /dev/null +++ b/db/migrate/20170728012754_split_public_in_groups.rb @@ -0,0 +1,17 @@ +class SplitPublicInGroups < ActiveRecord::Migration + def up + add_column :groups, :public_exit, :boolean, default: false, null: false + add_column :groups, :public_admission, :boolean, default: false, null: false + + ActiveRecord::Base.exec_sql <<~SQL + UPDATE groups + SET public_exit = true, public_admission = true + WHERE public = true + SQL + end + + def down + remove_column :groups, :public_exit + remove_column :groups, :public_admission + end +end diff --git a/db/migrate/20170728054115_remove_public_from_groups.rb b/db/migrate/20170728054115_remove_public_from_groups.rb new file mode 100644 index 0000000000..216bed0a6b --- /dev/null +++ b/db/migrate/20170728054115_remove_public_from_groups.rb @@ -0,0 +1,9 @@ +class RemovePublicFromGroups < ActiveRecord::Migration + def up + # Defer dropping of the columns until the new application code has been deployed. + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20170731030330_fix_primary_emails_for_staged_users.rb b/db/migrate/20170731030330_fix_primary_emails_for_staged_users.rb new file mode 100644 index 0000000000..d31e7578ef --- /dev/null +++ b/db/migrate/20170731030330_fix_primary_emails_for_staged_users.rb @@ -0,0 +1,28 @@ +class FixPrimaryEmailsForStagedUsers < ActiveRecord::Migration + def up + execute <<~SQL + INSERT INTO user_emails ( + user_id, + email, + "primary", + created_at, + updated_at + ) SELECT + users.id, + email_tokens.email, + 'TRUE', + users.created_at, + users.updated_at + FROM users + LEFT JOIN user_emails ON user_emails.user_id = users.id + LEFT JOIN email_tokens ON email_tokens.user_id = users.id + WHERE staged + AND NOT active + AND user_emails.id IS NULL + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index fce07d005c..a8bf7a1b84 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -19,13 +19,12 @@ To get your Ubuntu 16.04 LTS install up and running to develop Discourse and Dis curl -sSL https://get.rvm.io | bash -s stable echo 'gem: --no-document' >> ~/.gemrc - # Logout and back in to activate RVM installation + # exit the terminal and open it again to activate RVM - rvm install 2.3.1 - rvm --default use 2.3.1 # If this error out check https://rvm.io/integration/gnome-terminal + rvm install 2.3.4 + rvm --default use 2.3.4 # If this error out check https://rvm.io/integration/gnome-terminal gem install bundler mailcatcher - # Postgresql sudo su postgres createuser --createdb --superuser -Upostgres $(cat /tmp/username) @@ -37,10 +36,12 @@ To get your Ubuntu 16.04 LTS install up and running to develop Discourse and Dis exit # Node - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.1/install.sh | bash - # exit the terminal and open it again - nvm install 6.2.0 - nvm alias default 6.2.0 + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash + + # exit the terminal and open it again to activate NVM + + nvm install node + nvm alias default node npm install -g svgo phantomjs-prebuilt @@ -49,14 +50,19 @@ If everything goes alright, let's clone Discourse and start hacking: git clone https://github.com/discourse/discourse.git ~/discourse cd ~/discourse bundle install - bundle exec rake db:create db:migrate db:test:prepare - bundle exec rake autospec # CTRL + C to stop; Optional - bundle exec rails s -b 0.0.0.0 # Open browser on http://localhost:3000 and you should see Discourse + bundle exec rake db:migrate + RAILS_ENV=test bundle exec rake db:migrate + + # run the specs (optional) + bundle exec rake autospec # CTRL + C to stop + + # launch discourse + bundle exec rails s -b 0.0.0.0 # open browser on http://localhost:3000 and you should see Discourse Create a test account, and enable it with: bundle exec rails c - u = User.find_by_id 1 + u = User.find(1) u.activate u.grant_admin! exit @@ -64,7 +70,7 @@ Create a test account, and enable it with: Discourse does a lot of stuff async, so it's better to run sidekiq even on development mode: ruby $(mailcatcher) # open http://localhost:1080 to see the emails, stop with pkill -f mailcatcher - bundle exec sidekiq -q critical,low,default -d -l log/sidekiq.log # open http://localhost:3000/sidekiq to see the queue, stop with pkill -f sidekiq + bundle exec sidekiq # open http://localhost:3000/sidekiq to see queues bundle exec rails server And happy hacking! diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index 84500b50f7..ce2dfbb3e2 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -98,9 +98,7 @@ module ActiveRecord if fallback_handler.master_down? fallback_handler.verify_master - connection = postgresql_connection(config.dup.merge({ - host: config[:replica_host], port: config[:replica_port] - })) + connection = postgresql_connection(config.dup.merge(host: config[:replica_host], port: config[:replica_port])) verify_replica(connection) Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) diff --git a/lib/admin_constraint.rb b/lib/admin_constraint.rb index 0ee711c2bc..db5c0e73b6 100644 --- a/lib/admin_constraint.rb +++ b/lib/admin_constraint.rb @@ -2,7 +2,7 @@ require_dependency 'current_user' class AdminConstraint - def initialize(options={}) + def initialize(options = {}) @require_master = options[:require_master] end diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index ffd1dd404d..0f216c606b 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -4,7 +4,7 @@ class AdminUserIndexQuery def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels) @params = params - @query = initialize_query_with_order(klass) + @query = initialize_query_with_order(klass).joins(:user_emails) @trust_levels = trust_levels end @@ -24,10 +24,10 @@ class AdminUserIndexQuery 'read_time' => 'user_stats.time_read' } - def find_users(limit=100) + def find_users(limit = 100) page = params[:page].to_i - 1 if page < 0 - page = 0 + page = 0 end find_users_query.limit(limit).offset(page * limit) end @@ -52,7 +52,7 @@ class AdminUserIndexQuery if !custom_order.present? if params[:query] == "active" - order << "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD')) DESC" + order << "COALESCE(users.last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD')) DESC" else order << "users.created_at DESC" end @@ -81,22 +81,22 @@ class AdminUserIndexQuery where_conds << "user_stats.posts_read_count <= 1 AND user_stats.topics_entered <= 1" @query.activated - .references(:user_stats) - .includes(:user_profile) - .where("COALESCE(user_profiles.bio_raw, '') != ''") - .where('users.created_at <= ?', 1.day.ago) - .where(where_conds.map {|c| "(#{c})"}.join(" OR ")) + .references(:user_stats) + .includes(:user_profile) + .where("COALESCE(user_profiles.bio_raw, '') != ''") + .where('users.created_at <= ?', 1.day.ago) + .where(where_conds.map { |c| "(#{c})" }.join(" OR ")) end def filter_by_query_classification case params[:query] - when 'staff' then @query.where("admin or moderator") - when 'admins' then @query.where(admin: true) - when 'moderators' then @query.where(moderator: true) - when 'blocked' then @query.blocked - when 'suspended' then @query.suspended - when 'pending' then @query.not_suspended.where(approved: false) - when 'suspect' then suspect_users + when 'staff' then @query.where("admin or moderator") + when 'admins' then @query.where(admin: true) + when 'moderators' then @query.where(moderator: true) + when 'blocked' then @query.blocked + when 'suspended' then @query.suspended + when 'pending' then @query.not_suspended.where(approved: false) + when 'suspect' then suspect_users end end @@ -106,7 +106,7 @@ class AdminUserIndexQuery if ip = IPAddr.new(params[:filter]) rescue nil @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) else - @query.where('username_lower ILIKE :filter OR email ILIKE :filter', filter: "%#{params[:filter]}%") + @query.where('username_lower ILIKE :filter OR user_emails.email ILIKE :filter', filter: "%#{params[:filter]}%") end end end @@ -119,7 +119,7 @@ class AdminUserIndexQuery def filter_exclude if params[:exclude].present? - @query.where('id != ?', params[:exclude]) + @query.where('users.id != ?', params[:exclude]) end end diff --git a/lib/archetype.rb b/lib/archetype.rb index 7213c8b26f..8c8f3591ae 100644 --- a/lib/archetype.rb +++ b/lib/archetype.rb @@ -32,7 +32,7 @@ class Archetype @archetypes.values end - def self.register(name, options={}) + def self.register(name, options = {}) @archetypes ||= {} @archetypes[name] = Archetype.new(name, options) end diff --git a/lib/auth/current_user_provider.rb b/lib/auth/current_user_provider.rb index cdd4d495fc..1affbc8d87 100644 --- a/lib/auth/current_user_provider.rb +++ b/lib/auth/current_user_provider.rb @@ -12,12 +12,12 @@ class Auth::CurrentUserProvider end # log on a user and set cookies and session etc. - def log_on_user(user,session,cookies) + def log_on_user(user, session, cookies) raise NotImplementedError end # optional interface to be called to refresh cookies etc if needed - def refresh_session(user,session,cookies) + def refresh_session(user, session, cookies) end # api has special rights return true if api was detected @@ -35,7 +35,6 @@ class Auth::CurrentUserProvider raise NotImplementedError end - def log_off_user(session, cookies) raise NotImplementedError end diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 4be651472b..e94340c0de 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -44,7 +44,7 @@ class Auth::DefaultCurrentUserProvider current_user = nil if auth_token && auth_token.length == 32 - limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN ,60) + limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN , 60) if limiter.can_perform? @user_token = UserAuthToken.lookup(auth_token, @@ -127,8 +127,8 @@ class Auth::DefaultCurrentUserProvider if !@user_token.legacy && needs_rotation if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'], - client_ip: @request.ip, - path: @env['REQUEST_PATH']) + client_ip: @request.ip, + path: @env['REQUEST_PATH']) cookies[TOKEN_COOKIE] = cookie_hash(@user_token.unhashed_auth_token) end elsif @user_token.legacy @@ -200,7 +200,6 @@ class Auth::DefaultCurrentUserProvider cookies.delete(TOKEN_COOKIE) end - # api has special rights return true if api was detected def is_api? current_user diff --git a/lib/auth/facebook_authenticator.rb b/lib/auth/facebook_authenticator.rb index f7f90cb26b..f969c8dabf 100644 --- a/lib/auth/facebook_authenticator.rb +++ b/lib/auth/facebook_authenticator.rb @@ -52,8 +52,8 @@ class Auth::FacebookAuthenticator < Auth::Authenticator def register_middleware(omniauth) omniauth.provider :facebook, - :setup => lambda { |env| - strategy = env["omniauth.strategy"] + setup: lambda { |env| + strategy = env["omniauth.strategy"] strategy.options[:client_id] = SiteSetting.facebook_app_id strategy.options[:client_secret] = SiteSetting.facebook_app_secret strategy.options[:info_fields] = 'gender,email,name,about,first_name,link,last_name,website,location' @@ -61,7 +61,7 @@ class Auth::FacebookAuthenticator < Auth::Authenticator strategy.options[:scope] = 'email,user_about_me,user_location,user_website' end }, - :scope => "email" + scope: "email" end protected diff --git a/lib/auth/github_authenticator.rb b/lib/auth/github_authenticator.rb index e79414973a..41874a04c1 100644 --- a/lib/auth/github_authenticator.rb +++ b/lib/auth/github_authenticator.rb @@ -47,10 +47,8 @@ class Auth::GithubAuthenticator < Auth::Authenticator # Potentially use *any* of the emails from GitHub to find a match or # register a new user, with preference given to the primary email. all_emails = Array.new(auth_token[:extra][:all_emails]) - all_emails.unshift({ - :email => data[:email], - :verified => !!data[:email_verified] - }) + all_emails.unshift(email: data[:email], + verified: !!data[:email_verified]) # Only consider verified emails to match an existing user. We don't want # someone to be able to create a GitHub account with an unverified email @@ -106,14 +104,13 @@ class Auth::GithubAuthenticator < Auth::Authenticator ) end - def register_middleware(omniauth) omniauth.provider :github, - :setup => lambda { |env| - strategy = env["omniauth.strategy"] + setup: lambda { |env| + strategy = env["omniauth.strategy"] strategy.options[:client_id] = SiteSetting.github_client_id strategy.options[:client_secret] = SiteSetting.github_client_secret }, - :scope => "user:email" + scope: "user:email" end end diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 31e44ea25d..385aa92148 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -21,7 +21,7 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator if !result.user && !result.email.blank? && result.email_valid result.user = User.find_by_email(result.email) if result.user - ::GoogleUserInfo.create({user_id: result.user.id}.merge(google_hash)) + ::GoogleUserInfo.create({ user_id: result.user.id }.merge(google_hash)) end end @@ -30,7 +30,7 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator def after_create_account(user, auth) data = auth[:extra_data] - GoogleUserInfo.create({user_id: user.id}.merge(data)) + GoogleUserInfo.create({ user_id: user.id }.merge(data)) if auth[:email_valid].to_s == 'true' EmailToken.confirm(user.email_tokens.first.token) user.set_automatic_groups @@ -41,8 +41,8 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator # jwt encoding is causing auth to fail in quite a few conditions # skipping omniauth.provider :google_oauth2, - :setup => lambda { |env| - strategy = env["omniauth.strategy"] + setup: lambda { |env| + strategy = env["omniauth.strategy"] strategy.options[:client_id] = SiteSetting.google_oauth2_client_id strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret }, diff --git a/lib/auth/instagram_authenticator.rb b/lib/auth/instagram_authenticator.rb index ba2dd7f1bb..507981ef2a 100644 --- a/lib/auth/instagram_authenticator.rb +++ b/lib/auth/instagram_authenticator.rb @@ -39,8 +39,8 @@ class Auth::InstagramAuthenticator < Auth::Authenticator def register_middleware(omniauth) omniauth.provider :instagram, - :setup => lambda { |env| - strategy = env["omniauth.strategy"] + setup: lambda { |env| + strategy = env["omniauth.strategy"] strategy.options[:client_id] = SiteSetting.instagram_consumer_key strategy.options[:client_secret] = SiteSetting.instagram_consumer_secret } diff --git a/lib/auth/oauth2_authenticator.rb b/lib/auth/oauth2_authenticator.rb index 5484a19f5f..cadd1022a7 100644 --- a/lib/auth/oauth2_authenticator.rb +++ b/lib/auth/oauth2_authenticator.rb @@ -5,7 +5,7 @@ class Auth::OAuth2Authenticator < Auth::Authenticator end # only option at the moment is :trusted - def initialize(name, opts={}) + def initialize(name, opts = {}) @name = name @opts = opts end diff --git a/lib/auth/open_id_authenticator.rb b/lib/auth/open_id_authenticator.rb index 04f2764a9c..1bb17447c7 100644 --- a/lib/auth/open_id_authenticator.rb +++ b/lib/auth/open_id_authenticator.rb @@ -49,15 +49,14 @@ class Auth::OpenIdAuthenticator < Auth::Authenticator ) end - def register_middleware(omniauth) omniauth.provider :open_id, - :setup => lambda { |env| - strategy = env["omniauth.strategy"] + setup: lambda { |env| + strategy = env["omniauth.strategy"] strategy.options[:store] = OpenID::Store::Redis.new($redis) }, - :name => name, - :identifier => identifier, - :require => "omniauth-openid" + name: name, + identifier: identifier, + require: "omniauth-openid" end end diff --git a/lib/auth/result.rb b/lib/auth/result.rb index eabc699999..0c18d3e1e5 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -35,8 +35,8 @@ class Auth::Result if user.suspended? { suspended: true, - suspended_message: I18n.t( user.suspend_reason ? "login.suspended_with_reason" : "login.suspended", - {date: I18n.l(user.suspended_till, format: :date_only), reason: user.suspend_reason} ) + suspended_message: I18n.t(user.suspend_reason ? "login.suspended_with_reason" : "login.suspended", + date: I18n.l(user.suspended_till, format: :date_only), reason: user.suspend_reason) } else { diff --git a/lib/auth/twitter_authenticator.rb b/lib/auth/twitter_authenticator.rb index c5f91c0477..a88d2124c6 100644 --- a/lib/auth/twitter_authenticator.rb +++ b/lib/auth/twitter_authenticator.rb @@ -57,8 +57,8 @@ class Auth::TwitterAuthenticator < Auth::Authenticator def register_middleware(omniauth) omniauth.provider :twitter, - :setup => lambda { |env| - strategy = env["omniauth.strategy"] + setup: lambda { |env| + strategy = env["omniauth.strategy"] strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret } diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb index 98d872de8e..a5fe09acbe 100644 --- a/lib/autospec/formatter.rb +++ b/lib/autospec/formatter.rb @@ -16,7 +16,7 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter def start(example_count) super File.delete(RSPEC_RESULT) if File.exists?(RSPEC_RESULT) - @fail_file = File.open(RSPEC_RESULT,"w") + @fail_file = File.open(RSPEC_RESULT, "w") end def example_passed(_notification) diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb index 3cc52008e1..857967031c 100644 --- a/lib/autospec/manager.rb +++ b/lib/autospec/manager.rb @@ -9,7 +9,7 @@ module Autospec; end class Autospec::Manager - def self.run(opts={}) + def self.run(opts = {}) self.new(opts).run end @@ -66,10 +66,10 @@ class Autospec::Manager Autospec::QunitRunner.new end - def ensure_all_specs_will_run(current_runner=nil) + def ensure_all_specs_will_run(current_runner = nil) puts "@@@@@@@@@@@@ ensure_all_specs_will_run" if @debug - @queue.reject!{|_,s,_| s == "spec"} + @queue.reject! { |_, s, _| s == "spec" } if current_runner @queue.concat [['spec', 'spec', current_runner]] @@ -151,14 +151,14 @@ class Autospec::Manager # try focus tag if failed_specs.length > 0 - filename,_ = failed_specs[0].split(":") + filename, _ = failed_specs[0].split(":") if filename && File.exist?(filename) && !File.directory?(filename) spec = File.read(filename) - start,_ = spec.split(/\S*#focus\S*$/) + start, _ = spec.split(/\S*#focus\S*$/) if start.length < spec.length line = start.scan(/\n/).length + 1 puts "Found #focus tag on line #{line}!" - failed_specs = ["#{filename}:#{line+1}"] + failed_specs = ["#{filename}:#{line + 1}"] end end end @@ -188,7 +188,7 @@ class Autospec::Manager FileUtils.rm_f(socket_path) server = SocketServer.new(socket_path) server.start do |line| - file,line = line.split(' ') + file, line = line.split(' ') file = file.sub(Rails.root.to_s << "/", "") # process_change can aquire a mutex and block # the acceptor @@ -196,7 +196,7 @@ class Autospec::Manager if file =~ /(es6|js)$/ process_change([[file]]) else - process_change([[file,line]]) + process_change([[file, line]]) end end "OK" @@ -213,7 +213,7 @@ class Autospec::Manager listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _| paths = [modified, added].flatten paths.compact! - paths.map!{|long| long[(path.length+1)..-1]} + paths.map! { |long| long[(path.length + 1)..-1] } process_change(paths) end listener.start @@ -246,7 +246,7 @@ class Autospec::Manager end end # watchers - runner.watchers.each do |k,v| + runner.watchers.each do |k, v| if m = k.match(file) puts "@@@@@@@@@@@@ #{file} matched a watcher for #{runner}" if @debug hit = true diff --git a/lib/autospec/qunit_runner.rb b/lib/autospec/qunit_runner.rb index 577a7c824c..5c57064370 100644 --- a/lib/autospec/qunit_runner.rb +++ b/lib/autospec/qunit_runner.rb @@ -144,7 +144,7 @@ module Autospec end def try_to_find_module_name(file) - file,_ = file.split(/:\d+$/) + file, _ = file.split(/:\d+$/) return unless File.exists?(file) File.open(file, "r").each_line do |line| if m = /module\(['"]([^'"]+)/i.match(line) diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb index b6f5b90dd1..3fb9c09377 100644 --- a/lib/autospec/rspec_runner.rb +++ b/lib/autospec/rspec_runner.rb @@ -19,8 +19,8 @@ module Autospec watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } - watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb"} - watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb"} + watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } + watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } watch(%r{^plugins/.*/spec/.*\.rb}) diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb index 01c0aada5a..2c1fe8e656 100644 --- a/lib/autospec/simple_runner.rb +++ b/lib/autospec/simple_runner.rb @@ -13,8 +13,8 @@ module Autospec "-f", "Autospec::Formatter", specs.split].flatten.join(" ") # launch rspec Dir.chdir(Rails.root) do - env = {"RAILS_ENV" => "test"} - if specs.split(' ').any?{|s| s =~ /^(.\/)?plugins/} + env = { "RAILS_ENV" => "test" } + if specs.split(' ').any? { |s| s =~ /^(.\/)?plugins/ } env["LOAD_PLUGINS"] = "1" puts "Loading plugins while running specs" end diff --git a/lib/autospec/spork_runner.rb b/lib/autospec/spork_runner.rb index fbd05d93ef..b8ae2c8627 100644 --- a/lib/autospec/spork_runner.rb +++ b/lib/autospec/spork_runner.rb @@ -30,7 +30,7 @@ module Autospec def run(specs) args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", "-f", "Autospec::Formatter", specs.split].flatten - spork_service.run(args,$stderr,$stdout) + spork_service.run(args, $stderr, $stdout) end def reload @@ -66,7 +66,7 @@ module Autospec def write_pid_file(file, pid) FileUtils.mkdir_p(Rails.root + "tmp/pids") - File.open(file,'w') do |f| + File.open(file, 'w') do |f| f.write(pid) end end @@ -95,7 +95,7 @@ module Autospec sleep 1 end - @spork_pid = Process.spawn({'RAILS_ENV' => 'test'}, "bundle exec spork") + @spork_pid = Process.spawn({ 'RAILS_ENV' => 'test' }, "bundle exec spork") write_pid_file(spork_pid_file, @spork_pid) running = false diff --git a/lib/avatar_lookup.rb b/lib/avatar_lookup.rb index 4af184052a..d86871fee0 100644 --- a/lib/avatar_lookup.rb +++ b/lib/avatar_lookup.rb @@ -1,6 +1,6 @@ class AvatarLookup - def initialize(user_ids=[]) + def initialize(user_ids = []) @user_ids = user_ids.tap(&:compact!).tap(&:uniq!).tap(&:flatten!) end @@ -12,7 +12,7 @@ class AvatarLookup private def self.lookup_columns - @lookup_columns ||= %i{id email username uploaded_avatar_id} + @lookup_columns ||= %i{id user_emails.email username uploaded_avatar_id} end def users @@ -22,9 +22,10 @@ class AvatarLookup def user_lookup_hash # adding tap here is a personal taste thing hash = {} - User.where(:id => @user_ids) - .select(AvatarLookup.lookup_columns) - .each{ |user| hash[user.id] = user } + User.joins(:user_emails) + .where(id: @user_ids) + .select(AvatarLookup.lookup_columns) + .each { |user| hash[user.id] = user } hash end end diff --git a/lib/backup_restore/backup_restore.rb b/lib/backup_restore/backup_restore.rb index 12a9ecd21d..c16314f98c 100644 --- a/lib/backup_restore/backup_restore.rb +++ b/lib/backup_restore/backup_restore.rb @@ -11,11 +11,11 @@ module BackupRestore METADATA_FILE = "meta.json" LOGS_CHANNEL = "/admin/backups/logs" - def self.backup!(user_id, opts={}) + def self.backup!(user_id, opts = {}) start! BackupRestore::Backuper.new(user_id, opts) end - def self.restore!(user_id, opts={}) + def self.restore!(user_id, opts = {}) start! BackupRestore::Restorer.new(user_id, opts) end diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index c70a7a5850..9465457092 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -3,7 +3,7 @@ module BackupRestore class Backuper attr_reader :success - def initialize(user_id, opts={}) + def initialize(user_id, opts = {}) @user_id = user_id @client_id = opts[:client_id] @publish_to_message_bus = opts[:publish_to_message_bus] || false diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index de6e133df1..c0c2b41377 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -8,7 +8,7 @@ module BackupRestore class Restorer attr_reader :success - def initialize(user_id, opts={}) + def initialize(user_id, opts = {}) @user_id = user_id @client_id = opts[:client_id] @filename = opts[:filename] @@ -400,7 +400,7 @@ module BackupRestore end def notify_user - if user = User.find_by(email: @user_info[:email]) + if user = User.find_by_email(@user_info[:email]) log "Notifying '#{user.username}' of the end of the restore..." status = @success ? :restore_succeeded : :restore_failed diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index 20a08d7daf..cdb5ac8d6a 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -133,44 +133,44 @@ SQL GROUP BY acting_user_id SQL - def self.invite_badge(count,trust_level) -" - SELECT u.id user_id, current_timestamp granted_at - FROM users u - WHERE u.id IN ( - SELECT invited_by_id - FROM invites i - JOIN users u2 ON u2.id = i.user_id - WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.blocked - GROUP BY invited_by_id - HAVING COUNT(*) >= #{count.to_i} - ) AND u.active AND NOT u.blocked AND u.id > 0 AND - (:backfill OR u.id IN (:user_ids) ) -" + def self.invite_badge(count, trust_level) + " + SELECT u.id user_id, current_timestamp granted_at + FROM users u + WHERE u.id IN ( + SELECT invited_by_id + FROM invites i + JOIN users u2 ON u2.id = i.user_id + WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.blocked + GROUP BY invited_by_id + HAVING COUNT(*) >= #{count.to_i} + ) AND u.active AND NOT u.blocked AND u.id > 0 AND + (:backfill OR u.id IN (:user_ids) ) + " end def self.like_badge(count, is_topic) # we can do better with dates, but its hard work -" - SELECT p.user_id, p.id post_id, p.updated_at granted_at - FROM badge_posts p - WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND - (:backfill OR p.id IN (:post_ids) ) -" + " + SELECT p.user_id, p.id post_id, p.updated_at granted_at + FROM badge_posts p + WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND + (:backfill OR p.id IN (:post_ids) ) + " end def self.trust_level(level) # we can do better with dates, but its hard work figuring this out historically -" - SELECT u.id user_id, current_timestamp granted_at FROM users u - WHERE trust_level >= #{level.to_i} AND ( - :backfill OR u.id IN (:user_ids) - ) -" + " + SELECT u.id user_id, current_timestamp granted_at FROM users u + WHERE trust_level >= #{level.to_i} AND ( + :backfill OR u.id IN (:user_ids) + ) + " end def self.sharing_badge(count) -< 0 - on_drop&.call + on_drop&.call - columns.each do |column| - # safe cause it is protected on method entry, can not be passed in params - ActiveRecord::Base.exec_sql("ALTER TABLE #{table} DROP COLUMN IF EXISTS #{column}") - end + columns.each do |column| + # safe cause it is protected on method entry, can not be passed in params + ActiveRecord::Base.exec_sql("ALTER TABLE #{table} DROP COLUMN IF EXISTS #{column}") + + ActiveRecord::Base.exec_sql <<~SQL + DROP FUNCTION IF EXISTS #{readonly_function_name(table, column)}; + DROP TRIGGER IF EXISTS #{readonly_trigger_name(table, column)} ON #{table}; + SQL + end + + Discourse.reset_active_record_cache end end + + def self.mark_readonly(table_name, column_name) + ActiveRecord::Base.exec_sql <<-SQL + CREATE OR REPLACE FUNCTION #{readonly_function_name(table_name, column_name)} RETURNS trigger AS $rcr$ + BEGIN + RAISE EXCEPTION 'Discourse: #{column_name} in #{table_name} is readonly'; + END + $rcr$ LANGUAGE plpgsql; + SQL + + ActiveRecord::Base.exec_sql <<-SQL + CREATE TRIGGER #{readonly_trigger_name(table_name, column_name)} + BEFORE INSERT OR UPDATE OF #{column_name} + ON #{table_name} + FOR EACH ROW + WHEN (NEW.#{column_name} IS NOT NULL) + EXECUTE PROCEDURE #{readonly_function_name(table_name, column_name)}; + SQL + end + + private + + def self.readonly_function_name(table_name, column_name) + "raise_#{table_name}_#{column_name}_readonly()" + end + + def self.readonly_trigger_name(table_name, column_name) + "#{table_name}_#{column_name}_readonly" + end end diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index fa3c952abd..94d5a23efa 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -7,7 +7,7 @@ class ComposerMessagesFinder end def self.check_methods - @check_methods ||= instance_methods.find_all {|m| m =~ /^check\_/} + @check_methods ||= instance_methods.find_all { |m| m =~ /^check\_/ } end def find @@ -70,7 +70,7 @@ class ComposerMessagesFinder return if SiteSetting.disable_avatar_education_message || SiteSetting.sso_overrides_avatar || !SiteSetting.allow_uploaded_avatars # If we got this far, log that we've nagged them about the avatar - UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id ) + UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id) # Return the message { @@ -86,19 +86,19 @@ class ComposerMessagesFinder # Count the topics made by this user in the last day recent_posts_user_ids = Post.where(topic_id: @details[:topic_id]) - .where("created_at > ?", 1.day.ago) - .order('created_at desc') - .limit(SiteSetting.sequential_replies_threshold) - .pluck(:user_id) + .where("created_at > ?", 1.day.ago) + .order('created_at desc') + .limit(SiteSetting.sequential_replies_threshold) + .pluck(:user_id) # Did we get back as many posts as we asked for, and are they all by the current user? return if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || - recent_posts_user_ids.detect {|u| u != @user.id } + recent_posts_user_ids.detect { |u| u != @user.id } # If we got this far, log that we've nagged them about the sequential replies UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies], target_user_id: @user.id, - topic_id: @details[:topic_id] ) + topic_id: @details[:topic_id]) { id: 'sequential_replies', @@ -149,7 +149,7 @@ class ComposerMessagesFinder order('created_at desc'). limit(SiteSetting.get_a_room_threshold). pluck(:reply_to_user_id). - find_all {|uid| uid != @user.id && uid == reply_to_user_id} + find_all { |uid| uid != @user.id && uid == reply_to_user_id } return unless last_x_replies.size == SiteSetting.get_a_room_threshold return unless @topic.posts.count('distinct user_id') >= min_users_posted diff --git a/lib/content_buffer.rb b/lib/content_buffer.rb index 99d176d4cb..8db6146f0b 100644 --- a/lib/content_buffer.rb +++ b/lib/content_buffer.rb @@ -35,7 +35,7 @@ class ContentBuffer end # fix last line - @lines[start_row] << @lines[finish_row][finish_col-1..-1] + @lines[start_row] << @lines[finish_row][finish_col - 1..-1] end if transform[:operation] == :insert diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 9d950f4cf0..585a9e995a 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -9,7 +9,7 @@ class CookedPostProcessor attr_reader :cooking_options - def initialize(post, opts={}) + def initialize(post, opts = {}) @dirty = false @opts = opts @post = post @@ -63,7 +63,7 @@ class CookedPostProcessor upload_ids |= oneboxed_image_uploads.pluck(:id) - values = upload_ids.map{ |u| "(#{@post.id},#{u})" }.join(",") + values = upload_ids.map { |u| "(#{@post.id},#{u})" }.join(",") PostUpload.transaction do PostUpload.delete_all(post_id: @post.id) if upload_ids.length > 0 @@ -80,8 +80,6 @@ class CookedPostProcessor limit_size!(img) convert_to_link!(img) end - - update_post_image end def extract_images @@ -102,8 +100,6 @@ class CookedPostProcessor @doc.css("img[src]") - # minus, emojis @doc.css("img.emoji") - - # minus, image inside oneboxes - oneboxed_images - # minus, images inside quotes @doc.css(".quote img") end @@ -142,11 +138,11 @@ class CookedPostProcessor original_width, original_height = original_image_size.map(&:to_f) if w > 0 - ratio = w/original_width - [w.floor, (original_height*ratio).floor] + ratio = w / original_width + [w.floor, (original_height * ratio).floor] else - ratio = h/original_height - [(original_width*ratio).floor, h.floor] + ratio = h / original_height + [(original_width * ratio).floor, h.floor] end end end @@ -238,7 +234,7 @@ class CookedPostProcessor false end - def add_lightbox!(img, original_width, original_height, upload=nil) + def add_lightbox!(img, original_width, original_height, upload = nil) # first, create a div to hold our lightbox lightbox = Nokogiri::XML::Node.new("div", @doc) lightbox["class"] = "lightbox-wrapper" @@ -283,7 +279,7 @@ class CookedPostProcessor return I18n.t("upload.pasted_image_filename") end - def create_span_node(klass, content=nil) + def create_span_node(klass, content = nil) span = Nokogiri::XML::Node.new("span", @doc) span.content = content if content span["class"] = klass @@ -292,6 +288,8 @@ class CookedPostProcessor def update_post_image img = extract_images_for_post.first + return if img.blank? + if img["src"].present? @post.update_column(:image_url, img["src"][0...255]) # post @post.topic.update_column(:image_url, img["src"][0...255]) if @post.is_first_post? # topic @@ -310,6 +308,11 @@ class CookedPostProcessor Oneboxer.onebox(url, args) end + update_post_image + + # make sure we grab dimensions for oneboxed images + oneboxed_images.each { |img| limit_size!(img) } + uploads = oneboxed_image_uploads.select(:url, :origin) oneboxed_images.each do |img| upload = uploads.detect { |u| u.origin == img["src"] } @@ -374,7 +377,7 @@ class CookedPostProcessor # log the site setting change reason = I18n.t("disable_remote_images_download_reason") staff_action_logger = StaffActionLogger.new(Discourse.system_user) - staff_action_logger.log_site_setting_change("download_remote_images_to_local", true, false, { details: reason }) + staff_action_logger.log_site_setting_change("download_remote_images_to_local", true, false, details: reason) # also send a private message to the site contact user notify_about_low_disk_space diff --git a/lib/current_user.rb b/lib/current_user.rb index 01c5570028..a5035b9764 100644 --- a/lib/current_user.rb +++ b/lib/current_user.rb @@ -8,19 +8,18 @@ module CurrentUser Discourse.current_user_provider.new(env).current_user end - # can be used to pretend current user does no exist, for CSRF attacks def clear_current_user @current_user_provider = Discourse.current_user_provider.new({}) end def log_on_user(user) - current_user_provider.log_on_user(user,session,cookies) + current_user_provider.log_on_user(user, session, cookies) user.logged_in end def log_off_user - current_user_provider.log_off_user(session,cookies) + current_user_provider.log_off_user(session, cookies) end def is_api? @@ -36,7 +35,7 @@ module CurrentUser end def refresh_session(user) - current_user_provider.refresh_session(user,session,cookies) + current_user_provider.refresh_session(user, session, cookies) end private diff --git a/lib/demon/base.rb b/lib/demon/base.rb index 6fb162faad..188a27e9fe 100644 --- a/lib/demon/base.rb +++ b/lib/demon/base.rb @@ -7,7 +7,7 @@ class Demon::Base @demons end - def self.start(count=1) + def self.start(count = 1) @demons ||= {} count.times do |i| (@demons["#{prefix}_#{i}"] ||= new(i)).start @@ -50,7 +50,7 @@ class Demon::Base "#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid" end - def alive?(pid=nil) + def alive?(pid = nil) pid ||= @pid if pid Demon::Base.alive?(pid) @@ -63,14 +63,14 @@ class Demon::Base @started = false if @pid # TODO configurable stop signal - Process.kill("HUP",@pid) + Process.kill("HUP", @pid) wait_for_stop = lambda { timeout = @stop_timeout while alive? && timeout > 0 - timeout -= (@stop_timeout/10.0) - sleep(@stop_timeout/10.0) + timeout -= (@stop_timeout / 10.0) + sleep(@stop_timeout / 10.0) Process.waitpid(@pid, Process::WNOHANG) rescue -1 end @@ -86,7 +86,6 @@ class Demon::Base wait_for_stop.call - @pid = nil @started = false end @@ -116,7 +115,7 @@ class Demon::Base if existing = already_running? # should not happen ... so kill violently STDERR.puts "Attempting to kill pid #{existing}" - Process.kill("TERM",existing) + Process.kill("TERM", existing) end @started = true @@ -156,7 +155,7 @@ class Demon::Base def write_pid_file FileUtils.mkdir_p(Rails.root + "tmp/pids") - File.open(pid_file,'w') do |f| + File.open(pid_file, 'w') do |f| f.write(@pid) end end @@ -182,7 +181,6 @@ class Demon::Base end end - def suppress_stdout true end diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb index 92d15a8fb2..702876c87e 100644 --- a/lib/demon/rails_autospec.rb +++ b/lib/demon/rails_autospec.rb @@ -12,9 +12,9 @@ class Demon::RailsAutospec < Demon::Base require "rack" ENV["RAILS_ENV"] = "test" Rack::Server.start( - :config => "config.ru", - :AccessLog => [], - :Port => ENV["TEST_SERVER_PORT"] || 60099, + config: "config.ru", + AccessLog: [], + Port: ENV["TEST_SERVER_PORT"] || 60099, ) rescue => e STDERR.puts e.message diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index a6f0194415..85e343c987 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -7,7 +7,7 @@ class Demon::Sidekiq < Demon::Base end def self.after_fork(&blk) - blk ? (@blk=blk) : @blk + blk ? (@blk = blk) : @blk end private diff --git a/lib/directory_helper.rb b/lib/directory_helper.rb index ec8eaccc5a..7f8e6b86e3 100644 --- a/lib/directory_helper.rb +++ b/lib/directory_helper.rb @@ -2,7 +2,7 @@ module DirectoryHelper def tmp_directory(prefix) directory_cache[prefix] ||= begin - f = File.join( Rails.root, 'tmp', Time.now.strftime("#{prefix}%Y%m%d%H%M%S") ) + f = File.join(Rails.root, 'tmp', Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) FileUtils.mkdir_p(f) unless Dir[f].present? f end diff --git a/lib/discourse.rb b/lib/discourse.rb index 086ed28b72..dd9d9d3fef 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -63,7 +63,7 @@ module Discourse # When they don't have permission to do something class InvalidAccess < StandardError attr_reader :obj - def initialize(msg=nil, obj=nil) + def initialize(msg = nil, obj = nil) super(msg) @obj = obj end @@ -115,7 +115,6 @@ module Discourse set end - def self.activate_plugins! all_plugins = Plugin::Instance.find_all("#{Rails.root}/plugins") @@ -144,11 +143,11 @@ module Discourse end def self.official_plugins - plugins.find_all{|p| p.metadata.official?} + plugins.find_all { |p| p.metadata.official? } end def self.unofficial_plugins - plugins.find_all{|p| !p.metadata.official?} + plugins.find_all { |p| !p.metadata.official? } end def self.assets_digest @@ -214,6 +213,25 @@ module Discourse base_url_no_prefix + base_uri end + def self.route_for(uri) + + uri = URI(uri) rescue nil unless (uri.is_a?(URI)) + return unless uri + + path = uri.path || "" + if (uri.host == Discourse.current_hostname && + path.start_with?(Discourse.base_uri)) || + !uri.host + + path.slice!(Discourse.base_uri) + return Rails.application.routes.recognize_path(path) + end + + nil + rescue ActionController::RoutingError + nil + end + READONLY_MODE_KEY_TTL ||= 60 READONLY_MODE_KEY ||= 'readonly_mode'.freeze PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres'.freeze @@ -359,7 +377,7 @@ module Discourse Rails.cache.reconnect Logster.store.redis.reconnect # shuts down all connections in the pool - Sidekiq.redis_pool.shutdown{|c| nil} + Sidekiq.redis_pool.shutdown { |c| nil } # re-establish Sidekiq.redis = sidekiq_redis_config start_connection_reaper @@ -383,7 +401,7 @@ module Discourse sleep GlobalSetting.connection_reaper_interval reap_connections(GlobalSetting.connection_reaper_age, GlobalSetting.connection_reaper_max_age) rescue => e - Discourse.handle_exception(e, {message: "Error reaping connections"}) + Discourse.handle_exception(e, message: "Error reaping connections") end end end @@ -391,7 +409,7 @@ module Discourse def self.reap_connections(idle, max_age) pools = [] - ObjectSpace.each_object(ActiveRecord::ConnectionAdapters::ConnectionPool){|pool| pools << pool} + ObjectSpace.each_object(ActiveRecord::ConnectionAdapters::ConnectionPool) { |pool| pools << pool } pools.each do |pool| pool.drain(idle.seconds, max_age.seconds) @@ -414,7 +432,7 @@ module Discourse def self.reset_active_record_cache_if_needed(e) last_cache_reset = Discourse.last_ar_cache_reset - if e && e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago) + if e && e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago) Rails.logger.warn "Clear Active Record cache cause schema appears to have changed!" Discourse.last_ar_cache_reset = Time.zone.now Discourse.reset_active_record_cache @@ -429,5 +447,4 @@ module Discourse nil end - end diff --git a/lib/discourse_cookie_store.rb b/lib/discourse_cookie_store.rb index f962ba3a0c..2dbd56792f 100644 --- a/lib/discourse_cookie_store.rb +++ b/lib/discourse_cookie_store.rb @@ -1,6 +1,6 @@ class ActionDispatch::Session::DiscourseCookieStore < ActionDispatch::Session::CookieStore - def initialize(app, options={}) - super(app,options) + def initialize(app, options = {}) + super(app, options) end private diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index 174ca23fb8..940800be17 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -5,7 +5,7 @@ module DiscourseHub STATS_FETCHED_AT_KEY = "stats_fetched_at" def self.version_check_payload - default_payload = { installed_version: Discourse::VERSION::STRING }.merge!(Discourse.git_branch == "unknown" ? {} : {branch: Discourse.git_branch}) + default_payload = { installed_version: Discourse::VERSION::STRING }.merge!(Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch }) default_payload.merge!(get_payload) end @@ -23,23 +23,23 @@ module DiscourseHub SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago ? About.fetch_cached_stats.symbolize_keys : {} end - def self.get(rel_url, params={}) + def self.get(rel_url, params = {}) singular_action :get, rel_url, params end - def self.post(rel_url, params={}) + def self.post(rel_url, params = {}) collection_action :post, rel_url, params end - def self.put(rel_url, params={}) + def self.put(rel_url, params = {}) collection_action :put, rel_url, params end - def self.delete(rel_url, params={}) + def self.delete(rel_url, params = {}) singular_action :delete, rel_url, params end - def self.singular_action(action, rel_url, params={}) + def self.singular_action(action, rel_url, params = {}) JSON.parse(Excon.send(action, "#{hub_base_url}#{rel_url}", headers: { 'Referer' => referer, 'Accept' => accepts.join(', ') }, @@ -48,7 +48,7 @@ module DiscourseHub ).body) end - def self.collection_action(action, rel_url, params={}) + def self.collection_action(action, rel_url, params = {}) JSON.parse(Excon.send(action, "#{hub_base_url}#{rel_url}", body: JSON[params], diff --git a/lib/discourse_plugin.rb b/lib/discourse_plugin.rb index b39b676c02..a2f86705ae 100644 --- a/lib/discourse_plugin.rb +++ b/lib/discourse_plugin.rb @@ -27,10 +27,10 @@ class DiscoursePlugin # Find the modules defined in the plugin with "Mixin" in their name. def self.mixins constants.map { |const_name| const_get(const_name) } - .select { |const| const.class == Module && const.name["Mixin"] } + .select { |const| const.class == Module && const.name["Mixin"] } end - def register_js(file, opts={}) + def register_js(file, opts = {}) @registry.register_js(file, opts) end @@ -38,7 +38,7 @@ class DiscoursePlugin @registry.register_css(file) end - def register_archetype(name, options={}) + def register_archetype(name, options = {}) @registry.register_archetype(name, options) end @@ -48,4 +48,3 @@ class DiscoursePlugin end end - diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 568c48932d..9e404c16c5 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -70,7 +70,7 @@ class DiscoursePluginRegistry end - def register_js(filename, options={}) + def register_js(filename, options = {}) # If we have a server side option, add that too. self.class.javascripts << filename end @@ -79,15 +79,15 @@ class DiscoursePluginRegistry self.class.stylesheets << filename end - def register_archetype(name, options={}) + def register_archetype(name, options = {}) Archetype.register(name, options) end - def self.register_glob(root, extension, options=nil) + def self.register_glob(root, extension, options = nil) self.asset_globs << [root, extension, options || {}] end - def self.each_globbed_asset(each_options=nil) + def self.each_globbed_asset(each_options = nil) each_options ||= {} self.asset_globs.each do |g| @@ -108,7 +108,7 @@ class DiscoursePluginRegistry JS_REGEX = /\.js$|\.js\.erb$|\.js\.es6$/ HANDLEBARS_REGEX = /\.hbs$|\.js\.handlebars$/ - def self.register_asset(asset, opts=nil) + def self.register_asset(asset, opts = nil) if asset =~ JS_REGEX if opts == :admin self.admin_javascripts << asset @@ -140,7 +140,7 @@ class DiscoursePluginRegistry html_builders[name] = block end - def self.build_html(name, ctx=nil) + def self.build_html(name, ctx = nil) html_builders[name]&.call(ctx) end diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index aaa5f74ef7..f646cba58e 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -15,6 +15,7 @@ class DiscourseRedis @mutex = Mutex.new @slave_config = DiscourseRedis.slave_config @timer_task = init_timer_task + @message_bus_keepalive_interval = MessageBus.keepalive_interval end def verify_master @@ -41,6 +42,7 @@ class DiscourseRedis slave_client.call([:client, [:kill, 'type', connection_type]]) end + MessageBus.keepalive_interval = @message_bus_keepalive_interval Discourse.clear_readonly! Discourse.request_refresh! success = true @@ -57,7 +59,12 @@ class DiscourseRedis end def master=(args) - synchronize { @master = args } + synchronize do + @master = args + + # Disables MessageBus keepalive when Redis is in readonly mode + MessageBus.keepalive_interval = 0 if !@master + end end def running? @@ -125,10 +132,10 @@ class DiscourseRedis end def self.slave_config(options = config) - options.dup.merge!({ host: options[:slave_host], port: options[:slave_port] }) + options.dup.merge!(host: options[:slave_host], port: options[:slave_port]) end - def initialize(config=nil) + def initialize(config = nil) @config = config || DiscourseRedis.config @redis = DiscourseRedis.raw_connection(@config) end @@ -146,7 +153,7 @@ class DiscourseRedis yield rescue Redis::CommandError => ex if ex.message =~ /READONLY/ - unless Discourse.recently_readonly? + unless Discourse.recently_readonly? || Rails.env.test? STDERR.puts "WARN: Redis is in a readonly state. Performed a noop" end @@ -182,7 +189,7 @@ class DiscourseRedis end def mget(*args) - args.map!{|a| "#{namespace}:#{a}"} + args.map! { |a| "#{namespace}:#{a}" } DiscourseRedis.ignore_readonly { @redis.mget(*args) } end @@ -193,10 +200,10 @@ class DiscourseRedis end end - def keys(pattern=nil) + def keys(pattern = nil) DiscourseRedis.ignore_readonly do len = namespace.length + 1 - @redis.keys("#{namespace}:#{pattern || '*'}").map{ + @redis.keys("#{namespace}:#{pattern || '*'}").map { |k| k[len..-1] } end @@ -210,7 +217,7 @@ class DiscourseRedis def flushdb DiscourseRedis.ignore_readonly do - keys.each{|k| del(k)} + keys.each { |k| del(k) } end end diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index 67e4d71171..0fcf8bda9b 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -3,7 +3,6 @@ module DiscourseTagging TAGS_FIELD_NAME = "tags" TAGS_FILTER_REGEXP = /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<> - def self.tag_topic_by_names(topic, guardian, tag_names_arg, append: false) if SiteSetting.tagging_enabled tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || [] @@ -33,11 +32,9 @@ module DiscourseTagging # guardian is explicitly nil cause we don't want to strip all # staff tags that already passed validation - tags = filter_allowed_tags(Tag.where(name: tag_names), nil, { - for_input: true, - category: category, - selected_tags: tag_names - }).to_a + tags = filter_allowed_tags(Tag.where(name: tag_names), nil, for_input: true, + category: category, + selected_tags: tag_names).to_a if tags.size < tag_names.size && (category.nil? || category.tags.count == 0) tag_names.each do |name| @@ -51,7 +48,7 @@ module DiscourseTagging else topic.tags = [] end - topic.tags_changed=true + topic.tags_changed = true end true end @@ -61,7 +58,7 @@ module DiscourseTagging # category: a Category to which the object being tagged belongs # for_input: result is for an input field, so only show permitted tags # selected_tags: an array of tag names that are in the current selection - def self.filter_allowed_tags(query, guardian, opts={}) + def self.filter_allowed_tags(query, guardian, opts = {}) term = opts[:term] if term.present? term.gsub!("_", "\\_") @@ -124,9 +121,9 @@ module DiscourseTagging else # One tag per group restriction exclude_group_ids = TagGroup.where(one_per_topic: true) - .joins(:tag_group_memberships) - .where('tag_group_memberships.tag_id in (?)', selected_tag_ids) - .pluck(:id) + .joins(:tag_group_memberships) + .where('tag_group_memberships.tag_id in (?)', selected_tag_ids) + .pluck(:id) if exclude_group_ids.empty? sql = "tags.id NOT IN (#{select_sql} WHERE tg.parent_tag_id NOT IN (?))" @@ -135,9 +132,9 @@ module DiscourseTagging # It's possible that the selected tags violate some one-tag-per-group restrictions, # so filter them out by picking one from each group. limit_tag_ids = TagGroupMembership.select('distinct on (tag_group_id) tag_id') - .where(tag_id: selected_tag_ids) - .where(tag_group_id: exclude_group_ids) - .map(&:tag_id) + .where(tag_id: selected_tag_ids) + .where(tag_group_id: exclude_group_ids) + .map(&:tag_id) sql = "(tags.id NOT IN (#{select_sql} WHERE (tg.parent_tag_id NOT IN (?) OR tg.id in (?))) OR tags.id IN (?))" query = query.where(sql, selected_tag_ids, exclude_group_ids, limit_tag_ids) end @@ -149,8 +146,8 @@ module DiscourseTagging def self.clean_tag(tag) tag.downcase.strip - .gsub(/\s+/, '-').squeeze('-') - .gsub(TAGS_FILTER_REGEXP, '')[0...SiteSetting.max_tag_length] + .gsub(/\s+/, '-').squeeze('-') + .gsub(TAGS_FILTER_REGEXP, '')[0...SiteSetting.max_tag_length] end def self.staff_only_tags(tags) @@ -164,7 +161,7 @@ module DiscourseTagging tag_diff.present? ? tag_diff : nil end - def self.tags_for_saving(tags_arg, guardian, opts={}) + def self.tags_for_saving(tags_arg, guardian, opts = {}) return [] unless guardian.can_tag_topics? && tags_arg.present? @@ -179,7 +176,7 @@ module DiscourseTagging return opts[:unlimited] ? tag_names : tag_names[0...SiteSetting.max_tags_per_topic] end - def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts={}) + def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts = {}) tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || [] if taggable.tags.pluck(:name).sort != tag_names.sort taggable.tags = Tag.where(name: tag_names).all diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb index 3e2f14c682..dd56806360 100644 --- a/lib/discourse_updates.rb +++ b/lib/discourse_updates.rb @@ -33,10 +33,10 @@ module DiscourseUpdates # Handle cases when version check data is old so we report something that makes sense - if (version_info.updated_at.nil? or # never performed a version check - last_installed_version != Discourse::VERSION::STRING or # upgraded since the last version check - (version_info.missing_versions_count == 0 and version_info.latest_version != version_info.installed_version) or # old data - (version_info.missing_versions_count != 0 and version_info.latest_version == version_info.installed_version)) # old data + if (version_info.updated_at.nil? || # never performed a version check + last_installed_version != (Discourse::VERSION::STRING) || # upgraded since the last version check + (version_info.missing_versions_count == (0) && version_info.latest_version != (version_info.installed_version)) || # old data + (version_info.missing_versions_count != (0) && version_info.latest_version == (version_info.installed_version))) # old data Jobs.enqueue(:version_check, all_sites: true) version_info.version_check_pending = true unless version_info.updated_at.nil? @@ -92,7 +92,7 @@ module DiscourseUpdates if versions.present? # store the list in redis version_keys = [] - versions[0,5].each do |v| + versions[0, 5].each do |v| key = "#{missing_versions_key_prefix}:#{v['version']}" $redis.mapped_hmset key, v version_keys << key @@ -108,7 +108,6 @@ module DiscourseUpdates keys.present? ? keys.map { |k| $redis.hgetall(k) } : [] end - private def last_installed_version_key diff --git a/lib/distributed_cache.rb b/lib/distributed_cache.rb index 3175332036..2b08e64216 100644 --- a/lib/distributed_cache.rb +++ b/lib/distributed_cache.rb @@ -17,7 +17,7 @@ class DistributedCache end def self.process_message(message) - i = @subscribers.length-1 + i = @subscribers.length - 1 payload = message.data @@ -32,9 +32,9 @@ class DistributedCache hash = current.hash(message.site_id) case payload["op"] - when "set" then hash[payload["key"]] = payload["marshalled"] ? Marshal.load(Base64.decode64(payload["value"])) : payload["value"] - when "delete" then hash.delete(payload["key"]) - when "clear" then hash.clear + when "set" then hash[payload["key"]] = payload["marshalled"] ? Marshal.load(Base64.decode64(payload["value"])) : payload["value"] + when "delete" then hash.delete(payload["key"]) + when "clear" then hash.clear end rescue WeakRef::RefError @@ -66,22 +66,22 @@ class DistributedCache message[:origin] = hash.identity message[:hash_key] = hash.key message[:discourse_version] = Discourse.git_version - MessageBus.publish(channel_name, message, { user_ids: [-1] }) + MessageBus.publish(channel_name, message, user_ids: [-1]) end def self.set(hash, key, value) # special support for set marshal = (Set === value || Hash === value) value = Base64.encode64(Marshal.dump(value)) if marshal - publish(hash, { op: :set, key: key, value: value, marshalled: marshal }) + publish(hash, op: :set, key: key, value: value, marshalled: marshal) end def self.delete(hash, key) - publish(hash, { op: :delete, key: key }) + publish(hash, op: :delete, key: key) end def self.clear(hash) - publish(hash, { op: :clear }) + publish(hash, op: :clear) end def self.register(hash) @@ -103,7 +103,7 @@ class DistributedCache (@seed_id ||= SecureRandom.hex) + "#{Process.pid}" end - def []=(k,v) + def []=(k, v) k = k.to_s if Symbol === k DistributedCache.set(self, k, v) hash[k] = v diff --git a/lib/distributed_memoizer.rb b/lib/distributed_memoizer.rb index c5e7031d37..e80b687923 100644 --- a/lib/distributed_memoizer.rb +++ b/lib/distributed_memoizer.rb @@ -19,7 +19,7 @@ class DistributedMemoizer begin while Time.new < start + MAX_WAIT && !got_lock LOCK.synchronize do - got_lock = get_lock(redis,redis_lock_key) + got_lock = get_lock(redis, redis_lock_key) end sleep 0.001 end @@ -38,7 +38,6 @@ class DistributedMemoizer result end - def self.redis_lock_key(key) "memoize_lock_" << key end diff --git a/lib/distributed_mutex.rb b/lib/distributed_mutex.rb index 6a0e0af8cd..b014a554a3 100644 --- a/lib/distributed_mutex.rb +++ b/lib/distributed_mutex.rb @@ -1,11 +1,11 @@ # Cross-process locking using Redis. class DistributedMutex - def self.synchronize(key, redis=nil, &blk) + def self.synchronize(key, redis = nil, &blk) self.new(key, redis).synchronize(&blk) end - def initialize(key, redis=nil) + def initialize(key, redis = nil) @key = key @redis = redis || $redis @mutex = Mutex.new diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index 7632a24484..c72d7bca9e 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -17,7 +17,7 @@ module Email class MessageBuilder attr_reader :template_args - def initialize(to, opts=nil) + def initialize(to, opts = nil) @to = to @opts = opts || {} @@ -166,7 +166,6 @@ module Email result end - protected def reply_key @@ -195,11 +194,13 @@ module Email @reply_by_email_address = SiteSetting.reply_by_email_address.dup @reply_by_email_address.gsub!("%{reply_key}", reply_key) - @reply_by_email_address = if private_reply? - alias_email(@reply_by_email_address) - else - site_alias_email(@reply_by_email_address) - end + + @reply_by_email_address = + if private_reply? + alias_email(@reply_by_email_address) + else + site_alias_email(@reply_by_email_address) + end end def alias_email(source) diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 71c9b846f7..a13d3814df 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -2,12 +2,12 @@ module Email class Processor - def initialize(mail, retry_on_rate_limit=true) + def initialize(mail, retry_on_rate_limit = true) @mail = mail @retry_on_rate_limit = retry_on_rate_limit end - def self.process!(mail, retry_on_rate_limit=true) + def self.process!(mail, retry_on_rate_limit = true) Email::Processor.new(mail, retry_on_rate_limit).process! end @@ -35,23 +35,24 @@ module Email def handle_failure(mail_string, e, incoming_email) message_template = case e - when Email::Receiver::EmptyEmailError then :email_reject_empty - when Email::Receiver::NoBodyDetectedError then :email_reject_empty - when Email::Receiver::UserNotFoundError then :email_reject_user_not_found - when Email::Receiver::ScreenedEmailError then :email_reject_screened_email - when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated - when Email::Receiver::InactiveUserError then :email_reject_inactive_user - when Email::Receiver::BlockedUserError then :email_reject_blocked_user - when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address - when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed - when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level - when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching - when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found - when Email::Receiver::TopicClosedError then :email_reject_topic_closed - when Email::Receiver::InvalidPost then :email_reject_invalid_post - when ActiveRecord::Rollback then :email_reject_invalid_post - when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action - when Discourse::InvalidAccess then :email_reject_invalid_access + when Email::Receiver::EmptyEmailError then :email_reject_empty + when Email::Receiver::NoBodyDetectedError then :email_reject_empty + when Email::Receiver::UserNotFoundError then :email_reject_user_not_found + when Email::Receiver::ScreenedEmailError then :email_reject_screened_email + when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated + when Email::Receiver::InactiveUserError then :email_reject_inactive_user + when Email::Receiver::BlockedUserError then :email_reject_blocked_user + when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address + when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed + when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level + when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching + when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found + when Email::Receiver::TopicClosedError then :email_reject_topic_closed + when Email::Receiver::InvalidPost then :email_reject_invalid_post + when ActiveRecord::Rollback then :email_reject_invalid_post + when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action + when Discourse::InvalidAccess then :email_reject_invalid_access + else :email_reject_unrecognized_error end template_args = {} @@ -63,6 +64,14 @@ module Email template_args[:post_error] = e.message end + if message_template == :email_reject_unrecognized_error + msg = "Unrecognized error type (#{e.class}: #{e.message}) when processing incoming email" + msg += "\n\nBacktrace:\n#{e.backtrace.map { |l| " #{l}" }.join("\n")}" + msg += "\n\nMail:\n#{mail_string}" + + Rails.logger.error(msg) + end + if message_template # inform the user about the rejection message = Mail::Message.new(mail_string) @@ -76,18 +85,14 @@ module Email if can_send_rejection_email?(message.from, message_template) Email::Sender.new(client_message, message_template).send end - else - msg = "Unrecognized error type (#{e.class}: #{e.message}) when processing incoming email" - msg += "\n\nBacktrace:\n#{e.backtrace.map { |l| " #{l}" }.join("\n")}" - msg += "\n\nMail:\n#{mail_string}" - - Rails.logger.error(msg) end client_message end def can_send_rejection_email?(email, type) + return true if type == :email_reject_unrecognized_error + key = "rejection_email:#{email}:#{type}:#{Date.today}" if $redis.setnx(key, "1") diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index d4cdee0f7f..94a8dd77b3 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -153,7 +153,7 @@ module Email if $redis.setnx(key, "1") $redis.expire(key, 25.hours) - if user = User.find_by(email: email) + if user = User.find_by_email(email) user.user_stat.bounce_score += score user.user_stat.reset_bounce_score_after = SiteSetting.reset_bounce_score_after_days.days.from_now user.user_stat.save @@ -166,7 +166,7 @@ module Email elsif bounce_score >= SiteSetting.bounce_score_threshold # NOTE: we check bounce_score before sending emails, nothing to do # here other than log it happened. - reason = I18n.t("user.email.revoked", { email: user.email, date: user.user_stat.reset_bounce_score_after }) + reason = I18n.t("user.email.revoked", email: user.email, date: user.user_stat.reset_bounce_score_after) StaffActionLogger.new(Discourse.system_user).log_revoke_email(user, reason) end end @@ -466,7 +466,7 @@ module Email true end - def self.reply_by_email_address_regex(extract_reply_key=true) + def self.reply_by_email_address_regex(extract_reply_key = true) reply_addresses = [SiteSetting.reply_by_email_address] reply_addresses << (SiteSetting.alternative_reply_by_email_addresses.presence || "").split("|") @@ -538,11 +538,11 @@ module Email PostActionType.types[:like] if likes.include?(body.strip.downcase) end - def create_topic(options={}) + def create_topic(options = {}) create_post_with_attachments(options) end - def create_reply(options={}) + def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? if post_action_type = post_action_for(options[:raw]) @@ -572,7 +572,7 @@ module Email end end - def create_post_with_attachments(options={}) + def create_post_with_attachments(options = {}) # deal with attachments attachments.each do |attachment| tmp = Tempfile.new(["discourse-email-attachment", File.extname(attachment.filename)]) @@ -612,7 +612,7 @@ module Email end end - def create_post(options={}) + def create_post(options = {}) options[:via_email] = true options[:raw_email] = @raw_email @@ -622,7 +622,7 @@ module Email raise InvalidPost, "No post creation date found. Is the e-mail missing a Date: header?" end - options[:created_at] = DateTime.now if options[:created_at] > DateTime.now + options[:created_at] = DateTime.now if options[:created_at] > DateTime.now is_private_message = options[:archetype] == Archetype.private_message || options[:topic].try(:private_message?) diff --git a/lib/email/renderer.rb b/lib/email/renderer.rb index 9b209781ff..be802b7a99 100644 --- a/lib/email/renderer.rb +++ b/lib/email/renderer.rb @@ -3,7 +3,7 @@ require_dependency 'email/styles' module Email class Renderer - def initialize(message, opts=nil) + def initialize(message, opts = nil) @message = message @opts = opts || {} end diff --git a/lib/email/sender.rb b/lib/email/sender.rb index 61c5fc1685..08e141a923 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -15,7 +15,7 @@ SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError] module Email class Sender - def initialize(message, email_type, user=nil) + def initialize(message, email_type, user = nil) @message = message @email_type = email_type @user = user @@ -55,8 +55,8 @@ module Email # These are the links we add when a user uploads a file or image. # Ideally we would parse general markdown into plain text, but that is almost an intractable problem. url_prefix = Discourse.base_url - @message.parts[0].body = @message.parts[0].body.to_s.gsub(/([^<]*)<\/a>/, '[\2]('+url_prefix+'\1)') - @message.parts[0].body = @message.parts[0].body.to_s.gsub(/]*)>/, '![]('+url_prefix+'\1)') + @message.parts[0].body = @message.parts[0].body.to_s.gsub(/([^<]*)<\/a>/, '[\2](' + url_prefix + '\1)') + @message.parts[0].body = @message.parts[0].body.to_s.gsub(/]*)>/, '![](' + url_prefix + '\1)') @message.text_part.content_type = 'text/plain; charset=UTF-8' @@ -88,8 +88,8 @@ module Email "" referenced_posts = Post.includes(:incoming_email) - .where(id: PostReply.where(reply_id: post_id).select(:post_id)) - .order(id: :desc) + .where(id: PostReply.where(reply_id: post_id).select(:post_id)) + .order(id: :desc) referenced_post_message_ids = referenced_posts.map do |post| if post.incoming_email&.message_id.present? @@ -165,9 +165,9 @@ module Email when /\.mailjet\.com/ @message.header['X-MJ-CustomID'] = @message.message_id when "smtp.mandrillapp.com" - merge_json_x_header('X-MC-Metadata', { message_id: @message.message_id }) + merge_json_x_header('X-MC-Metadata', message_id: @message.message_id) when "smtp.sparkpostmail.com" - merge_json_x_header('X-MSYS-API', { metadata: { message_id: @message.message_id } }) + merge_json_x_header('X-MSYS-API', metadata: { message_id: @message.message_id }) end # Suppress images from short emails diff --git a/lib/email/styles.rb b/lib/email/styles.rb index 4b51a02331..09a7273136 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -10,7 +10,7 @@ module Email delegate :css, to: :fragment - def initialize(html, opts=nil) + def initialize(html, opts = nil) @html = html @opts = opts || {} @fragment = Nokogiri::HTML.fragment(@html) @@ -41,7 +41,7 @@ module Email img['width'] = img['height'] = 20 else # use dimensions of original iPhone screen for 'too big, let device rescale' - if img['width'].to_i > 320 or img['height'].to_i > 480 + if img['width'].to_i > (320) || img['height'].to_i > (480) img['width'] = img['height'] = 'auto' end end @@ -246,7 +246,7 @@ module Email linknum = 0 element.css('a').each do |inner| # we want the first footer link to be specially highlighted as IMPORTANT - if footernum == 0 and linknum == 0 + if footernum == (0) && linknum == (0) inner['style'] = "background-color: #006699; color:#ffffff; border-top: 4px solid #006699; border-right: 6px solid #006699; border-bottom: 4px solid #006699; border-left: 6px solid #006699; display: inline-block;" else inner['style'] = "color:#666;" @@ -271,7 +271,7 @@ module Email def style(selector, style, attribs = {}) @fragment.css(selector).each do |element| add_styles(element, style) if style - attribs.each do |k,v| + attribs.each do |k, v| element[k] = v end end diff --git a/lib/email_updater.rb b/lib/email_updater.rb index ab4c8246a2..12a2878194 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -7,12 +7,12 @@ class EmailUpdater attr_reader :user - def initialize(guardian=nil, user=nil) + def initialize(guardian = nil, user = nil) @guardian = guardian @user = user end - def self.human_attribute_name(name, options={}) + def self.human_attribute_name(name, options = {}) User.human_attribute_name(name, options) end @@ -68,8 +68,8 @@ class EmailUpdater @user = token.user change_req = user.email_change_requests - .where('old_email_token_id = :token_id OR new_email_token_id = :token_id', { token_id: token.id}) - .first + .where('old_email_token_id = :token_id OR new_email_token_id = :token_id', token_id: token.id) + .first # Simple state machine case change_req.try(:change_state) @@ -81,7 +81,7 @@ class EmailUpdater confirm_result = :authorizing_new when EmailChangeRequest.states[:authorizing_new] change_req.update_column(:change_state, EmailChangeRequest.states[:complete]) - user.update_column(:email, token.email) + user.primary_email.update_column(:email, token.email) confirm_result = :complete end else diff --git a/lib/emoji/db.json b/lib/emoji/db.json index 4036e94a08..b5835bf3af 100644 --- a/lib/emoji/db.json +++ b/lib/emoji/db.json @@ -829,7 +829,7 @@ "name": "bowing_man" }, { - "code": "1f926", + "code": "1f926-200d-2642-fe0f", "name": "man_facepalming" }, { @@ -6142,7 +6142,23 @@ "business_suit_levitating", "woman_juggling", "man_juggling", - "sleeping_bed" + "sleeping_bed", + "child", + "adult", + "older_adult", + "woman_with_headscarf", + "bearded_person", + "breast_feeding", + "mage", + "fairy", + "vampire", + "merperson", + "elf", + "person_in_steamy_room", + "person_climbing", + "person_in_lotus_position", + "love_you_gesture", + "palms_up_together" ], "aliases": { "right_anger_bubble": [ diff --git a/lib/emoji/groups.json b/lib/emoji/groups.json new file mode 100644 index 0000000000..5b313dfc11 --- /dev/null +++ b/lib/emoji/groups.json @@ -0,0 +1,6047 @@ +[ + { + "name": "people", + "fullname": "People", + "tabicon": "grinning", + "icons": [ + { + "name": "grinning", + "diversity": false + }, + { + "name": "grin", + "diversity": false + }, + { + "name": "joy", + "diversity": false + }, + { + "name": "rofl", + "diversity": false + }, + { + "name": "smiley", + "diversity": false + }, + { + "name": "smile", + "diversity": false + }, + { + "name": "sweat_smile", + "diversity": false + }, + { + "name": "laughing", + "diversity": false + }, + { + "name": "wink", + "diversity": false + }, + { + "name": "blush", + "diversity": false + }, + { + "name": "yum", + "diversity": false + }, + { + "name": "sunglasses", + "diversity": false + }, + { + "name": "heart_eyes", + "diversity": false + }, + { + "name": "kissing_heart", + "diversity": false + }, + { + "name": "kissing", + "diversity": false + }, + { + "name": "kissing_smiling_eyes", + "diversity": false + }, + { + "name": "kissing_closed_eyes", + "diversity": false + }, + { + "name": "relaxed", + "diversity": false + }, + { + "name": "slightly_smiling_face", + "diversity": false + }, + { + "name": "hugs", + "diversity": false + }, + { + "name": "star_struck", + "diversity": false + }, + { + "name": "thinking", + "diversity": false + }, + { + "name": "face_with_raised_eyebrow", + "diversity": false + }, + { + "name": "neutral_face", + "diversity": false + }, + { + "name": "expressionless", + "diversity": false + }, + { + "name": "no_mouth", + "diversity": false + }, + { + "name": "roll_eyes", + "diversity": false + }, + { + "name": "smirk", + "diversity": false + }, + { + "name": "persevere", + "diversity": false + }, + { + "name": "disappointed_relieved", + "diversity": false + }, + { + "name": "open_mouth", + "diversity": false + }, + { + "name": "zipper_mouth_face", + "diversity": false + }, + { + "name": "hushed", + "diversity": false + }, + { + "name": "sleepy", + "diversity": false + }, + { + "name": "tired_face", + "diversity": false + }, + { + "name": "sleeping", + "diversity": false + }, + { + "name": "relieved", + "diversity": false + }, + { + "name": "stuck_out_tongue", + "diversity": false + }, + { + "name": "stuck_out_tongue_winking_eye", + "diversity": false + }, + { + "name": "stuck_out_tongue_closed_eyes", + "diversity": false + }, + { + "name": "drooling_face", + "diversity": false + }, + { + "name": "unamused", + "diversity": false + }, + { + "name": "sweat", + "diversity": false + }, + { + "name": "pensive", + "diversity": false + }, + { + "name": "confused", + "diversity": false + }, + { + "name": "upside_down_face", + "diversity": false + }, + { + "name": "money_mouth_face", + "diversity": false + }, + { + "name": "astonished", + "diversity": false + }, + { + "name": "frowning_face", + "diversity": false + }, + { + "name": "slightly_frowning_face", + "diversity": false + }, + { + "name": "confounded", + "diversity": false + }, + { + "name": "disappointed", + "diversity": false + }, + { + "name": "worried", + "diversity": false + }, + { + "name": "triumph", + "diversity": false + }, + { + "name": "cry", + "diversity": false + }, + { + "name": "sob", + "diversity": false + }, + { + "name": "frowning", + "diversity": false + }, + { + "name": "anguished", + "diversity": false + }, + { + "name": "fearful", + "diversity": false + }, + { + "name": "weary", + "diversity": false + }, + { + "name": "exploding_head", + "diversity": false + }, + { + "name": "grimacing", + "diversity": false + }, + { + "name": "cold_sweat", + "diversity": false + }, + { + "name": "scream", + "diversity": false + }, + { + "name": "flushed", + "diversity": false + }, + { + "name": "crazy_face", + "diversity": false + }, + { + "name": "dizzy_face", + "diversity": false + }, + { + "name": "rage", + "diversity": false + }, + { + "name": "angry", + "diversity": false + }, + { + "name": "face_with_symbols_over_mouth", + "diversity": false + }, + { + "name": "mask", + "diversity": false + }, + { + "name": "face_with_thermometer", + "diversity": false + }, + { + "name": "face_with_head_bandage", + "diversity": false + }, + { + "name": "nauseated_face", + "diversity": false + }, + { + "name": "face_vomiting", + "diversity": false + }, + { + "name": "sneezing_face", + "diversity": false + }, + { + "name": "innocent", + "diversity": false + }, + { + "name": "cowboy_hat_face", + "diversity": false + }, + { + "name": "clown_face", + "diversity": false + }, + { + "name": "lying_face", + "diversity": false + }, + { + "name": "sushing_face", + "diversity": false + }, + { + "name": "face_with_hand_over_mouth", + "diversity": false + }, + { + "name": "face_with_monocle", + "diversity": false + }, + { + "name": "nerd_face", + "diversity": false + }, + { + "name": "smiling_imp", + "diversity": false + }, + { + "name": "imp", + "diversity": false + }, + { + "name": "japanese_ogre", + "diversity": false + }, + { + "name": "japanese_goblin", + "diversity": false + }, + { + "name": "skull", + "diversity": false + }, + { + "name": "skull_and_crossbones", + "diversity": false + }, + { + "name": "ghost", + "diversity": false + }, + { + "name": "alien", + "diversity": false + }, + { + "name": "space_invader", + "diversity": false + }, + { + "name": "robot", + "diversity": false + }, + { + "name": "poop", + "diversity": false + }, + { + "name": "smiley_cat", + "diversity": false + }, + { + "name": "smile_cat", + "diversity": false + }, + { + "name": "joy_cat", + "diversity": false + }, + { + "name": "heart_eyes_cat", + "diversity": false + }, + { + "name": "smirk_cat", + "diversity": false + }, + { + "name": "kissing_cat", + "diversity": false + }, + { + "name": "scream_cat", + "diversity": false + }, + { + "name": "crying_cat_face", + "diversity": false + }, + { + "name": "pouting_cat", + "diversity": false + }, + { + "name": "see_no_evil", + "diversity": false + }, + { + "name": "hear_no_evil", + "diversity": false + }, + { + "name": "speak_no_evil", + "diversity": false + }, + { + "name": "baby", + "diversity": true + }, + { + "name": "child", + "diversity": true + }, + { + "name": "boy", + "diversity": true + }, + { + "name": "girl", + "diversity": true + }, + { + "name": "adult", + "diversity": true + }, + { + "name": "man", + "diversity": true + }, + { + "name": "woman", + "diversity": true + }, + { + "name": "older_adult", + "diversity": true + }, + { + "name": "older_man", + "diversity": true + }, + { + "name": "older_woman", + "diversity": true + }, + { + "name": "man_health_worker", + "diversity": true + }, + { + "name": "woman_health_worker", + "diversity": true + }, + { + "name": "man_student", + "diversity": true + }, + { + "name": "woman_student", + "diversity": true + }, + { + "name": "man_teacher", + "diversity": true + }, + { + "name": "woman_teacher", + "diversity": true + }, + { + "name": "man_judge", + "diversity": true + }, + { + "name": "woman_judge", + "diversity": true + }, + { + "name": "man_farmer", + "diversity": true + }, + { + "name": "woman_farmer", + "diversity": true + }, + { + "name": "man_cook", + "diversity": true + }, + { + "name": "woman_cook", + "diversity": true + }, + { + "name": "man_mechanic", + "diversity": true + }, + { + "name": "woman_mechanic", + "diversity": true + }, + { + "name": "man_factory_worker", + "diversity": true + }, + { + "name": "woman_factory_worker", + "diversity": true + }, + { + "name": "man_office_worker", + "diversity": true + }, + { + "name": "woman_office_worker", + "diversity": true + }, + { + "name": "man_scientist", + "diversity": true + }, + { + "name": "woman_scientist", + "diversity": true + }, + { + "name": "man_technologist", + "diversity": true + }, + { + "name": "woman_technologist", + "diversity": true + }, + { + "name": "man_singer", + "diversity": true + }, + { + "name": "woman_singer", + "diversity": true + }, + { + "name": "man_artist", + "diversity": true + }, + { + "name": "woman_artist", + "diversity": true + }, + { + "name": "man_pilot", + "diversity": true + }, + { + "name": "woman_pilot", + "diversity": true + }, + { + "name": "man_astronaut", + "diversity": true + }, + { + "name": "woman_astronaut", + "diversity": true + }, + { + "name": "man_firefighter", + "diversity": true + }, + { + "name": "woman_firefighter", + "diversity": true + }, + { + "name": "policeman", + "diversity": true + }, + { + "name": "policewoman", + "diversity": true + }, + { + "name": "male_detective", + "diversity": true + }, + { + "name": "female_detective", + "diversity": false + }, + { + "name": "guardsman", + "diversity": true + }, + { + "name": "guardswoman", + "diversity": true + }, + { + "name": "construction_worker_man", + "diversity": true + }, + { + "name": "construction_worker_woman", + "diversity": true + }, + { + "name": "prince", + "diversity": true + }, + { + "name": "princess", + "diversity": true + }, + { + "name": "man_with_turban", + "diversity": true + }, + { + "name": "woman_with_turban", + "diversity": true + }, + { + "name": "man_with_gua_pi_mao", + "diversity": true + }, + { + "name": "woman_with_headscarf", + "diversity": true + }, + { + "name": "bearded_person", + "diversity": true + }, + { + "name": "blonde_man", + "diversity": true + }, + { + "name": "blonde_woman", + "diversity": true + }, + { + "name": "man_in_tuxedo", + "diversity": true + }, + { + "name": "bride_with_veil", + "diversity": true + }, + { + "name": "pregnant_woman", + "diversity": true + }, + { + "name": "breast_feeding", + "diversity": true + }, + { + "name": "angel", + "diversity": true + }, + { + "name": "santa", + "diversity": true + }, + { + "name": "mrs_claus", + "diversity": true + }, + { + "name": "mage", + "diversity": true + }, + { + "name": "fairy", + "diversity": true + }, + { + "name": "vampire", + "diversity": true + }, + { + "name": "merperson", + "diversity": true + }, + { + "name": "elf", + "diversity": true + }, + { + "name": "genie", + "diversity": false + }, + { + "name": "zombie", + "diversity": false + }, + { + "name": "frowning_woman", + "diversity": true + }, + { + "name": "frowning_man", + "diversity": true + }, + { + "name": "pouting_woman", + "diversity": true + }, + { + "name": "pouting_man", + "diversity": true + }, + { + "name": "no_good_woman", + "diversity": true + }, + { + "name": "no_good_man", + "diversity": true + }, + { + "name": "ok_woman", + "diversity": true + }, + { + "name": "ok_man", + "diversity": true + }, + { + "name": "tipping_hand_woman", + "diversity": true + }, + { + "name": "tipping_hand_man", + "diversity": true + }, + { + "name": "raising_hand_woman", + "diversity": true + }, + { + "name": "raising_hand_man", + "diversity": true + }, + { + "name": "bowing_man", + "diversity": true + }, + { + "name": "bowing_woman", + "diversity": true + }, + { + "name": "man_facepalming", + "diversity": true + }, + { + "name": "woman_facepalming", + "diversity": true + }, + { + "name": "man_shrugging", + "diversity": true + }, + { + "name": "woman_shrugging", + "diversity": true + }, + { + "name": "couple", + "diversity": false + }, + { + "name": "two_men_holding_hands", + "diversity": false + }, + { + "name": "two_women_holding_hands", + "diversity": false + }, + { + "name": "couplekiss_man_woman", + "diversity": false + }, + { + "name": "couplekiss_man_man", + "diversity": false + }, + { + "name": "couplekiss_woman_woman", + "diversity": false + }, + { + "name": "couple_with_heart_woman_man", + "diversity": false + }, + { + "name": "couple_with_heart_man_man", + "diversity": false + }, + { + "name": "couple_with_heart_woman_woman", + "diversity": false + }, + { + "name": "family_man_woman_boy", + "diversity": false + }, + { + "name": "family_man_woman_girl", + "diversity": false + }, + { + "name": "family_man_woman_girl_boy", + "diversity": false + }, + { + "name": "family_man_woman_boy_boy", + "diversity": false + }, + { + "name": "family_man_woman_girl_girl", + "diversity": false + }, + { + "name": "family_man_man_boy", + "diversity": false + }, + { + "name": "family_man_man_girl", + "diversity": false + }, + { + "name": "family_man_man_girl_boy", + "diversity": false + }, + { + "name": "family_man_man_boy_boy", + "diversity": false + }, + { + "name": "family_man_man_girl_girl", + "diversity": false + }, + { + "name": "family_woman_woman_boy", + "diversity": false + }, + { + "name": "family_woman_woman_girl", + "diversity": false + }, + { + "name": "family_woman_woman_girl_boy", + "diversity": false + }, + { + "name": "family_woman_woman_boy_boy", + "diversity": false + }, + { + "name": "family_woman_woman_girl_girl", + "diversity": false + }, + { + "name": "family_man_boy", + "diversity": false + }, + { + "name": "family_man_boy_boy", + "diversity": false + }, + { + "name": "family_man_girl", + "diversity": false + }, + { + "name": "family_man_girl_boy", + "diversity": false + }, + { + "name": "family_man_girl_girl", + "diversity": false + }, + { + "name": "family_woman_boy", + "diversity": false + }, + { + "name": "family_woman_boy_boy", + "diversity": false + }, + { + "name": "family_woman_girl", + "diversity": false + }, + { + "name": "family_woman_girl_boy", + "diversity": false + }, + { + "name": "family_woman_girl_girl", + "diversity": false + }, + { + "name": "selfie", + "diversity": true + }, + { + "name": "muscle", + "diversity": true + }, + { + "name": "point_left", + "diversity": true + }, + { + "name": "point_right", + "diversity": true + }, + { + "name": "point_up", + "diversity": true + }, + { + "name": "point_up_2", + "diversity": true + }, + { + "name": "fu", + "diversity": true + }, + { + "name": "point_down", + "diversity": true + }, + { + "name": "v", + "diversity": true + }, + { + "name": "crossed_fingers", + "diversity": true + }, + { + "name": "vulcan_salute", + "diversity": true + }, + { + "name": "metal", + "diversity": true + }, + { + "name": "call_me_hand", + "diversity": true + }, + { + "name": "raised_hand_with_fingers_splayed", + "diversity": true + }, + { + "name": "raised_hand", + "diversity": true + }, + { + "name": "ok_hand", + "diversity": true + }, + { + "name": "+1", + "diversity": true + }, + { + "name": "-1", + "diversity": true + }, + { + "name": "fist", + "diversity": true + }, + { + "name": "facepunch", + "diversity": true + }, + { + "name": "fist_left", + "diversity": true + }, + { + "name": "fist_right", + "diversity": true + }, + { + "name": "raised_back_of_hand", + "diversity": true + }, + { + "name": "wave", + "diversity": true + }, + { + "name": "love_you_gesture", + "diversity": true + }, + { + "name": "writing_hand", + "diversity": true + }, + { + "name": "clap", + "diversity": true + }, + { + "name": "open_hands", + "diversity": true + }, + { + "name": "raised_hands", + "diversity": true + }, + { + "name": "palms_up_together", + "diversity": true + }, + { + "name": "pray", + "diversity": true + }, + { + "name": "handshake", + "diversity": false + }, + { + "name": "nail_care", + "diversity": true + }, + { + "name": "ear", + "diversity": true + }, + { + "name": "nose", + "diversity": true + }, + { + "name": "footprints", + "diversity": false + }, + { + "name": "eyes", + "diversity": false + }, + { + "name": "eye", + "diversity": false + }, + { + "name": "brain", + "diversity": false + }, + { + "name": "tongue", + "diversity": false + }, + { + "name": "lips", + "diversity": false + } + ] + }, + { + "name": "nature", + "fullname": "Nature", + "tabicon": "evergreen_tree", + "icons": [ + { + "name": "monkey_face", + "diversity": false + }, + { + "name": "monkey", + "diversity": false + }, + { + "name": "gorilla", + "diversity": false + }, + { + "name": "dog", + "diversity": false + }, + { + "name": "dog2", + "diversity": false + }, + { + "name": "poodle", + "diversity": false + }, + { + "name": "wolf", + "diversity": false + }, + { + "name": "fox_face", + "diversity": false + }, + { + "name": "cat", + "diversity": false + }, + { + "name": "cat2", + "diversity": false + }, + { + "name": "lion", + "diversity": false + }, + { + "name": "tiger", + "diversity": false + }, + { + "name": "tiger2", + "diversity": false + }, + { + "name": "leopard", + "diversity": false + }, + { + "name": "horse", + "diversity": false + }, + { + "name": "racehorse", + "diversity": false + }, + { + "name": "unicorn", + "diversity": false + }, + { + "name": "zebra", + "diversity": false + }, + { + "name": "deer", + "diversity": false + }, + { + "name": "cow", + "diversity": false + }, + { + "name": "ox", + "diversity": false + }, + { + "name": "water_buffalo", + "diversity": false + }, + { + "name": "cow2", + "diversity": false + }, + { + "name": "pig", + "diversity": false + }, + { + "name": "pig2", + "diversity": false + }, + { + "name": "boar", + "diversity": false + }, + { + "name": "pig_nose", + "diversity": false + }, + { + "name": "ram", + "diversity": false + }, + { + "name": "sheep", + "diversity": false + }, + { + "name": "goat", + "diversity": false + }, + { + "name": "dromedary_camel", + "diversity": false + }, + { + "name": "camel", + "diversity": false + }, + { + "name": "giraffe", + "diversity": false + }, + { + "name": "elephant", + "diversity": false + }, + { + "name": "rhinoceros", + "diversity": false + }, + { + "name": "mouse", + "diversity": false + }, + { + "name": "mouse2", + "diversity": false + }, + { + "name": "rat", + "diversity": false + }, + { + "name": "hamster", + "diversity": false + }, + { + "name": "rabbit", + "diversity": false + }, + { + "name": "rabbit2", + "diversity": false + }, + { + "name": "chipmunk", + "diversity": false + }, + { + "name": "hedgehog", + "diversity": false + }, + { + "name": "bat", + "diversity": false + }, + { + "name": "bear", + "diversity": false + }, + { + "name": "koala", + "diversity": false + }, + { + "name": "panda_face", + "diversity": false + }, + { + "name": "paw_prints", + "diversity": false + }, + { + "name": "turkey", + "diversity": false + }, + { + "name": "chicken", + "diversity": false + }, + { + "name": "rooster", + "diversity": false + }, + { + "name": "hatching_chick", + "diversity": false + }, + { + "name": "baby_chick", + "diversity": false + }, + { + "name": "hatched_chick", + "diversity": false + }, + { + "name": "bird", + "diversity": false + }, + { + "name": "penguin", + "diversity": false + }, + { + "name": "dove", + "diversity": false + }, + { + "name": "eagle", + "diversity": false + }, + { + "name": "duck", + "diversity": false + }, + { + "name": "owl", + "diversity": false + }, + { + "name": "frog", + "diversity": false + }, + { + "name": "crocodile", + "diversity": false + }, + { + "name": "turtle", + "diversity": false + }, + { + "name": "lizard", + "diversity": false + }, + { + "name": "snake", + "diversity": false + }, + { + "name": "dragon_face", + "diversity": false + }, + { + "name": "dragon", + "diversity": false + }, + { + "name": "sauropod", + "diversity": false + }, + { + "name": "t_rex", + "diversity": false + }, + { + "name": "whale", + "diversity": false + }, + { + "name": "whale2", + "diversity": false + }, + { + "name": "dolphin", + "diversity": false + }, + { + "name": "fish", + "diversity": false + }, + { + "name": "tropical_fish", + "diversity": false + }, + { + "name": "blowfish", + "diversity": false + }, + { + "name": "shark", + "diversity": false + }, + { + "name": "octopus", + "diversity": false + }, + { + "name": "shell", + "diversity": false + }, + { + "name": "crab", + "diversity": false + }, + { + "name": "shrimp", + "diversity": false + }, + { + "name": "squid", + "diversity": false + }, + { + "name": "snail", + "diversity": false + }, + { + "name": "butterfly", + "diversity": false + }, + { + "name": "bug", + "diversity": false + }, + { + "name": "ant", + "diversity": false + }, + { + "name": "honeybee", + "diversity": false + }, + { + "name": "beetle", + "diversity": false + }, + { + "name": "cricket", + "diversity": false + }, + { + "name": "spider", + "diversity": false + }, + { + "name": "spider_web", + "diversity": false + }, + { + "name": "scorpion", + "diversity": false + }, + { + "name": "bouquet", + "diversity": false + }, + { + "name": "cherry_blossom", + "diversity": false + }, + { + "name": "white_flower", + "diversity": false + }, + { + "name": "rosette", + "diversity": false + }, + { + "name": "rose", + "diversity": false + }, + { + "name": "wilted_flower", + "diversity": false + }, + { + "name": "hibiscus", + "diversity": false + }, + { + "name": "sunflower", + "diversity": false + }, + { + "name": "blossom", + "diversity": false + }, + { + "name": "tulip", + "diversity": false + }, + { + "name": "seedling", + "diversity": false + }, + { + "name": "evergreen_tree", + "diversity": false + }, + { + "name": "deciduous_tree", + "diversity": false + }, + { + "name": "palm_tree", + "diversity": false + }, + { + "name": "cactus", + "diversity": false + }, + { + "name": "ear_of_rice", + "diversity": false + }, + { + "name": "herb", + "diversity": false + }, + { + "name": "shamrock", + "diversity": false + }, + { + "name": "four_leaf_clover", + "diversity": false + }, + { + "name": "maple_leaf", + "diversity": false + }, + { + "name": "fallen_leaf", + "diversity": false + }, + { + "name": "leaves", + "diversity": false + }, + { + "name": "new_moon", + "diversity": false + }, + { + "name": "waxing_crescent_moon", + "diversity": false + }, + { + "name": "first_quarter_moon", + "diversity": false + }, + { + "name": "waxing_gibbous_moon", + "diversity": false + }, + { + "name": "full_moon", + "diversity": false + }, + { + "name": "waning_gibbous_moon", + "diversity": false + }, + { + "name": "last_quarter_moon", + "diversity": false + }, + { + "name": "waning_crescent_moon", + "diversity": false + }, + { + "name": "crescent_moon", + "diversity": false + }, + { + "name": "new_moon_with_face", + "diversity": false + }, + { + "name": "first_quarter_moon_with_face", + "diversity": false + }, + { + "name": "last_quarter_moon_with_face", + "diversity": false + }, + { + "name": "thermometer", + "diversity": false + }, + { + "name": "sunny", + "diversity": false + }, + { + "name": "full_moon_with_face", + "diversity": false + }, + { + "name": "sun_with_face", + "diversity": false + }, + { + "name": "star", + "diversity": false + }, + { + "name": "star2", + "diversity": false + }, + { + "name": "stars", + "diversity": false + }, + { + "name": "cloud", + "diversity": false + }, + { + "name": "partly_sunny", + "diversity": false + }, + { + "name": "cloud_with_lightning_and_rain", + "diversity": false + }, + { + "name": "sun_behind_small_cloud", + "diversity": false + }, + { + "name": "sun_behind_large_cloud", + "diversity": false + }, + { + "name": "sun_behind_rain_cloud", + "diversity": false + }, + { + "name": "cloud_with_rain", + "diversity": false + }, + { + "name": "cloud_with_snow", + "diversity": false + }, + { + "name": "cloud_with_lightning", + "diversity": false + }, + { + "name": "tornado", + "diversity": false + }, + { + "name": "fog", + "diversity": false + }, + { + "name": "wind_face", + "diversity": false + }, + { + "name": "cyclone", + "diversity": false + }, + { + "name": "rainbow", + "diversity": false + }, + { + "name": "closed_umbrella", + "diversity": false + }, + { + "name": "open_umbrella", + "diversity": false + }, + { + "name": "umbrella", + "diversity": false + }, + { + "name": "parasol_on_ground", + "diversity": false + }, + { + "name": "zap", + "diversity": false + }, + { + "name": "snowflake", + "diversity": false + }, + { + "name": "snowman_with_snow", + "diversity": false + }, + { + "name": "snowman", + "diversity": false + }, + { + "name": "comet", + "diversity": false + }, + { + "name": "fire", + "diversity": false + }, + { + "name": "droplet", + "diversity": false + }, + { + "name": "ocean", + "diversity": false + } + ] + }, + { + "name": "food", + "fullname": "Food & Drink", + "tabicon": "hamburger", + "icons": [ + { + "name": "grapes", + "diversity": false + }, + { + "name": "melon", + "diversity": false + }, + { + "name": "watermelon", + "diversity": false + }, + { + "name": "tangerine", + "diversity": false + }, + { + "name": "lemon", + "diversity": false + }, + { + "name": "banana", + "diversity": false + }, + { + "name": "pineapple", + "diversity": false + }, + { + "name": "apple", + "diversity": false + }, + { + "name": "green_apple", + "diversity": false + }, + { + "name": "pear", + "diversity": false + }, + { + "name": "peach", + "diversity": false + }, + { + "name": "cherries", + "diversity": false + }, + { + "name": "strawberry", + "diversity": false + }, + { + "name": "kiwi_fruit", + "diversity": false + }, + { + "name": "tomato", + "diversity": false + }, + { + "name": "coconut", + "diversity": false + }, + { + "name": "avocado", + "diversity": false + }, + { + "name": "eggplant", + "diversity": false + }, + { + "name": "potato", + "diversity": false + }, + { + "name": "carrot", + "diversity": false + }, + { + "name": "corn", + "diversity": false + }, + { + "name": "hot_pepper", + "diversity": false + }, + { + "name": "cucumber", + "diversity": false + }, + { + "name": "broccoli", + "diversity": false + }, + { + "name": "mushroom", + "diversity": false + }, + { + "name": "peanuts", + "diversity": false + }, + { + "name": "chestnut", + "diversity": false + }, + { + "name": "bread", + "diversity": false + }, + { + "name": "croissant", + "diversity": false + }, + { + "name": "baguette_bread", + "diversity": false + }, + { + "name": "pretzel", + "diversity": false + }, + { + "name": "pancakes", + "diversity": false + }, + { + "name": "cheese", + "diversity": false + }, + { + "name": "meat_on_bone", + "diversity": false + }, + { + "name": "poultry_leg", + "diversity": false + }, + { + "name": "cut_of_meat", + "diversity": false + }, + { + "name": "bacon", + "diversity": false + }, + { + "name": "hamburger", + "diversity": false + }, + { + "name": "fries", + "diversity": false + }, + { + "name": "pizza", + "diversity": false + }, + { + "name": "hotdog", + "diversity": false + }, + { + "name": "sandwich", + "diversity": false + }, + { + "name": "taco", + "diversity": false + }, + { + "name": "burrito", + "diversity": false + }, + { + "name": "stuffed_flatbread", + "diversity": false + }, + { + "name": "egg", + "diversity": false + }, + { + "name": "fried_egg", + "diversity": false + }, + { + "name": "shallow_pan_of_food", + "diversity": false + }, + { + "name": "stew", + "diversity": false + }, + { + "name": "bowl_with_spoon", + "diversity": false + }, + { + "name": "green_salad", + "diversity": false + }, + { + "name": "popcorn", + "diversity": false + }, + { + "name": "canned_food", + "diversity": false + }, + { + "name": "bento", + "diversity": false + }, + { + "name": "rice_cracker", + "diversity": false + }, + { + "name": "rice_ball", + "diversity": false + }, + { + "name": "rice", + "diversity": false + }, + { + "name": "curry", + "diversity": false + }, + { + "name": "ramen", + "diversity": false + }, + { + "name": "spaghetti", + "diversity": false + }, + { + "name": "sweet_potato", + "diversity": false + }, + { + "name": "oden", + "diversity": false + }, + { + "name": "sushi", + "diversity": false + }, + { + "name": "fried_shrimp", + "diversity": false + }, + { + "name": "fish_cake", + "diversity": false + }, + { + "name": "dango", + "diversity": false + }, + { + "name": "dumpling", + "diversity": false + }, + { + "name": "fortune_cookie", + "diversity": false + }, + { + "name": "takeout_box", + "diversity": false + }, + { + "name": "icecream", + "diversity": false + }, + { + "name": "shaved_ice", + "diversity": false + }, + { + "name": "ice_cream", + "diversity": false + }, + { + "name": "doughnut", + "diversity": false + }, + { + "name": "cookie", + "diversity": false + }, + { + "name": "birthday", + "diversity": false + }, + { + "name": "cake", + "diversity": false + }, + { + "name": "pie", + "diversity": false + }, + { + "name": "chocolate_bar", + "diversity": false + }, + { + "name": "candy", + "diversity": false + }, + { + "name": "lollipop", + "diversity": false + }, + { + "name": "custard", + "diversity": false + }, + { + "name": "honey_pot", + "diversity": false + }, + { + "name": "baby_bottle", + "diversity": false + }, + { + "name": "milk_glass", + "diversity": false + }, + { + "name": "coffee", + "diversity": false + }, + { + "name": "tea", + "diversity": false + }, + { + "name": "sake", + "diversity": false + }, + { + "name": "champagne", + "diversity": false + }, + { + "name": "wine_glass", + "diversity": false + }, + { + "name": "cocktail", + "diversity": false + }, + { + "name": "tropical_drink", + "diversity": false + }, + { + "name": "beer", + "diversity": false + }, + { + "name": "beers", + "diversity": false + }, + { + "name": "clinking_glasses", + "diversity": false + }, + { + "name": "tumbler_glass", + "diversity": false + }, + { + "name": "cup_with_straw", + "diversity": false + }, + { + "name": "chopsticks", + "diversity": false + }, + { + "name": "plate_with_cutlery", + "diversity": false + }, + { + "name": "fork_and_knife", + "diversity": false + }, + { + "name": "spoon", + "diversity": false + }, + { + "name": "hocho", + "diversity": false + }, + { + "name": "amphora", + "diversity": false + } + ] + }, + { + "name": "celebration", + "fullname": "Celebration", + "tabicon": "gift", + "icons": [ + { + "name": "jack_o_lantern", + "diversity": false + }, + { + "name": "christmas_tree", + "diversity": false + }, + { + "name": "fireworks", + "diversity": false + }, + { + "name": "sparkler", + "diversity": false + }, + { + "name": "sparkles", + "diversity": false + }, + { + "name": "balloon", + "diversity": false + }, + { + "name": "tada", + "diversity": false + }, + { + "name": "confetti_ball", + "diversity": false + }, + { + "name": "tanabata_tree", + "diversity": false + }, + { + "name": "bamboo", + "diversity": false + }, + { + "name": "dolls", + "diversity": false + }, + { + "name": "flags", + "diversity": false + }, + { + "name": "wind_chime", + "diversity": false + }, + { + "name": "rice_scene", + "diversity": false + }, + { + "name": "ribbon", + "diversity": false + }, + { + "name": "gift", + "diversity": false + }, + { + "name": "reminder_ribbon", + "diversity": false + }, + { + "name": "tickets", + "diversity": false + }, + { + "name": "ticket", + "diversity": false + }, + { + "name": "kiss", + "diversity": false + }, + { + "name": "cupid", + "diversity": false + }, + { + "name": "heart", + "diversity": false + }, + { + "name": "heartbeat", + "diversity": false + }, + { + "name": "broken_heart", + "diversity": false + }, + { + "name": "two_hearts", + "diversity": false + }, + { + "name": "sparkling_heart", + "diversity": false + }, + { + "name": "heartpulse", + "diversity": false + }, + { + "name": "blue_heart", + "diversity": false + }, + { + "name": "green_heart", + "diversity": false + }, + { + "name": "yellow_heart", + "diversity": false + }, + { + "name": "orange_heart", + "diversity": false + }, + { + "name": "purple_heart", + "diversity": false + }, + { + "name": "black_heart", + "diversity": false + }, + { + "name": "gift_heart", + "diversity": false + }, + { + "name": "revolving_hearts", + "diversity": false + }, + { + "name": "heart_decoration", + "diversity": false + }, + { + "name": "heavy_heart_exclamation", + "diversity": false + }, + { + "name": "love_letter", + "diversity": false + }, + { + "name": "zzz", + "diversity": false + }, + { + "name": "anger", + "diversity": false + }, + { + "name": "bomb", + "diversity": false + }, + { + "name": "boom", + "diversity": false + }, + { + "name": "sweat_drops", + "diversity": false + }, + { + "name": "dash", + "diversity": false + }, + { + "name": "dizzy", + "diversity": false + }, + { + "name": "speech_balloon", + "diversity": false + }, + { + "name": "left_speech_bubble", + "diversity": false + }, + { + "name": "right_anger_bubble", + "diversity": false + }, + { + "name": "thought_balloon", + "diversity": false + }, + { + "name": "hole", + "diversity": false + } + ] + }, + { + "name": "activity", + "fullname": "Activities", + "tabicon": "soccer", + "icons": [ + { + "name": "massage_woman", + "diversity": true + }, + { + "name": "massage_man", + "diversity": true + }, + { + "name": "haircut_woman", + "diversity": true + }, + { + "name": "haircut_man", + "diversity": true + }, + { + "name": "walking_man", + "diversity": true + }, + { + "name": "walking_woman", + "diversity": true + }, + { + "name": "running_man", + "diversity": true + }, + { + "name": "running_woman", + "diversity": true + }, + { + "name": "dancer", + "diversity": true + }, + { + "name": "man_dancing", + "diversity": true + }, + { + "name": "dancing_women", + "diversity": false + }, + { + "name": "dancing_men", + "diversity": false + }, + { + "name": "person_in_steamy_room", + "diversity": true + }, + { + "name": "person_climbing", + "diversity": true + }, + { + "name": "person_in_lotus_position", + "diversity": true + }, + { + "name": "bath", + "diversity": true + }, + { + "name": "sleeping_bed", + "diversity": true + }, + { + "name": "business_suit_levitating", + "diversity": true + }, + { + "name": "speaking_head", + "diversity": false + }, + { + "name": "bust_in_silhouette", + "diversity": false + }, + { + "name": "busts_in_silhouette", + "diversity": false + }, + { + "name": "person_fencing", + "diversity": false + }, + { + "name": "horse_racing", + "diversity": true + }, + { + "name": "skier", + "diversity": false + }, + { + "name": "snowboarder", + "diversity": true + }, + { + "name": "golfing_man", + "diversity": true + }, + { + "name": "golfing_woman", + "diversity": false + }, + { + "name": "surfing_man", + "diversity": true + }, + { + "name": "surfing_woman", + "diversity": true + }, + { + "name": "rowing_man", + "diversity": true + }, + { + "name": "rowing_woman", + "diversity": true + }, + { + "name": "swimming_man", + "diversity": true + }, + { + "name": "swimming_woman", + "diversity": true + }, + { + "name": "basketball_man", + "diversity": true + }, + { + "name": "basketball_woman", + "diversity": false + }, + { + "name": "weight_lifting_man", + "diversity": true + }, + { + "name": "weight_lifting_woman", + "diversity": false + }, + { + "name": "biking_man", + "diversity": true + }, + { + "name": "biking_woman", + "diversity": true + }, + { + "name": "mountain_biking_man", + "diversity": true + }, + { + "name": "mountain_biking_woman", + "diversity": true + }, + { + "name": "racing_car", + "diversity": false + }, + { + "name": "motorcycle", + "diversity": false + }, + { + "name": "man_cartwheeling", + "diversity": true + }, + { + "name": "woman_cartwheeling", + "diversity": true + }, + { + "name": "men_wrestling", + "diversity": false + }, + { + "name": "women_wrestling", + "diversity": false + }, + { + "name": "man_playing_water_polo", + "diversity": true + }, + { + "name": "woman_playing_water_polo", + "diversity": true + }, + { + "name": "man_playing_handball", + "diversity": true + }, + { + "name": "woman_playing_handball", + "diversity": true + }, + { + "name": "man_juggling", + "diversity": true + }, + { + "name": "woman_juggling", + "diversity": true + }, + { + "name": "soccer", + "diversity": false + }, + { + "name": "baseball", + "diversity": false + }, + { + "name": "basketball", + "diversity": false + }, + { + "name": "volleyball", + "diversity": false + }, + { + "name": "football", + "diversity": false + }, + { + "name": "rugby_football", + "diversity": false + }, + { + "name": "tennis", + "diversity": false + }, + { + "name": "8ball", + "diversity": false + }, + { + "name": "bowling", + "diversity": false + }, + { + "name": "cricket_bat_and_ball", + "diversity": false + }, + { + "name": "field_hockey", + "diversity": false + }, + { + "name": "ice_hockey", + "diversity": false + }, + { + "name": "ping_pong", + "diversity": false + }, + { + "name": "badminton", + "diversity": false + }, + { + "name": "boxing_glove", + "diversity": false + }, + { + "name": "martial_arts_uniform", + "diversity": false + }, + { + "name": "goal_net", + "diversity": false + }, + { + "name": "dart", + "diversity": false + }, + { + "name": "golf", + "diversity": false + }, + { + "name": "ice_skate", + "diversity": false + }, + { + "name": "fishing_pole_and_fish", + "diversity": false + }, + { + "name": "running_shirt_with_sash", + "diversity": false + }, + { + "name": "ski", + "diversity": false + }, + { + "name": "sled", + "diversity": false + }, + { + "name": "curling_stone", + "diversity": false + }, + { + "name": "video_game", + "diversity": false + }, + { + "name": "joystick", + "diversity": false + }, + { + "name": "game_die", + "diversity": false + }, + { + "name": "spades", + "diversity": false + }, + { + "name": "hearts", + "diversity": false + }, + { + "name": "diamonds", + "diversity": false + }, + { + "name": "clubs", + "diversity": false + }, + { + "name": "black_joker", + "diversity": false + }, + { + "name": "mahjong", + "diversity": false + }, + { + "name": "flower_playing_cards", + "diversity": false + }, + { + "name": "musical_score", + "diversity": false + }, + { + "name": "musical_note", + "diversity": false + }, + { + "name": "notes", + "diversity": false + }, + { + "name": "studio_microphone", + "diversity": false + }, + { + "name": "level_slider", + "diversity": false + }, + { + "name": "control_knobs", + "diversity": false + }, + { + "name": "microphone", + "diversity": false + }, + { + "name": "headphones", + "diversity": false + }, + { + "name": "radio", + "diversity": false + }, + { + "name": "saxophone", + "diversity": false + }, + { + "name": "guitar", + "diversity": false + }, + { + "name": "musical_keyboard", + "diversity": false + }, + { + "name": "trumpet", + "diversity": false + }, + { + "name": "violin", + "diversity": false + }, + { + "name": "drum", + "diversity": false + } + ] + }, + { + "name": "travel", + "fullname": "Travel & Places", + "tabicon": "airplane", + "icons": [ + { + "name": "earth_africa", + "diversity": false + }, + { + "name": "earth_americas", + "diversity": false + }, + { + "name": "earth_asia", + "diversity": false + }, + { + "name": "globe_with_meridians", + "diversity": false + }, + { + "name": "world_map", + "diversity": false + }, + { + "name": "japan", + "diversity": false + }, + { + "name": "mountain_snow", + "diversity": false + }, + { + "name": "mountain", + "diversity": false + }, + { + "name": "volcano", + "diversity": false + }, + { + "name": "mount_fuji", + "diversity": false + }, + { + "name": "camping", + "diversity": false + }, + { + "name": "beach_umbrella", + "diversity": false + }, + { + "name": "desert", + "diversity": false + }, + { + "name": "desert_island", + "diversity": false + }, + { + "name": "national_park", + "diversity": false + }, + { + "name": "stadium", + "diversity": false + }, + { + "name": "classical_building", + "diversity": false + }, + { + "name": "building_construction", + "diversity": false + }, + { + "name": "houses", + "diversity": false + }, + { + "name": "cityscape", + "diversity": false + }, + { + "name": "derelict_house", + "diversity": false + }, + { + "name": "house", + "diversity": false + }, + { + "name": "house_with_garden", + "diversity": false + }, + { + "name": "office", + "diversity": false + }, + { + "name": "post_office", + "diversity": false + }, + { + "name": "european_post_office", + "diversity": false + }, + { + "name": "hospital", + "diversity": false + }, + { + "name": "bank", + "diversity": false + }, + { + "name": "hotel", + "diversity": false + }, + { + "name": "love_hotel", + "diversity": false + }, + { + "name": "convenience_store", + "diversity": false + }, + { + "name": "school", + "diversity": false + }, + { + "name": "department_store", + "diversity": false + }, + { + "name": "factory", + "diversity": false + }, + { + "name": "japanese_castle", + "diversity": false + }, + { + "name": "european_castle", + "diversity": false + }, + { + "name": "wedding", + "diversity": false + }, + { + "name": "tokyo_tower", + "diversity": false + }, + { + "name": "statue_of_liberty", + "diversity": false + }, + { + "name": "church", + "diversity": false + }, + { + "name": "mosque", + "diversity": false + }, + { + "name": "synagogue", + "diversity": false + }, + { + "name": "shinto_shrine", + "diversity": false + }, + { + "name": "kaaba", + "diversity": false + }, + { + "name": "fountain", + "diversity": false + }, + { + "name": "tent", + "diversity": false + }, + { + "name": "foggy", + "diversity": false + }, + { + "name": "night_with_stars", + "diversity": false + }, + { + "name": "sunrise_over_mountains", + "diversity": false + }, + { + "name": "sunrise", + "diversity": false + }, + { + "name": "city_sunset", + "diversity": false + }, + { + "name": "city_sunrise", + "diversity": false + }, + { + "name": "bridge_at_night", + "diversity": false + }, + { + "name": "hotsprings", + "diversity": false + }, + { + "name": "milky_way", + "diversity": false + }, + { + "name": "carousel_horse", + "diversity": false + }, + { + "name": "ferris_wheel", + "diversity": false + }, + { + "name": "roller_coaster", + "diversity": false + }, + { + "name": "barber", + "diversity": false + }, + { + "name": "circus_tent", + "diversity": false + }, + { + "name": "performing_arts", + "diversity": false + }, + { + "name": "framed_picture", + "diversity": false + }, + { + "name": "art", + "diversity": false + }, + { + "name": "slot_machine", + "diversity": false + }, + { + "name": "steam_locomotive", + "diversity": false + }, + { + "name": "railway_car", + "diversity": false + }, + { + "name": "bullettrain_side", + "diversity": false + }, + { + "name": "bullettrain_front", + "diversity": false + }, + { + "name": "train2", + "diversity": false + }, + { + "name": "metro", + "diversity": false + }, + { + "name": "light_rail", + "diversity": false + }, + { + "name": "station", + "diversity": false + }, + { + "name": "tram", + "diversity": false + }, + { + "name": "monorail", + "diversity": false + }, + { + "name": "mountain_railway", + "diversity": false + }, + { + "name": "train", + "diversity": false + }, + { + "name": "bus", + "diversity": false + }, + { + "name": "oncoming_bus", + "diversity": false + }, + { + "name": "trolleybus", + "diversity": false + }, + { + "name": "minibus", + "diversity": false + }, + { + "name": "ambulance", + "diversity": false + }, + { + "name": "fire_engine", + "diversity": false + }, + { + "name": "police_car", + "diversity": false + }, + { + "name": "oncoming_police_car", + "diversity": false + }, + { + "name": "taxi", + "diversity": false + }, + { + "name": "oncoming_taxi", + "diversity": false + }, + { + "name": "red_car", + "diversity": false + }, + { + "name": "oncoming_automobile", + "diversity": false + }, + { + "name": "blue_car", + "diversity": false + }, + { + "name": "truck", + "diversity": false + }, + { + "name": "articulated_lorry", + "diversity": false + }, + { + "name": "tractor", + "diversity": false + }, + { + "name": "bike", + "diversity": false + }, + { + "name": "kick_scooter", + "diversity": false + }, + { + "name": "motor_scooter", + "diversity": false + }, + { + "name": "busstop", + "diversity": false + }, + { + "name": "motorway", + "diversity": false + }, + { + "name": "railway_track", + "diversity": false + }, + { + "name": "fuelpump", + "diversity": false + }, + { + "name": "rotating_light", + "diversity": false + }, + { + "name": "traffic_light", + "diversity": false + }, + { + "name": "vertical_traffic_light", + "diversity": false + }, + { + "name": "construction", + "diversity": false + }, + { + "name": "stop_sign", + "diversity": false + }, + { + "name": "anchor", + "diversity": false + }, + { + "name": "sailboat", + "diversity": false + }, + { + "name": "canoe", + "diversity": false + }, + { + "name": "speedboat", + "diversity": false + }, + { + "name": "passenger_ship", + "diversity": false + }, + { + "name": "ferry", + "diversity": false + }, + { + "name": "motor_boat", + "diversity": false + }, + { + "name": "ship", + "diversity": false + }, + { + "name": "airplane", + "diversity": false + }, + { + "name": "small_airplane", + "diversity": false + }, + { + "name": "flight_departure", + "diversity": false + }, + { + "name": "flight_arrival", + "diversity": false + }, + { + "name": "seat", + "diversity": false + }, + { + "name": "helicopter", + "diversity": false + }, + { + "name": "suspension_railway", + "diversity": false + }, + { + "name": "mountain_cableway", + "diversity": false + }, + { + "name": "aerial_tramway", + "diversity": false + }, + { + "name": "artificial_satellite", + "diversity": false + }, + { + "name": "rocket", + "diversity": false + }, + { + "name": "flying_saucer", + "diversity": false + }, + { + "name": "bellhop_bell", + "diversity": false + }, + { + "name": "door", + "diversity": false + }, + { + "name": "bed", + "diversity": false + }, + { + "name": "couch_and_lamp", + "diversity": false + }, + { + "name": "toilet", + "diversity": false + }, + { + "name": "shower", + "diversity": false + }, + { + "name": "bathtub", + "diversity": false + }, + { + "name": "checkered_flag", + "diversity": false + }, + { + "name": "triangular_flag_on_post", + "diversity": false + }, + { + "name": "crossed_flags", + "diversity": false + }, + { + "name": "black_flag", + "diversity": false + }, + { + "name": "white_flag", + "diversity": false + }, + { + "name": "rainbow_flag", + "diversity": false + }, + { + "name": "ascension_island", + "diversity": false + }, + { + "name": "andorra", + "diversity": false + }, + { + "name": "united_arab_emirates", + "diversity": false + }, + { + "name": "afghanistan", + "diversity": false + }, + { + "name": "antigua_barbuda", + "diversity": false + }, + { + "name": "anguilla", + "diversity": false + }, + { + "name": "albania", + "diversity": false + }, + { + "name": "armenia", + "diversity": false + }, + { + "name": "angola", + "diversity": false + }, + { + "name": "antarctica", + "diversity": false + }, + { + "name": "argentina", + "diversity": false + }, + { + "name": "american_samoa", + "diversity": false + }, + { + "name": "austria", + "diversity": false + }, + { + "name": "australia", + "diversity": false + }, + { + "name": "aruba", + "diversity": false + }, + { + "name": "aland_islands", + "diversity": false + }, + { + "name": "azerbaijan", + "diversity": false + }, + { + "name": "bosnia_herzegovina", + "diversity": false + }, + { + "name": "barbados", + "diversity": false + }, + { + "name": "bangladesh", + "diversity": false + }, + { + "name": "belgium", + "diversity": false + }, + { + "name": "burkina_faso", + "diversity": false + }, + { + "name": "bulgaria", + "diversity": false + }, + { + "name": "bahrain", + "diversity": false + }, + { + "name": "burundi", + "diversity": false + }, + { + "name": "benin", + "diversity": false + }, + { + "name": "st_barthelemy", + "diversity": false + }, + { + "name": "bermuda", + "diversity": false + }, + { + "name": "brunei", + "diversity": false + }, + { + "name": "bolivia", + "diversity": false + }, + { + "name": "caribbean_netherlands", + "diversity": false + }, + { + "name": "brazil", + "diversity": false + }, + { + "name": "bahamas", + "diversity": false + }, + { + "name": "bhutan", + "diversity": false + }, + { + "name": "bouvet_island", + "diversity": false + }, + { + "name": "botswana", + "diversity": false + }, + { + "name": "belarus", + "diversity": false + }, + { + "name": "belize", + "diversity": false + }, + { + "name": "canada", + "diversity": false + }, + { + "name": "cocos_islands", + "diversity": false + }, + { + "name": "congo_kinshasa", + "diversity": false + }, + { + "name": "central_african_republic", + "diversity": false + }, + { + "name": "congo_brazzaville", + "diversity": false + }, + { + "name": "switzerland", + "diversity": false + }, + { + "name": "cote_divoire", + "diversity": false + }, + { + "name": "cook_islands", + "diversity": false + }, + { + "name": "chile", + "diversity": false + }, + { + "name": "cameroon", + "diversity": false + }, + { + "name": "cn", + "diversity": false + }, + { + "name": "colombia", + "diversity": false + }, + { + "name": "clipperton_island", + "diversity": false + }, + { + "name": "costa_rica", + "diversity": false + }, + { + "name": "cuba", + "diversity": false + }, + { + "name": "cape_verde", + "diversity": false + }, + { + "name": "curacao", + "diversity": false + }, + { + "name": "christmas_island", + "diversity": false + }, + { + "name": "cyprus", + "diversity": false + }, + { + "name": "czech_republic", + "diversity": false + }, + { + "name": "de", + "diversity": false + }, + { + "name": "diego_garcia", + "diversity": false + }, + { + "name": "djibouti", + "diversity": false + }, + { + "name": "denmark", + "diversity": false + }, + { + "name": "dominica", + "diversity": false + }, + { + "name": "dominican_republic", + "diversity": false + }, + { + "name": "algeria", + "diversity": false + }, + { + "name": "ceuta_and_melilla", + "diversity": false + }, + { + "name": "ecuador", + "diversity": false + }, + { + "name": "estonia", + "diversity": false + }, + { + "name": "egypt", + "diversity": false + }, + { + "name": "western_sahara", + "diversity": false + }, + { + "name": "eritrea", + "diversity": false + }, + { + "name": "es", + "diversity": false + }, + { + "name": "ethiopia", + "diversity": false + }, + { + "name": "eu", + "diversity": false + }, + { + "name": "finland", + "diversity": false + }, + { + "name": "fiji", + "diversity": false + }, + { + "name": "falkland_islands", + "diversity": false + }, + { + "name": "micronesia", + "diversity": false + }, + { + "name": "faroe_islands", + "diversity": false + }, + { + "name": "fr", + "diversity": false + }, + { + "name": "gabon", + "diversity": false + }, + { + "name": "uk", + "diversity": false + }, + { + "name": "grenada", + "diversity": false + }, + { + "name": "georgia", + "diversity": false + }, + { + "name": "french_guiana", + "diversity": false + }, + { + "name": "guernsey", + "diversity": false + }, + { + "name": "ghana", + "diversity": false + }, + { + "name": "gibraltar", + "diversity": false + }, + { + "name": "greenland", + "diversity": false + }, + { + "name": "gambia", + "diversity": false + }, + { + "name": "guinea", + "diversity": false + }, + { + "name": "guadeloupe", + "diversity": false + }, + { + "name": "equatorial_guinea", + "diversity": false + }, + { + "name": "greece", + "diversity": false + }, + { + "name": "south_georgia_south_sandwich_islands", + "diversity": false + }, + { + "name": "guatemala", + "diversity": false + }, + { + "name": "guam", + "diversity": false + }, + { + "name": "guinea_bissau", + "diversity": false + }, + { + "name": "guyana", + "diversity": false + }, + { + "name": "hong_kong", + "diversity": false + }, + { + "name": "heard_and_mc_donald_islands", + "diversity": false + }, + { + "name": "honduras", + "diversity": false + }, + { + "name": "croatia", + "diversity": false + }, + { + "name": "haiti", + "diversity": false + }, + { + "name": "hungary", + "diversity": false + }, + { + "name": "canary_islands", + "diversity": false + }, + { + "name": "indonesia", + "diversity": false + }, + { + "name": "ireland", + "diversity": false + }, + { + "name": "israel", + "diversity": false + }, + { + "name": "isle_of_man", + "diversity": false + }, + { + "name": "india", + "diversity": false + }, + { + "name": "british_indian_ocean_territory", + "diversity": false + }, + { + "name": "iraq", + "diversity": false + }, + { + "name": "iran", + "diversity": false + }, + { + "name": "iceland", + "diversity": false + }, + { + "name": "it", + "diversity": false + }, + { + "name": "jersey", + "diversity": false + }, + { + "name": "jamaica", + "diversity": false + }, + { + "name": "jordan", + "diversity": false + }, + { + "name": "jp", + "diversity": false + }, + { + "name": "kenya", + "diversity": false + }, + { + "name": "kyrgyzstan", + "diversity": false + }, + { + "name": "cambodia", + "diversity": false + }, + { + "name": "kiribati", + "diversity": false + }, + { + "name": "comoros", + "diversity": false + }, + { + "name": "st_kitts_nevis", + "diversity": false + }, + { + "name": "north_korea", + "diversity": false + }, + { + "name": "kr", + "diversity": false + }, + { + "name": "kuwait", + "diversity": false + }, + { + "name": "cayman_islands", + "diversity": false + }, + { + "name": "kazakhstan", + "diversity": false + }, + { + "name": "laos", + "diversity": false + }, + { + "name": "lebanon", + "diversity": false + }, + { + "name": "st_lucia", + "diversity": false + }, + { + "name": "liechtenstein", + "diversity": false + }, + { + "name": "sri_lanka", + "diversity": false + }, + { + "name": "liberia", + "diversity": false + }, + { + "name": "lesotho", + "diversity": false + }, + { + "name": "lithuania", + "diversity": false + }, + { + "name": "luxembourg", + "diversity": false + }, + { + "name": "latvia", + "diversity": false + }, + { + "name": "libya", + "diversity": false + }, + { + "name": "morocco", + "diversity": false + }, + { + "name": "monaco", + "diversity": false + }, + { + "name": "moldova", + "diversity": false + }, + { + "name": "montenegro", + "diversity": false + }, + { + "name": "st_martin", + "diversity": false + }, + { + "name": "madagascar", + "diversity": false + }, + { + "name": "marshall_islands", + "diversity": false + }, + { + "name": "macedonia", + "diversity": false + }, + { + "name": "mali", + "diversity": false + }, + { + "name": "myanmar", + "diversity": false + }, + { + "name": "mongolia", + "diversity": false + }, + { + "name": "macau", + "diversity": false + }, + { + "name": "northern_mariana_islands", + "diversity": false + }, + { + "name": "martinique", + "diversity": false + }, + { + "name": "mauritania", + "diversity": false + }, + { + "name": "montserrat", + "diversity": false + }, + { + "name": "malta", + "diversity": false + }, + { + "name": "mauritius", + "diversity": false + }, + { + "name": "maldives", + "diversity": false + }, + { + "name": "malawi", + "diversity": false + }, + { + "name": "mexico", + "diversity": false + }, + { + "name": "malaysia", + "diversity": false + }, + { + "name": "mozambique", + "diversity": false + }, + { + "name": "namibia", + "diversity": false + }, + { + "name": "new_caledonia", + "diversity": false + }, + { + "name": "niger", + "diversity": false + }, + { + "name": "norfolk_island", + "diversity": false + }, + { + "name": "nigeria", + "diversity": false + }, + { + "name": "nicaragua", + "diversity": false + }, + { + "name": "netherlands", + "diversity": false + }, + { + "name": "norway", + "diversity": false + }, + { + "name": "nepal", + "diversity": false + }, + { + "name": "nauru", + "diversity": false + }, + { + "name": "niue", + "diversity": false + }, + { + "name": "new_zealand", + "diversity": false + }, + { + "name": "oman", + "diversity": false + }, + { + "name": "panama", + "diversity": false + }, + { + "name": "peru", + "diversity": false + }, + { + "name": "french_polynesia", + "diversity": false + }, + { + "name": "papua_new_guinea", + "diversity": false + }, + { + "name": "philippines", + "diversity": false + }, + { + "name": "pakistan", + "diversity": false + }, + { + "name": "poland", + "diversity": false + }, + { + "name": "st_pierre_miquelon", + "diversity": false + }, + { + "name": "pitcairn_islands", + "diversity": false + }, + { + "name": "puerto_rico", + "diversity": false + }, + { + "name": "palestinian_territories", + "diversity": false + }, + { + "name": "portugal", + "diversity": false + }, + { + "name": "palau", + "diversity": false + }, + { + "name": "paraguay", + "diversity": false + }, + { + "name": "qatar", + "diversity": false + }, + { + "name": "reunion", + "diversity": false + }, + { + "name": "romania", + "diversity": false + }, + { + "name": "serbia", + "diversity": false + }, + { + "name": "ru", + "diversity": false + }, + { + "name": "rwanda", + "diversity": false + }, + { + "name": "saudi_arabia", + "diversity": false + }, + { + "name": "solomon_islands", + "diversity": false + }, + { + "name": "seychelles", + "diversity": false + }, + { + "name": "sudan", + "diversity": false + }, + { + "name": "sweden", + "diversity": false + }, + { + "name": "singapore", + "diversity": false + }, + { + "name": "st_helena", + "diversity": false + }, + { + "name": "slovenia", + "diversity": false + }, + { + "name": "svalbard_and_jan_mayen", + "diversity": false + }, + { + "name": "slovakia", + "diversity": false + }, + { + "name": "sierra_leone", + "diversity": false + }, + { + "name": "san_marino", + "diversity": false + }, + { + "name": "senegal", + "diversity": false + }, + { + "name": "somalia", + "diversity": false + }, + { + "name": "suriname", + "diversity": false + }, + { + "name": "south_sudan", + "diversity": false + }, + { + "name": "sao_tome_principe", + "diversity": false + }, + { + "name": "el_salvador", + "diversity": false + }, + { + "name": "sint_maarten", + "diversity": false + }, + { + "name": "syria", + "diversity": false + }, + { + "name": "swaziland", + "diversity": false + }, + { + "name": "tristan_da_cunha", + "diversity": false + }, + { + "name": "turks_caicos_islands", + "diversity": false + }, + { + "name": "chad", + "diversity": false + }, + { + "name": "french_southern_territories", + "diversity": false + }, + { + "name": "togo", + "diversity": false + }, + { + "name": "thailand", + "diversity": false + }, + { + "name": "tajikistan", + "diversity": false + }, + { + "name": "tokelau", + "diversity": false + }, + { + "name": "timor_leste", + "diversity": false + }, + { + "name": "turkmenistan", + "diversity": false + }, + { + "name": "tunisia", + "diversity": false + }, + { + "name": "tonga", + "diversity": false + }, + { + "name": "tr", + "diversity": false + }, + { + "name": "trinidad_tobago", + "diversity": false + }, + { + "name": "tuvalu", + "diversity": false + }, + { + "name": "taiwan", + "diversity": false + }, + { + "name": "tanzania", + "diversity": false + }, + { + "name": "ukraine", + "diversity": false + }, + { + "name": "uganda", + "diversity": false + }, + { + "name": "us_outlying_islands", + "diversity": false + }, + { + "name": "united_nations", + "diversity": false + }, + { + "name": "us", + "diversity": false + }, + { + "name": "uruguay", + "diversity": false + }, + { + "name": "uzbekistan", + "diversity": false + }, + { + "name": "vatican_city", + "diversity": false + }, + { + "name": "st_vincent_grenadines", + "diversity": false + }, + { + "name": "venezuela", + "diversity": false + }, + { + "name": "british_virgin_islands", + "diversity": false + }, + { + "name": "us_virgin_islands", + "diversity": false + }, + { + "name": "vietnam", + "diversity": false + }, + { + "name": "vanuatu", + "diversity": false + }, + { + "name": "wallis_futuna", + "diversity": false + }, + { + "name": "samoa", + "diversity": false + }, + { + "name": "kosovo", + "diversity": false + }, + { + "name": "yemen", + "diversity": false + }, + { + "name": "mayotte", + "diversity": false + }, + { + "name": "south_africa", + "diversity": false + }, + { + "name": "zambia", + "diversity": false + }, + { + "name": "zimbabwe", + "diversity": false + } + ] + }, + { + "name": "objects", + "fullname": "Objects & Symbols", + "tabicon": "eyeglasses", + "icons": [ + { + "name": "eyeglasses", + "diversity": false + }, + { + "name": "dark_sunglasses", + "diversity": false + }, + { + "name": "necktie", + "diversity": false + }, + { + "name": "tshirt", + "diversity": false + }, + { + "name": "jeans", + "diversity": false + }, + { + "name": "scarf", + "diversity": false + }, + { + "name": "gloves", + "diversity": false + }, + { + "name": "coat", + "diversity": false + }, + { + "name": "socks", + "diversity": false + }, + { + "name": "dress", + "diversity": false + }, + { + "name": "kimono", + "diversity": false + }, + { + "name": "bikini", + "diversity": false + }, + { + "name": "womans_clothes", + "diversity": false + }, + { + "name": "purse", + "diversity": false + }, + { + "name": "handbag", + "diversity": false + }, + { + "name": "pouch", + "diversity": false + }, + { + "name": "shopping", + "diversity": false + }, + { + "name": "school_satchel", + "diversity": false + }, + { + "name": "mans_shoe", + "diversity": false + }, + { + "name": "athletic_shoe", + "diversity": false + }, + { + "name": "high_heel", + "diversity": false + }, + { + "name": "sandal", + "diversity": false + }, + { + "name": "boot", + "diversity": false + }, + { + "name": "crown", + "diversity": false + }, + { + "name": "womans_hat", + "diversity": false + }, + { + "name": "tophat", + "diversity": false + }, + { + "name": "mortar_board", + "diversity": false + }, + { + "name": "billed_cap", + "diversity": false + }, + { + "name": "rescue_worker_helmet", + "diversity": false + }, + { + "name": "prayer_beads", + "diversity": false + }, + { + "name": "lipstick", + "diversity": false + }, + { + "name": "ring", + "diversity": false + }, + { + "name": "gem", + "diversity": false + }, + { + "name": "medal_military", + "diversity": false + }, + { + "name": "trophy", + "diversity": false + }, + { + "name": "medal_sports", + "diversity": false + }, + { + "name": "1st_place_medal", + "diversity": false + }, + { + "name": "2nd_place_medal", + "diversity": false + }, + { + "name": "3rd_place_medal", + "diversity": false + }, + { + "name": "mute", + "diversity": false + }, + { + "name": "speaker", + "diversity": false + }, + { + "name": "sound", + "diversity": false + }, + { + "name": "loud_sound", + "diversity": false + }, + { + "name": "loudspeaker", + "diversity": false + }, + { + "name": "mega", + "diversity": false + }, + { + "name": "postal_horn", + "diversity": false + }, + { + "name": "bell", + "diversity": false + }, + { + "name": "no_bell", + "diversity": false + }, + { + "name": "iphone", + "diversity": false + }, + { + "name": "calling", + "diversity": false + }, + { + "name": "phone", + "diversity": false + }, + { + "name": "telephone_receiver", + "diversity": false + }, + { + "name": "pager", + "diversity": false + }, + { + "name": "fax", + "diversity": false + }, + { + "name": "battery", + "diversity": false + }, + { + "name": "electric_plug", + "diversity": false + }, + { + "name": "computer", + "diversity": false + }, + { + "name": "desktop_computer", + "diversity": false + }, + { + "name": "printer", + "diversity": false + }, + { + "name": "keyboard", + "diversity": false + }, + { + "name": "computer_mouse", + "diversity": false + }, + { + "name": "trackball", + "diversity": false + }, + { + "name": "minidisc", + "diversity": false + }, + { + "name": "floppy_disk", + "diversity": false + }, + { + "name": "cd", + "diversity": false + }, + { + "name": "dvd", + "diversity": false + }, + { + "name": "movie_camera", + "diversity": false + }, + { + "name": "film_strip", + "diversity": false + }, + { + "name": "film_projector", + "diversity": false + }, + { + "name": "clapper", + "diversity": false + }, + { + "name": "tv", + "diversity": false + }, + { + "name": "camera", + "diversity": false + }, + { + "name": "camera_flash", + "diversity": false + }, + { + "name": "video_camera", + "diversity": false + }, + { + "name": "vhs", + "diversity": false + }, + { + "name": "mag", + "diversity": false + }, + { + "name": "mag_right", + "diversity": false + }, + { + "name": "microscope", + "diversity": false + }, + { + "name": "telescope", + "diversity": false + }, + { + "name": "satellite", + "diversity": false + }, + { + "name": "candle", + "diversity": false + }, + { + "name": "bulb", + "diversity": false + }, + { + "name": "flashlight", + "diversity": false + }, + { + "name": "izakaya_lantern", + "diversity": false + }, + { + "name": "notebook_with_decorative_cover", + "diversity": false + }, + { + "name": "closed_book", + "diversity": false + }, + { + "name": "open_book", + "diversity": false + }, + { + "name": "green_book", + "diversity": false + }, + { + "name": "blue_book", + "diversity": false + }, + { + "name": "orange_book", + "diversity": false + }, + { + "name": "books", + "diversity": false + }, + { + "name": "notebook", + "diversity": false + }, + { + "name": "ledger", + "diversity": false + }, + { + "name": "page_with_curl", + "diversity": false + }, + { + "name": "scroll", + "diversity": false + }, + { + "name": "page_facing_up", + "diversity": false + }, + { + "name": "newspaper", + "diversity": false + }, + { + "name": "newspaper_roll", + "diversity": false + }, + { + "name": "bookmark_tabs", + "diversity": false + }, + { + "name": "bookmark", + "diversity": false + }, + { + "name": "label", + "diversity": false + }, + { + "name": "moneybag", + "diversity": false + }, + { + "name": "yen", + "diversity": false + }, + { + "name": "dollar", + "diversity": false + }, + { + "name": "euro", + "diversity": false + }, + { + "name": "pound", + "diversity": false + }, + { + "name": "money_with_wings", + "diversity": false + }, + { + "name": "credit_card", + "diversity": false + }, + { + "name": "chart", + "diversity": false + }, + { + "name": "currency_exchange", + "diversity": false + }, + { + "name": "heavy_dollar_sign", + "diversity": false + }, + { + "name": "email", + "diversity": false + }, + { + "name": "e-mail", + "diversity": false + }, + { + "name": "incoming_envelope", + "diversity": false + }, + { + "name": "envelope_with_arrow", + "diversity": false + }, + { + "name": "outbox_tray", + "diversity": false + }, + { + "name": "inbox_tray", + "diversity": false + }, + { + "name": "package", + "diversity": false + }, + { + "name": "mailbox", + "diversity": false + }, + { + "name": "mailbox_closed", + "diversity": false + }, + { + "name": "mailbox_with_mail", + "diversity": false + }, + { + "name": "mailbox_with_no_mail", + "diversity": false + }, + { + "name": "postbox", + "diversity": false + }, + { + "name": "ballot_box", + "diversity": false + }, + { + "name": "pencil2", + "diversity": false + }, + { + "name": "black_nib", + "diversity": false + }, + { + "name": "fountain_pen", + "diversity": false + }, + { + "name": "pen", + "diversity": false + }, + { + "name": "paintbrush", + "diversity": false + }, + { + "name": "crayon", + "diversity": false + }, + { + "name": "memo", + "diversity": false + }, + { + "name": "briefcase", + "diversity": false + }, + { + "name": "file_folder", + "diversity": false + }, + { + "name": "open_file_folder", + "diversity": false + }, + { + "name": "card_index_dividers", + "diversity": false + }, + { + "name": "date", + "diversity": false + }, + { + "name": "calendar", + "diversity": false + }, + { + "name": "spiral_notepad", + "diversity": false + }, + { + "name": "spiral_calendar", + "diversity": false + }, + { + "name": "card_index", + "diversity": false + }, + { + "name": "chart_with_upwards_trend", + "diversity": false + }, + { + "name": "chart_with_downwards_trend", + "diversity": false + }, + { + "name": "bar_chart", + "diversity": false + }, + { + "name": "clipboard", + "diversity": false + }, + { + "name": "pushpin", + "diversity": false + }, + { + "name": "round_pushpin", + "diversity": false + }, + { + "name": "paperclip", + "diversity": false + }, + { + "name": "paperclips", + "diversity": false + }, + { + "name": "straight_ruler", + "diversity": false + }, + { + "name": "triangular_ruler", + "diversity": false + }, + { + "name": "scissors", + "diversity": false + }, + { + "name": "card_file_box", + "diversity": false + }, + { + "name": "file_cabinet", + "diversity": false + }, + { + "name": "wastebasket", + "diversity": false + }, + { + "name": "lock", + "diversity": false + }, + { + "name": "unlock", + "diversity": false + }, + { + "name": "lock_with_ink_pen", + "diversity": false + }, + { + "name": "closed_lock_with_key", + "diversity": false + }, + { + "name": "key", + "diversity": false + }, + { + "name": "old_key", + "diversity": false + }, + { + "name": "hammer", + "diversity": false + }, + { + "name": "pick", + "diversity": false + }, + { + "name": "hammer_and_pick", + "diversity": false + }, + { + "name": "hammer_and_wrench", + "diversity": false + }, + { + "name": "dagger", + "diversity": false + }, + { + "name": "crossed_swords", + "diversity": false + }, + { + "name": "gun", + "diversity": false + }, + { + "name": "bow_and_arrow", + "diversity": false + }, + { + "name": "shield", + "diversity": false + }, + { + "name": "wrench", + "diversity": false + }, + { + "name": "nut_and_bolt", + "diversity": false + }, + { + "name": "gear", + "diversity": false + }, + { + "name": "clamp", + "diversity": false + }, + { + "name": "alembic", + "diversity": false + }, + { + "name": "balance_scale", + "diversity": false + }, + { + "name": "link", + "diversity": false + }, + { + "name": "chains", + "diversity": false + }, + { + "name": "syringe", + "diversity": false + }, + { + "name": "pill", + "diversity": false + }, + { + "name": "smoking", + "diversity": false + }, + { + "name": "coffin", + "diversity": false + }, + { + "name": "funeral_urn", + "diversity": false + }, + { + "name": "moyai", + "diversity": false + }, + { + "name": "oil_drum", + "diversity": false + }, + { + "name": "crystal_ball", + "diversity": false + }, + { + "name": "shopping_cart", + "diversity": false + }, + { + "name": "atm", + "diversity": false + }, + { + "name": "put_litter_in_its_place", + "diversity": false + }, + { + "name": "potable_water", + "diversity": false + }, + { + "name": "wheelchair", + "diversity": false + }, + { + "name": "mens", + "diversity": false + }, + { + "name": "womens", + "diversity": false + }, + { + "name": "restroom", + "diversity": false + }, + { + "name": "baby_symbol", + "diversity": false + }, + { + "name": "wc", + "diversity": false + }, + { + "name": "passport_control", + "diversity": false + }, + { + "name": "customs", + "diversity": false + }, + { + "name": "baggage_claim", + "diversity": false + }, + { + "name": "left_luggage", + "diversity": false + }, + { + "name": "warning", + "diversity": false + }, + { + "name": "children_crossing", + "diversity": false + }, + { + "name": "no_entry", + "diversity": false + }, + { + "name": "no_entry_sign", + "diversity": false + }, + { + "name": "no_bicycles", + "diversity": false + }, + { + "name": "no_smoking", + "diversity": false + }, + { + "name": "do_not_litter", + "diversity": false + }, + { + "name": "non-potable_water", + "diversity": false + }, + { + "name": "no_pedestrians", + "diversity": false + }, + { + "name": "no_mobile_phones", + "diversity": false + }, + { + "name": "underage", + "diversity": false + }, + { + "name": "radioactive", + "diversity": false + }, + { + "name": "biohazard", + "diversity": false + }, + { + "name": "arrow_up", + "diversity": false + }, + { + "name": "arrow_upper_right", + "diversity": false + }, + { + "name": "arrow_right", + "diversity": false + }, + { + "name": "arrow_lower_right", + "diversity": false + }, + { + "name": "arrow_down", + "diversity": false + }, + { + "name": "arrow_lower_left", + "diversity": false + }, + { + "name": "arrow_left", + "diversity": false + }, + { + "name": "arrow_upper_left", + "diversity": false + }, + { + "name": "arrow_up_down", + "diversity": false + }, + { + "name": "left_right_arrow", + "diversity": false + }, + { + "name": "leftwards_arrow_with_hook", + "diversity": false + }, + { + "name": "arrow_right_hook", + "diversity": false + }, + { + "name": "arrow_heading_up", + "diversity": false + }, + { + "name": "arrow_heading_down", + "diversity": false + }, + { + "name": "arrows_clockwise", + "diversity": false + }, + { + "name": "arrows_counterclockwise", + "diversity": false + }, + { + "name": "back", + "diversity": false + }, + { + "name": "end", + "diversity": false + }, + { + "name": "on", + "diversity": false + }, + { + "name": "soon", + "diversity": false + }, + { + "name": "top", + "diversity": false + }, + { + "name": "place_of_worship", + "diversity": false + }, + { + "name": "atom_symbol", + "diversity": false + }, + { + "name": "om", + "diversity": false + }, + { + "name": "star_of_david", + "diversity": false + }, + { + "name": "wheel_of_dharma", + "diversity": false + }, + { + "name": "yin_yang", + "diversity": false + }, + { + "name": "latin_cross", + "diversity": false + }, + { + "name": "orthodox_cross", + "diversity": false + }, + { + "name": "star_and_crescent", + "diversity": false + }, + { + "name": "peace_symbol", + "diversity": false + }, + { + "name": "menorah", + "diversity": false + }, + { + "name": "six_pointed_star", + "diversity": false + }, + { + "name": "aries", + "diversity": false + }, + { + "name": "taurus", + "diversity": false + }, + { + "name": "gemini", + "diversity": false + }, + { + "name": "cancer", + "diversity": false + }, + { + "name": "leo", + "diversity": false + }, + { + "name": "virgo", + "diversity": false + }, + { + "name": "libra", + "diversity": false + }, + { + "name": "scorpius", + "diversity": false + }, + { + "name": "sagittarius", + "diversity": false + }, + { + "name": "capricorn", + "diversity": false + }, + { + "name": "aquarius", + "diversity": false + }, + { + "name": "pisces", + "diversity": false + }, + { + "name": "ophiuchus", + "diversity": false + }, + { + "name": "twisted_rightwards_arrows", + "diversity": false + }, + { + "name": "repeat", + "diversity": false + }, + { + "name": "repeat_one", + "diversity": false + }, + { + "name": "arrow_forward", + "diversity": false + }, + { + "name": "fast_forward", + "diversity": false + }, + { + "name": "next_track_button", + "diversity": false + }, + { + "name": "play_or_pause_button", + "diversity": false + }, + { + "name": "arrow_backward", + "diversity": false + }, + { + "name": "rewind", + "diversity": false + }, + { + "name": "previous_track_button", + "diversity": false + }, + { + "name": "arrow_up_small", + "diversity": false + }, + { + "name": "arrow_double_up", + "diversity": false + }, + { + "name": "arrow_down_small", + "diversity": false + }, + { + "name": "arrow_double_down", + "diversity": false + }, + { + "name": "pause_button", + "diversity": false + }, + { + "name": "stop_button", + "diversity": false + }, + { + "name": "record_button", + "diversity": false + }, + { + "name": "cinema", + "diversity": false + }, + { + "name": "low_brightness", + "diversity": false + }, + { + "name": "high_brightness", + "diversity": false + }, + { + "name": "signal_strength", + "diversity": false + }, + { + "name": "vibration_mode", + "diversity": false + }, + { + "name": "mobile_phone_off", + "diversity": false + }, + { + "name": "recycle", + "diversity": false + }, + { + "name": "fleur_de_lis", + "diversity": false + }, + { + "name": "trident", + "diversity": false + }, + { + "name": "name_badge", + "diversity": false + }, + { + "name": "beginner", + "diversity": false + }, + { + "name": "o", + "diversity": false + }, + { + "name": "white_check_mark", + "diversity": false + }, + { + "name": "ballot_box_with_check", + "diversity": false + }, + { + "name": "heavy_check_mark", + "diversity": false + }, + { + "name": "heavy_multiplication_x", + "diversity": false + }, + { + "name": "x", + "diversity": false + }, + { + "name": "negative_squared_cross_mark", + "diversity": false + }, + { + "name": "heavy_plus_sign", + "diversity": false + }, + { + "name": "heavy_minus_sign", + "diversity": false + }, + { + "name": "heavy_division_sign", + "diversity": false + }, + { + "name": "curly_loop", + "diversity": false + }, + { + "name": "loop", + "diversity": false + }, + { + "name": "part_alternation_mark", + "diversity": false + }, + { + "name": "eight_spoked_asterisk", + "diversity": false + }, + { + "name": "eight_pointed_black_star", + "diversity": false + }, + { + "name": "sparkle", + "diversity": false + }, + { + "name": "bangbang", + "diversity": false + }, + { + "name": "interrobang", + "diversity": false + }, + { + "name": "question", + "diversity": false + }, + { + "name": "grey_question", + "diversity": false + }, + { + "name": "grey_exclamation", + "diversity": false + }, + { + "name": "exclamation", + "diversity": false + }, + { + "name": "wavy_dash", + "diversity": false + }, + { + "name": "copyright", + "diversity": false + }, + { + "name": "registered", + "diversity": false + }, + { + "name": "tm", + "diversity": false + }, + { + "name": "hash", + "diversity": false + }, + { + "name": "asterisk", + "diversity": false + }, + { + "name": "zero", + "diversity": false + }, + { + "name": "one", + "diversity": false + }, + { + "name": "two", + "diversity": false + }, + { + "name": "three", + "diversity": false + }, + { + "name": "four", + "diversity": false + }, + { + "name": "five", + "diversity": false + }, + { + "name": "six", + "diversity": false + }, + { + "name": "seven", + "diversity": false + }, + { + "name": "eight", + "diversity": false + }, + { + "name": "nine", + "diversity": false + }, + { + "name": "keycap_ten", + "diversity": false + }, + { + "name": "100", + "diversity": false + }, + { + "name": "capital_abcd", + "diversity": false + }, + { + "name": "abcd", + "diversity": false + }, + { + "name": "1234", + "diversity": false + }, + { + "name": "symbols", + "diversity": false + }, + { + "name": "abc", + "diversity": false + }, + { + "name": "a", + "diversity": false + }, + { + "name": "ab", + "diversity": false + }, + { + "name": "b", + "diversity": false + }, + { + "name": "cl", + "diversity": false + }, + { + "name": "cool", + "diversity": false + }, + { + "name": "free", + "diversity": false + }, + { + "name": "information_source", + "diversity": false + }, + { + "name": "id", + "diversity": false + }, + { + "name": "m", + "diversity": false + }, + { + "name": "new", + "diversity": false + }, + { + "name": "ng", + "diversity": false + }, + { + "name": "o2", + "diversity": false + }, + { + "name": "ok", + "diversity": false + }, + { + "name": "parking", + "diversity": false + }, + { + "name": "sos", + "diversity": false + }, + { + "name": "up", + "diversity": false + }, + { + "name": "vs", + "diversity": false + }, + { + "name": "koko", + "diversity": false + }, + { + "name": "sa", + "diversity": false + }, + { + "name": "u6708", + "diversity": false + }, + { + "name": "u6709", + "diversity": false + }, + { + "name": "u6307", + "diversity": false + }, + { + "name": "ideograph_advantage", + "diversity": false + }, + { + "name": "u5272", + "diversity": false + }, + { + "name": "u7121", + "diversity": false + }, + { + "name": "u7981", + "diversity": false + }, + { + "name": "accept", + "diversity": false + }, + { + "name": "u7533", + "diversity": false + }, + { + "name": "u5408", + "diversity": false + }, + { + "name": "u7a7a", + "diversity": false + }, + { + "name": "congratulations", + "diversity": false + }, + { + "name": "secret", + "diversity": false + }, + { + "name": "u55b6", + "diversity": false + }, + { + "name": "u6e80", + "diversity": false + }, + { + "name": "black_small_square", + "diversity": false + }, + { + "name": "white_small_square", + "diversity": false + }, + { + "name": "white_medium_square", + "diversity": false + }, + { + "name": "black_medium_square", + "diversity": false + }, + { + "name": "white_medium_small_square", + "diversity": false + }, + { + "name": "black_medium_small_square", + "diversity": false + }, + { + "name": "black_large_square", + "diversity": false + }, + { + "name": "white_large_square", + "diversity": false + }, + { + "name": "large_orange_diamond", + "diversity": false + }, + { + "name": "large_blue_diamond", + "diversity": false + }, + { + "name": "small_orange_diamond", + "diversity": false + }, + { + "name": "small_blue_diamond", + "diversity": false + }, + { + "name": "small_red_triangle", + "diversity": false + }, + { + "name": "small_red_triangle_down", + "diversity": false + }, + { + "name": "diamond_shape_with_a_dot_inside", + "diversity": false + }, + { + "name": "radio_button", + "diversity": false + }, + { + "name": "black_square_button", + "diversity": false + }, + { + "name": "white_square_button", + "diversity": false + }, + { + "name": "white_circle", + "diversity": false + }, + { + "name": "black_circle", + "diversity": false + }, + { + "name": "red_circle", + "diversity": false + }, + { + "name": "large_blue_circle", + "diversity": false + }, + { + "name": "hourglass", + "diversity": false + }, + { + "name": "hourglass_flowing_sand", + "diversity": false + }, + { + "name": "watch", + "diversity": false + }, + { + "name": "alarm_clock", + "diversity": false + }, + { + "name": "stopwatch", + "diversity": false + }, + { + "name": "timer_clock", + "diversity": false + }, + { + "name": "mantelpiece_clock", + "diversity": false + }, + { + "name": "clock12", + "diversity": false + }, + { + "name": "clock1230", + "diversity": false + }, + { + "name": "clock1", + "diversity": false + }, + { + "name": "clock130", + "diversity": false + }, + { + "name": "clock2", + "diversity": false + }, + { + "name": "clock230", + "diversity": false + }, + { + "name": "clock3", + "diversity": false + }, + { + "name": "clock330", + "diversity": false + }, + { + "name": "clock4", + "diversity": false + }, + { + "name": "clock430", + "diversity": false + }, + { + "name": "clock5", + "diversity": false + }, + { + "name": "clock530", + "diversity": false + }, + { + "name": "clock6", + "diversity": false + }, + { + "name": "clock630", + "diversity": false + }, + { + "name": "clock7", + "diversity": false + }, + { + "name": "clock730", + "diversity": false + }, + { + "name": "clock8", + "diversity": false + }, + { + "name": "clock830", + "diversity": false + }, + { + "name": "clock9", + "diversity": false + }, + { + "name": "clock930", + "diversity": false + }, + { + "name": "clock10", + "diversity": false + }, + { + "name": "clock1030", + "diversity": false + }, + { + "name": "clock11", + "diversity": false + }, + { + "name": "clock1130", + "diversity": false + } + ] + } +] \ No newline at end of file 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 0814a968be..545f752280 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -14,7 +14,7 @@ module Tilt source = input[:data] context = input[:environment].context_class.new(input) - result = new(filename){source}.render(context) + result = new(filename) { source }.render(context) context.metadata.merge(data: result) end @@ -28,8 +28,8 @@ module Tilt ctx = MiniRacer::Context.new(timeout: 15000) ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}") ctx.eval("module = {}; exports = {};"); - ctx.attach("rails.logger.info", proc{|err| Rails.logger.info(err.to_s)}) - ctx.attach("rails.logger.error", proc{|err| Rails.logger.error(err.to_s)}) + ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) }) + ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) }) ctx.eval <]*class\s*=\s*['|"]excerpt['|"][^>]*>/ - def initialize(length, options=nil) + def initialize(length, options = nil) @length = length @excerpt = "" @current_length = 0 @@ -45,13 +45,13 @@ class ExcerptParser < Nokogiri::XML::SAX::Document end def include_tag(name, attributes) - characters("<#{name} #{attributes.map{|k,v| "#{k}=\"#{escape_attribute(v)}\""}.join(' ')}>", false, false, false) + characters("<#{name} #{attributes.map { |k, v| "#{k}=\"#{escape_attribute(v)}\"" }.join(' ')}>", false, false, false) end - def start_element(name, attributes=[]) + def start_element(name, attributes = []) case name - when "img" - attributes = Hash[*attributes.flatten] + when "img" + attributes = Hash[*attributes.flatten] if attributes["class"] == 'emoji' if @remap_emoji @@ -78,28 +78,28 @@ class ExcerptParser < Nokogiri::XML::SAX::Document characters("(#{attributes['src']})") if @markdown_images - when "a" - unless @strip_links - include_tag(name, attributes) - @in_a = true - end + when "a" + unless @strip_links + include_tag(name, attributes) + @in_a = true + end - when "aside" - attributes = Hash[*attributes.flatten] + when "aside" + attributes = Hash[*attributes.flatten] unless @keep_onebox_source && attributes['class'].include?('onebox') @in_quote = true end - when 'article' - if @keep_onebox_source && attributes.include?(['class', 'onebox-body']) - @in_quote = true - end - when "div", "span" - if attributes.include?(["class", "excerpt"]) - @excerpt = "" - @current_length = 0 - @start_excerpt = true - end + when 'article' + if @keep_onebox_source && attributes.include?(['class', 'onebox-body']) + @in_quote = true + end + when "div", "span" + if attributes.include?(["class", "excerpt"]) + @excerpt = "" + @current_length = 0 + @start_excerpt = true + end # Preserve spoilers if attributes.include?(["class", "spoiler"]) include_tag("span", attributes) @@ -112,7 +112,7 @@ class ExcerptParser < Nokogiri::XML::SAX::Document case name when "a" unless @strip_links - characters("",false, false, false) + characters("", false, false, false) @in_a = false end when "p", "br" @@ -132,7 +132,7 @@ class ExcerptParser < Nokogiri::XML::SAX::Document def characters(string, truncate = true, count_it = true, encode = true) return if @in_quote - encode = encode ? lambda{|s| ERB::Util.html_escape(s)} : lambda {|s| s} + encode = encode ? lambda { |s| ERB::Util.html_escape(s) } : lambda { |s| s } if count_it && @current_length + string.length > @length length = [0, @length - @current_length - 1].max @excerpt << encode.call(string[0..length]) if truncate diff --git a/lib/file_helper.rb b/lib/file_helper.rb index f3ae79d4a5..be727230d0 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -48,14 +48,32 @@ class FileHelper downloaded&.close end + def self.optimize_image!(filename) + ImageOptim.new( + # GLOBAL + timeout: 15, + skip_missing_workers: true, + # PNG + optipng: { level: 2, strip: SiteSetting.strip_image_metadata }, + advpng: false, + pngcrush: false, + pngout: false, + pngquant: false, + # JPG + jpegoptim: { strip: SiteSetting.strip_image_metadata ? "all" : "none" }, + jpegtran: false, + jpegrecompress: false, + ).optimize_image!(filename) + end + private - def self.images - @@images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} - end + def self.images + @@images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} + end - def self.images_regexp - @@images_regexp ||= /\.(#{images.to_a.join("|")})$/i - end + def self.images_regexp + @@images_regexp ||= /\.(#{images.to_a.join("|")})$/i + end end diff --git a/lib/file_store/base_store.rb b/lib/file_store/base_store.rb index b79282fe68..0a7ae8ba5a 100644 --- a/lib/file_store/base_store.rb +++ b/lib/file_store/base_store.rb @@ -95,7 +95,7 @@ module FileStore end def get_path_for_upload(upload) - get_path_for("original".freeze, upload.id, upload.sha1, upload.extension) + get_path_for("original".freeze, upload.id, upload.sha1, "." + upload.extension) end def get_path_for_optimized_image(optimized_image) diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb index 265c7d53a8..98f1c13f08 100644 --- a/lib/file_store/s3_store.rb +++ b/lib/file_store/s3_store.rb @@ -9,7 +9,7 @@ module FileStore class S3Store < BaseStore TOMBSTONE_PREFIX ||= "tombstone/" - def initialize(s3_helper=nil) + def initialize(s3_helper = nil) @s3_helper = s3_helper || S3Helper.new(s3_bucket, TOMBSTONE_PREFIX) end @@ -27,7 +27,7 @@ module FileStore # - filename # - content_type # - cache_locally - def store_file(file, path, opts={}) + def store_file(file, path, opts = {}) filename = opts[:filename].presence || File.basename(path) # cache file locally when needed cache_file(file, File.basename(path)) if opts[:cache_locally] diff --git a/lib/filter_best_posts.rb b/lib/filter_best_posts.rb index 88f97d4d1a..e01e79e0e0 100644 --- a/lib/filter_best_posts.rb +++ b/lib/filter_best_posts.rb @@ -2,26 +2,27 @@ class FilterBestPosts attr_accessor :filtered_posts, :posts - def initialize(topic, filtered_posts, limit, options={}) + def initialize(topic, filtered_posts, limit, options = {}) @filtered_posts = filtered_posts @topic = topic @limit = limit options.each do |key, value| - self.instance_variable_set("@#{key}".to_sym, value) + self.instance_variable_set("@#{key}".to_sym, value) end filter end def filter - @posts = if @min_replies && @topic.posts_count < @min_replies + 1 - [] - else - filter_posts_liked_by_moderators - setup_posts - filter_posts_based_on_trust_level - filter_posts_based_on_score - sort_posts - end + @posts = + if @min_replies && @topic.posts_count < @min_replies + 1 + [] + else + filter_posts_liked_by_moderators + setup_posts + filter_posts_based_on_trust_level + filter_posts_based_on_score + sort_posts + end end private @@ -39,24 +40,26 @@ class FilterBestPosts end def filter_posts_based_on_trust_level - return unless @min_trust_level.try('>',0) - @posts = if @bypass_trust_level_score.try('>',0) - @posts.where('COALESCE(users.trust_level,0) >= ? OR posts.score >= ?', - @min_trust_level, - @bypass_trust_level_score) - else - @posts.where('COALESCE(users.trust_level,0) >= ?', @min_trust_level) - end + return unless @min_trust_level.try('>', 0) + + @posts = + if @bypass_trust_level_score.try('>', 0) + @posts.where('COALESCE(users.trust_level,0) >= ? OR posts.score >= ?', + @min_trust_level, + @bypass_trust_level_score + ) + else + @posts.where('COALESCE(users.trust_level,0) >= ?', @min_trust_level) + end end def filter_posts_based_on_score - return unless @min_score.try('>',0) + return unless @min_score.try('>', 0) @posts = @posts.where('posts.score >= ?', @min_score) end def sort_posts - @posts.to_a.sort!{|a,b| a.post_number <=> b.post_number} + @posts.to_a.sort! { |a, b| a.post_number <=> b.post_number } end end - diff --git a/lib/final_destination.rb b/lib/final_destination.rb index 928253f34f..1365d6f0d5 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -9,8 +9,14 @@ class FinalDestination attr_reader :status attr_reader :cookie - def initialize(url, opts=nil) - @uri = URI(url) rescue nil + def initialize(url, opts = nil) + @url = url + @uri = + begin + URI(escape_url) if @url + rescue URI::InvalidURIError + end + @opts = opts || {} @opts[:max_redirects] ||= 5 @opts[:lookup_ip] ||= lambda do |host| @@ -161,7 +167,7 @@ class FinalDestination address = IPAddr.new(address_s) - if private_ranges.any? {|r| r === address } + if private_ranges.any? { |r| r === address } @status = :invalid_address return false end @@ -176,9 +182,13 @@ class FinalDestination false end + def escape_url + URI.escape(CGI.unescapeHTML(@url), Regexp.new("[^#{URI::PATTERN::UNRESERVED}#{URI::PATTERN::RESERVED}#]")) + end + def private_ranges FinalDestination.standard_private_ranges + - SiteSetting.blacklist_ip_blocks.split('|').map {|r| IPAddr.new(r) rescue nil }.compact + SiteSetting.blacklist_ip_blocks.split('|').map { |r| IPAddr.new(r) rescue nil }.compact end def self.standard_private_ranges diff --git a/lib/flag_query.rb b/lib/flag_query.rb index 11b21c8966..53616e042f 100644 --- a/lib/flag_query.rb +++ b/lib/flag_query.rb @@ -1,6 +1,6 @@ module FlagQuery - def self.flagged_posts_report(current_user, filter, offset=0, per_page=25) + def self.flagged_posts_report(current_user, filter, offset = 0, per_page = 25) actions = flagged_post_actions(filter) guardian = Guardian.new(current_user) @@ -12,11 +12,11 @@ module FlagQuery end post_ids = actions.limit(per_page) - .offset(offset) - .group(:post_id) - .order('MIN(post_actions.created_at) DESC') - .pluck(:post_id) - .uniq + .offset(offset) + .group(:post_id) + .order('MIN(post_actions.created_at) DESC') + .pluck(:post_id) + .uniq return nil if post_ids.blank? @@ -47,8 +47,8 @@ module FlagQuery end post_actions = actions.order('post_actions.created_at DESC') - .includes(related_post: { topic: { ordered_posts: :user }}) - .where(post_id: post_ids) + .includes(related_post: { topic: { ordered_posts: :user } }) + .where(post_id: post_ids) post_actions.each do |pa| post = post_lookup[pa.post_id] @@ -111,10 +111,10 @@ module FlagQuery def self.flagged_post_actions(filter) post_actions = PostAction.flags - .joins("INNER JOIN posts ON posts.id = post_actions.post_id") - .joins("INNER JOIN topics ON topics.id = posts.topic_id") - .joins("LEFT JOIN users ON users.id = posts.user_id") - .where("posts.user_id > 0") + .joins("INNER JOIN posts ON posts.id = post_actions.post_id") + .joins("INNER JOIN topics ON topics.id = posts.topic_id") + .joins("LEFT JOIN users ON users.id = posts.user_id") + .where("posts.user_id > 0") if filter == "old" post_actions.where("post_actions.disagreed_at IS NOT NULL OR @@ -122,8 +122,8 @@ module FlagQuery post_actions.agreed_at IS NOT NULL") else post_actions.active - .where("posts.deleted_at" => nil) - .where("topics.deleted_at" => nil) + .where("posts.deleted_at" => nil) + .where("topics.deleted_at" => nil) end end diff --git a/lib/freedom_patches/active_record_base.rb b/lib/freedom_patches/active_record_base.rb index 4f62d40647..bd4eb2c1e8 100644 --- a/lib/freedom_patches/active_record_base.rb +++ b/lib/freedom_patches/active_record_base.rb @@ -19,13 +19,12 @@ class ActiveRecord::Base ActiveRecord::Base.exec_sql(*args) end - # Executes the given block +retries+ times (or forever, if explicitly given nil), # catching and retrying SQL Deadlock errors. # # Thanks to: http://stackoverflow.com/a/7427186/165668 # - def self.retry_lock_error(retries=5, &block) + def self.retry_lock_error(retries = 5, &block) begin yield rescue ActiveRecord::StatementInvalid => e diff --git a/lib/freedom_patches/ams_include_without_root.rb b/lib/freedom_patches/ams_include_without_root.rb index 424489b3f5..67c4cf185b 100644 --- a/lib/freedom_patches/ams_include_without_root.rb +++ b/lib/freedom_patches/ams_include_without_root.rb @@ -8,7 +8,7 @@ module ActiveModel # This method is copied over verbatim from the AMS version, except for silently # ignoring associations that cannot be embedded without a root instead of # raising an exception. - def include!(name, options={}) + def include!(name, options = {}) unique_values = if hash = options[:hash] if @options[:hash] == hash diff --git a/lib/freedom_patches/better_handlebars_errors.rb b/lib/freedom_patches/better_handlebars_errors.rb index 3cab31cc94..940afe71a2 100644 --- a/lib/freedom_patches/better_handlebars_errors.rb +++ b/lib/freedom_patches/better_handlebars_errors.rb @@ -13,4 +13,3 @@ module Ember end end end - diff --git a/lib/freedom_patches/fast_pluck.rb b/lib/freedom_patches/fast_pluck.rb index ac1203c66d..9eacba972f 100644 --- a/lib/freedom_patches/fast_pluck.rb +++ b/lib/freedom_patches/fast_pluck.rb @@ -64,7 +64,7 @@ class ActiveRecord::Relation columns_hash.key?(cn) ? arel_table[cn] : cn } - conn.select_raw(relation, nil, relation.arel.bind_values + bind_values) do |result,_| + conn.select_raw(relation, nil, relation.arel.bind_values + bind_values) do |result, _| result.type_map = SqlBuilder.pg_type_map result.nfields == 1 ? result.column_values(0) : result.values end diff --git a/lib/freedom_patches/inflector_backport.rb b/lib/freedom_patches/inflector_backport.rb index 299ee1107c..1e192b0033 100644 --- a/lib/freedom_patches/inflector_backport.rb +++ b/lib/freedom_patches/inflector_backport.rb @@ -23,7 +23,7 @@ module ActiveSupport define_method(method_name) do |*args| # this avoids recursive locks found = true - data = cache.fetch(args){found = false} + data = cache.fetch(args) { found = false } unless found cache[args] = data = send(uncached, *args) end @@ -56,8 +56,3 @@ module ActiveSupport end end end - - - - - diff --git a/lib/freedom_patches/pool_drainer.rb b/lib/freedom_patches/pool_drainer.rb index 6ded19de97..bf1eb1e89c 100644 --- a/lib/freedom_patches/pool_drainer.rb +++ b/lib/freedom_patches/pool_drainer.rb @@ -27,7 +27,7 @@ end class ActiveRecord::ConnectionAdapters::ConnectionPool # drain all idle connections # if idle_time is specified only connections idle for N seconds will be drained - def drain(idle_time=nil, max_age=nil) + def drain(idle_time = nil, max_age = nil) synchronize do @available.clear @connections.delete_if do |conn| diff --git a/lib/freedom_patches/rack_patches.rb b/lib/freedom_patches/rack_patches.rb index e02aca52cf..890435d9b9 100644 --- a/lib/freedom_patches/rack_patches.rb +++ b/lib/freedom_patches/rack_patches.rb @@ -4,7 +4,7 @@ class Rack::ETag private def digest_body(body) - parts = [] + parts = [] has_body = false body.each do |part| @@ -28,15 +28,15 @@ end class Rack::ConditionalGet private def to_rfc2822(since) - # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A - # anything shorter is invalid, this avoids exceptions for common cases - # most common being the empty string - if since && since.length >= 16 - # NOTE: there is no trivial way to write this in a non execption way - # _rfc2822 returns a hash but is not that usable - Time.rfc2822(since) rescue nil - else - nil - end + # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A + # anything shorter is invalid, this avoids exceptions for common cases + # most common being the empty string + if since && since.length >= 16 + # NOTE: there is no trivial way to write this in a non execption way + # _rfc2822 returns a hash but is not that usable + Time.rfc2822(since) rescue nil + else + nil + end end end diff --git a/lib/freedom_patches/rails4.rb b/lib/freedom_patches/rails4.rb index 82b4ed9fb9..5052dd8654 100644 --- a/lib/freedom_patches/rails4.rb +++ b/lib/freedom_patches/rails4.rb @@ -1,4 +1,4 @@ -# Sam: This has now forked of rails. Trouble is we would never like to use "about 1 month" ever, we only want months for 2 or more months. +# Sam: This has now forked of rails. Trouble is we would never like to use "about 1 month" ever, we only want months for 2 or more months. # # Backporting a fix to rails itself may get too complex module FreedomPatches @@ -6,7 +6,7 @@ module FreedomPatches def self.distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) options = { - :scope => :'datetime.distance_in_words', + scope: :'datetime.distance_in_words', }.merge!(options) from_time = from_time.to_time if from_time.respond_to?(:to_time) @@ -15,36 +15,36 @@ module FreedomPatches distance_in_minutes = (distance / 60.0).round distance_in_seconds = distance.round - I18n.with_options :locale => options[:locale], :scope => options[:scope] do |locale| + I18n.with_options locale: options[:locale], scope: options[:scope] do |locale| case distance_in_minutes - when 0..1 - return distance_in_minutes == 0 ? - locale.t(:less_than_x_minutes, :count => 1) : - locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds + when 0..1 + return distance_in_minutes == 0 ? + locale.t(:less_than_x_minutes, count: 1) : + locale.t(:x_minutes, count: distance_in_minutes) unless include_seconds case distance_in_seconds - when 0..4 then locale.t :less_than_x_seconds, :count => 5 - when 5..9 then locale.t :less_than_x_seconds, :count => 10 - when 10..19 then locale.t :less_than_x_seconds, :count => 20 - when 20..39 then locale.t :half_a_minute - when 40..59 then locale.t :less_than_x_minutes, :count => 1 - else locale.t :x_minutes, :count => 1 + when 0..4 then locale.t :less_than_x_seconds, count: 5 + when 5..9 then locale.t :less_than_x_seconds, count: 10 + when 10..19 then locale.t :less_than_x_seconds, count: 20 + when 20..39 then locale.t :half_a_minute + when 40..59 then locale.t :less_than_x_minutes, count: 1 + else locale.t :x_minutes, count: 1 end - when 2..44 then locale.t :x_minutes, :count => distance_in_minutes - when 45..89 then locale.t :about_x_hours, :count => 1 - when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round - when 1440..2519 then locale.t :x_days, :count => 1 - + when 2..44 then locale.t :x_minutes, count: distance_in_minutes + when 45..89 then locale.t :about_x_hours, count: 1 + when 90..1439 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round + when 1440..2519 then locale.t :x_days, count: 1 + # this is were we diverge from Rails - when 2520..129599 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round - when 129600..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round + when 2520..129599 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round + when 129600..525599 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round else - fyear = from_time.year + fyear = from_time.year fyear += 1 if from_time.month >= 3 tyear = to_time.year tyear -= 1 if to_time.month < 3 - leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)} + leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } minute_offset_for_leap_year = leap_years * 1440 # Discount the leap year days when calculating year distance. # e.g. if there are 20 leap year days between 2 dates having the same day @@ -55,11 +55,11 @@ module FreedomPatches remainder = (minutes_with_offset % 525600) distance_in_years = (minutes_with_offset / 525600) if remainder < 131400 - locale.t(:about_x_years, :count => distance_in_years) + locale.t(:about_x_years, count: distance_in_years) elsif remainder < 394200 - locale.t(:over_x_years, :count => distance_in_years) + locale.t(:over_x_years, count: distance_in_years) else - locale.t(:almost_x_years, :count => distance_in_years + 1) + locale.t(:almost_x_years, count: distance_in_years + 1) end end end diff --git a/lib/freedom_patches/raw_handlebars.rb b/lib/freedom_patches/raw_handlebars.rb index a3bd47c8a7..3d3a8a84f2 100644 --- a/lib/freedom_patches/raw_handlebars.rb +++ b/lib/freedom_patches/raw_handlebars.rb @@ -54,11 +54,11 @@ end class Ember::Handlebars::Template include Discourse::Ember::Handlebars::Helper - def precompile_handlebars(string, input=nil) + def precompile_handlebars(string, input = nil) "requirejs('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(string)});" end - def compile_handlebars(string, input=nil) + def compile_handlebars(string, input = nil) "requirejs('discourse-common/lib/raw-handlebars').compile(#{indent(string).inspect});" end diff --git a/lib/freedom_patches/safe_buffer.rb b/lib/freedom_patches/safe_buffer.rb index b39a96ebaa..8c8a294bfd 100644 --- a/lib/freedom_patches/safe_buffer.rb +++ b/lib/freedom_patches/safe_buffer.rb @@ -4,7 +4,7 @@ # The alternative is a broken website when this happens class ActiveSupport::SafeBuffer - def concat(value, raise_encoding_err=false) + def concat(value, raise_encoding_err = false) if !html_safe? || value.html_safe? super(value) else @@ -22,8 +22,8 @@ class ActiveSupport::SafeBuffer self.force_encoding("UTF-8") unless valid_encoding? - encode!("utf-16","utf-8",:invalid => :replace) - encode!("utf-8","utf-16") + encode!("utf-16", "utf-8", invalid: :replace) + encode!("utf-8", "utf-16") end Rails.logger.warn("Encountered a non UTF-8 string in SafeBuffer - #{self} - #{encoding_diags}") end @@ -36,7 +36,7 @@ class ActiveSupport::SafeBuffer Rails.logger.warn("Attempted to concat a non UTF-8 string in SafeBuffer - #{value} - #{encoding_diags}") end - concat(value,_raise=true) + concat(value, _raise = true) end end diff --git a/lib/freedom_patches/schema_migration_details.rb b/lib/freedom_patches/schema_migration_details.rb index bbae956c11..2c2722fdc7 100644 --- a/lib/freedom_patches/schema_migration_details.rb +++ b/lib/freedom_patches/schema_migration_details.rb @@ -4,7 +4,7 @@ module FreedomPatches rval = nil time = Benchmark.measure do - rval=super + rval = super end sql = < :replace) - str.encode!("utf-8","utf-16") + str.encode!("utf-16", "utf-8", invalid: :replace) + str.encode!("utf-8", "utf-16") end str diff --git a/lib/freedom_patches/sprockets_patches.rb b/lib/freedom_patches/sprockets_patches.rb index ca83e2aabb..7cbf529aff 100644 --- a/lib/freedom_patches/sprockets_patches.rb +++ b/lib/freedom_patches/sprockets_patches.rb @@ -21,9 +21,9 @@ if Rails.env == "development" end if source[0] != ?/ - # CODE REMOVED - # source = compute_asset_path(source, options) - source = "/assets/#{source}" + # CODE REMOVED + # source = compute_asset_path(source, options) + source = "/assets/#{source}" end relative_url_root = defined?(config.relative_url_root) && config.relative_url_root diff --git a/lib/freedom_patches/translate_accelerator.rb b/lib/freedom_patches/translate_accelerator.rb index ae6c1d48f5..92f0b80b31 100644 --- a/lib/freedom_patches/translate_accelerator.rb +++ b/lib/freedom_patches/translate_accelerator.rb @@ -49,10 +49,10 @@ module I18n end def ensure_all_loaded! - backend.fallbacks(locale).each {|l| ensure_loaded!(l) } + backend.fallbacks(locale).each { |l| ensure_loaded!(l) } end - def search(query, opts=nil) + def search(query, opts = nil) locale = opts[:locale] || config.locale load_locale(locale) unless @loaded_locales.include?(locale) @@ -167,7 +167,7 @@ module I18n alias_method :t, :translate - def exists?(key, locale=nil) + def exists?(key, locale = nil) locale ||= config.locale load_locale(locale) unless @loaded_locales.include?(locale) exists_no_cache?(key, locale) diff --git a/lib/gaps.rb b/lib/gaps.rb index c1f3f89602..0bf52b14fc 100644 --- a/lib/gaps.rb +++ b/lib/gaps.rb @@ -23,7 +23,7 @@ class Gaps end def find_gaps - return if @subset.nil? or @original.nil? + return if @subset.nil? || @original.nil? i = j = 0 gaps = {} @@ -48,7 +48,7 @@ class Gaps break if (i == @subset.size) || (j == @original.size) end - @after[@subset[i-1]] = @original[j..-1] if j < @original.size + @after[@subset[i - 1]] = @original[j..-1] if j < @original.size end end diff --git a/lib/guardian.rb b/lib/guardian.rb index fb092563af..dd75225f82 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -34,7 +34,7 @@ class Guardian attr_accessor :can_see_emails - def initialize(user=nil) + def initialize(user = nil) @user = user.presence || AnonymousUser.new end @@ -91,7 +91,7 @@ class Guardian end end - def can_create?(klass, parent=nil) + def can_create?(klass, parent = nil) return false unless authenticated? && klass # If no parent is provided, we look for a can_create_klass? @@ -155,8 +155,6 @@ class Guardian true end - - # Can we impersonate this user? def can_impersonate?(target) target && @@ -233,7 +231,7 @@ class Guardian is_me?(user) end - def can_invite_to_forum?(groups=nil) + def can_invite_to_forum?(groups = nil) authenticated? && (SiteSetting.max_invites_per_day.to_i > 0 || is_staff?) && !SiteSetting.enable_sso && @@ -242,16 +240,16 @@ class Guardian (!SiteSetting.must_approve_users? && @user.has_trust_level?(TrustLevel[2])) || is_staff? ) && - (groups.blank? || is_admin?) + (groups.blank? || is_admin? || groups.all? { |g| can_edit_group?(g) }) end - def can_invite_to?(object, group_ids=nil) + def can_invite_to?(object, groups = nil) return false unless authenticated? return true if is_admin? return false unless SiteSetting.enable_private_messages? return false if (SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?) return false unless can_see?(object) - return false if group_ids.present? + return false if groups.present? if object.is_a?(Topic) && object.category if object.category.groups.any? @@ -322,7 +320,6 @@ class Guardian end end - private def is_my_own?(obj) diff --git a/lib/guardian/category_guardian.rb b/lib/guardian/category_guardian.rb index 7e67066e89..567fbdbdd9 100644 --- a/lib/guardian/category_guardian.rb +++ b/lib/guardian/category_guardian.rb @@ -2,7 +2,7 @@ module CategoryGuardian # Creating Method - def can_create_category?(parent=nil) + def can_create_category?(parent = nil) is_admin? || ( SiteSetting.allow_moderators_to_create_categories && @@ -34,10 +34,10 @@ module CategoryGuardian if category.topic_count != 0 oldest_topic = category.topics.where.not(id: category.topic_id).order('created_at ASC').limit(1).first if oldest_topic - return I18n.t('category.cannot_delete.topic_exists', {count: category.topic_count, topic_link: "#{oldest_topic.title}"}) + return I18n.t('category.cannot_delete.topic_exists', count: category.topic_count, topic_link: "#{oldest_topic.title}") else # This is a weird case, probably indicating a bug. - return I18n.t('category.cannot_delete.topic_exists_no_oldest', {count: category.topic_count}) + return I18n.t('category.cannot_delete.topic_exists_no_oldest', count: category.topic_count) end end diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 9b8f8ab0c1..da4275650c 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -3,7 +3,7 @@ module PostGuardian # Can the user act on the post in a particular way. # taken_actions = the list of actions the user has already taken - def post_can_act?(post, action_key, opts={}) + def post_can_act?(post, action_key, opts = {}) return false unless can_see_post?(post) @@ -85,7 +85,7 @@ module PostGuardian (!SpamRule::AutoBlock.block?(@user) || (!!parent.try(:private_message?) && parent.allowed_users.include?(@user))) && ( !parent || !parent.category || - Category.post_create_allowed(self).where(:id => parent.category.id).count == 1 + Category.post_create_allowed(self).where(id: parent.category.id).count == 1 ) end @@ -186,8 +186,8 @@ module PostGuardian can_see_post?(post) end - def can_vote?(post, opts={}) - post_can_act?(post,:vote, opts) + def can_vote?(post, opts = {}) + post_can_act?(post, :vote, opts) end def can_change_post_owner? diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index add956351b..8b8f08a12d 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -76,7 +76,7 @@ module TopicGuardian is_staff? end - def can_see_topic?(topic, hide_deleted=true) + def can_see_topic?(topic, hide_deleted = true) return false unless topic return true if is_admin? return false if hide_deleted && topic.deleted_at && !can_see_deleted_topics? @@ -107,6 +107,6 @@ module TopicGuardian def can_edit_featured_link?(category_id) return false unless SiteSetting.topic_featured_link_enabled - Category.where(id: category_id||SiteSetting.uncategorized_category_id, topic_featured_link_allowed: true).exists? + Category.where(id: category_id || SiteSetting.uncategorized_category_id, topic_featured_link_allowed: true).exists? end end diff --git a/lib/html_normalize.rb b/lib/html_normalize.rb index 072789c0cb..9b5ebb5d58 100644 --- a/lib/html_normalize.rb +++ b/lib/html_normalize.rb @@ -51,7 +51,7 @@ class HtmlNormalize Oga::XML::Text === node || !BLOCK.include?(node.name.downcase) end - def dump_node(node, indent=0, buffer) + def dump_node(node, indent = 0, buffer) if Oga::XML::Text === node if node.parent&.name @@ -70,7 +70,7 @@ class HtmlNormalize attrs = node&.attributes if (attrs && attrs.length > 0) - attrs.sort!{|x,y| x.name <=> y.name} + attrs.sort! { |x, y| x.name <=> y.name } attrs.each do |a| buffer << " " buffer << a.name @@ -94,20 +94,20 @@ class HtmlNormalize children&.each do |child| if block && inline?(child) inline_buffer ||= String.new - dump_node(child, indent+1, inline_buffer) + dump_node(child, indent + 1, inline_buffer) else if inline_buffer - buffer << " " * (indent+1) * 2 + buffer << " " * (indent + 1) * 2 buffer << inline_buffer.strip inline_buffer = nil else - dump_node(child, indent+1, buffer) + dump_node(child, indent + 1, buffer) end end end if inline_buffer - buffer << " " * (indent+1) * 2 + buffer << " " * (indent + 1) * 2 buffer << inline_buffer.strip inline_buffer = nil end @@ -146,5 +146,4 @@ class HtmlNormalize nodes[start...finish] end - end diff --git a/lib/html_prettify.rb b/lib/html_prettify.rb index 39a8ca7d30..df91d767d3 100644 --- a/lib/html_prettify.rb +++ b/lib/html_prettify.rb @@ -12,7 +12,7 @@ class HtmlPrettify < String new(html).to_html end - # Create a new RubyPants instance with the text in +string+. + # Create a new RubyPants instance with the text in +string+. # # Allowed elements in the options array: # @@ -50,7 +50,7 @@ class HtmlPrettify < String # :ellipsis :: # :html_quote :: " # - def initialize(string, options=[2], entities = {}) + def initialize(string, options = [2], entities = {}) super string @options = [*options] @@ -173,7 +173,6 @@ class HtmlPrettify < String protected - # The string, with each instance of "--" translated to an # em-dash HTML entity. # @@ -319,14 +318,14 @@ class HtmlPrettify < String new_str = str.dup { - :en_dash => '-', - :em_dash => '--', - :single_left_quote => "'", - :single_right_quote => "'", - :double_left_quote => '"', - :double_right_quote => '"', - :ellipsis => '...' - }.each do |k,v| + en_dash: '-', + em_dash: '--', + single_left_quote: "'", + single_right_quote: "'", + double_left_quote: '"', + double_right_quote: '"', + ellipsis: '...' + }.each do |k, v| new_str.gsub!(/#{entity(k)}/, v) end diff --git a/lib/html_to_markdown.rb b/lib/html_to_markdown.rb index e536047740..21a3cded14 100644 --- a/lib/html_to_markdown.rb +++ b/lib/html_to_markdown.rb @@ -3,24 +3,23 @@ require "nokogiri" class HtmlToMarkdown class Block < Struct.new(:name, :head, :body, :opened, :markdown) - def initialize(name, head="", body="", opened=false, markdown=""); super; end + def initialize(name, head = "", body = "", opened = false, markdown = ""); super; end end - def initialize(html, opts={}) + def initialize(html, opts = {}) @opts = opts || {} @doc = fix_span_elements(Nokogiri::HTML(html)) remove_whitespaces! end - # If a `
    ` is within a `` that's invalid, so let's hoist the `
    ` up def fix_span_elements(node) if node.name == 'span' && node.at('div') node.swap(node.children) end - node.children.each {|c| fix_span_elements(c)} + node.children.each { |c| fix_span_elements(c) } node end diff --git a/lib/i18n/backend/discourse_i18n.rb b/lib/i18n/backend/discourse_i18n.rb index ee133d5066..65a090cd26 100644 --- a/lib/i18n/backend/discourse_i18n.rb +++ b/lib/i18n/backend/discourse_i18n.rb @@ -51,7 +51,7 @@ module I18n protected - def find_results(regexp, results, translations, path=nil) + def find_results(regexp, results, translations, path = nil) return results if translations.blank? translations.each do |k_sym, v| @@ -72,11 +72,12 @@ module I18n # the original translations before applying our overrides. def lookup(locale, key, scope = [], options = {}) existing_translations = super(locale, key, scope, options) + return existing_translations if scope.is_a?(Array) && scope.include?(:models) + overrides = options.dig(:overrides, locale) - if overrides && existing_translations - if options[:count] - + if overrides + if existing_translations && options[:count] remapped_translations = if existing_translations.is_a?(Hash) Hash[existing_translations.map { |k, v| ["#{key}.#{k}", v] }] diff --git a/lib/import/normalize.rb b/lib/import/normalize.rb index 79dfb255f8..5279439dc4 100644 --- a/lib/import/normalize.rb +++ b/lib/import/normalize.rb @@ -4,10 +4,10 @@ require 'htmlentities' module Import; end module Import::Normalize - def self.normalize_code_blocks(code, lang=nil) + def self.normalize_code_blocks(code, lang = nil) coder = HTMLEntities.new code.gsub(/
    \s*\n?(.*?)\n?<\/code>\s*<\/pre>/m) {
    -        "\n```#{lang}\n#{coder.decode($1)}\n```\n"
    +      "\n```#{lang}\n#{coder.decode($1)}\n```\n"
         }
       end
     end
    diff --git a/lib/import_export/category_exporter.rb b/lib/import_export/category_exporter.rb
    index 9f182e7247..4e2a57e820 100644
    --- a/lib/import_export/category_exporter.rb
    +++ b/lib/import_export/category_exporter.rb
    @@ -22,15 +22,14 @@ module ImportExport
           self
         end
     
    -
         CATEGORY_ATTRS = [:id, :name, :color, :created_at, :user_id, :slug, :description, :text_color,
                           :auto_close_hours, :auto_close_based_on_last_post,
                           :topic_template, :suppress_from_homepage, :all_topics_wiki, :permissions_params]
     
         def export_categories
    -      @export_data[:category] = CATEGORY_ATTRS.inject({}) { |h,a| h[a] = @category.send(a); h }
    +      @export_data[:category] = CATEGORY_ATTRS.inject({}) { |h, a| h[a] = @category.send(a); h }
           @subcategories.find_each do |subcat|
    -        @export_data[:subcategories] << CATEGORY_ATTRS.inject({}) { |h,a| h[a] = subcat.send(a); h }
    +        @export_data[:subcategories] << CATEGORY_ATTRS.inject({}) { |h, a| h[a] = subcat.send(a); h }
           end
     
           # export groups that are mentioned in category permissions
    @@ -49,7 +48,6 @@ module ImportExport
           self
         end
     
    -
         GROUP_ATTRS = [ :id, :name, :created_at, :alias_level, :visible,
                         :automatic_membership_email_domains, :automatic_membership_retroactive,
                         :primary_group, :title, :grant_trust_level, :incoming_email]
    @@ -57,7 +55,7 @@ module ImportExport
         def export_groups(group_names)
           group_names.each do |name|
             group = Group.find_by_name(name)
    -        group_attrs = GROUP_ATTRS.inject({}) { |h,a| h[a] = group.send(a); h }
    +        group_attrs = GROUP_ATTRS.inject({}) { |h, a| h[a] = group.send(a); h }
             group_attrs[:user_ids] = group.users.pluck(:id)
             @export_data[:groups] << group_attrs
           end
    @@ -75,7 +73,7 @@ module ImportExport
           self
         end
     
    -    def save_to_file(filename=nil)
    +    def save_to_file(filename = nil)
           require 'json'
           output_basename = filename || File.join("category-export-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json")
           File.open(output_basename, "w:UTF-8") do |f|
    diff --git a/lib/import_export/category_importer.rb b/lib/import_export/category_importer.rb
    index 25b57acfcb..8493b006a0 100644
    --- a/lib/import_export/category_importer.rb
    +++ b/lib/import_export/category_importer.rb
    @@ -28,7 +28,7 @@ module ImportExport
             external_id = g.delete(:id)
             new_group = Group.find_by_name(g[:name]) || Group.create!(g)
             user_ids.each do |external_user_id|
    -          new_group.add( User.find(@topic_importer.new_user_id(external_user_id)) ) rescue ActiveRecord::RecordNotUnique
    +          new_group.add(User.find(@topic_importer.new_user_id(external_user_id))) rescue ActiveRecord::RecordNotUnique
             end
           end
         end
    @@ -47,7 +47,7 @@ module ImportExport
             parent = Category.new(@export_data[:category])
             parent.user_id = @topic_importer.new_user_id(@export_data[:category][:user_id]) # imported user's new id
             parent.custom_fields["import_id"] = id
    -        parent.permissions = permissions.present? ? permissions : {"everyone" => CategoryGroup.permission_types[:full]}
    +        parent.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] }
             parent.save!
             set_category_description(parent, @export_data[:category][:description])
           end
    @@ -62,7 +62,7 @@ module ImportExport
               subcategory.parent_category_id = parent.id
               subcategory.user_id = @topic_importer.new_user_id(cat_attrs[:user_id])
               subcategory.custom_fields["import_id"] = id
    -          subcategory.permissions = permissions.present? ? permissions : {"everyone" => CategoryGroup.permission_types[:full]}
    +          subcategory.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] }
               subcategory.save!
               set_category_description(subcategory, cat_attrs[:description])
             end
    diff --git a/lib/import_export/import_export.rb b/lib/import_export/import_export.rb
    index 4bb6fa9250..fad560768f 100644
    --- a/lib/import_export/import_export.rb
    +++ b/lib/import_export/import_export.rb
    @@ -6,7 +6,7 @@ require "json"
     
     module ImportExport
     
    -  def self.export_category(category_id, filename=nil)
    +  def self.export_category(category_id, filename = nil)
         ImportExport::CategoryExporter.new(category_id).perform.save_to_file(filename)
       end
     
    diff --git a/lib/import_export/topic_exporter.rb b/lib/import_export/topic_exporter.rb
    index 78b4dc4d9e..4c494003ed 100644
    --- a/lib/import_export/topic_exporter.rb
    +++ b/lib/import_export/topic_exporter.rb
    @@ -20,7 +20,6 @@ module ImportExport
           self
         end
     
    -
         USER_ATTRS = [:id, :email, :username, :name, :created_at, :trust_level, :active, :last_emailed_at]
     
         def export_users
    @@ -33,11 +32,9 @@ module ImportExport
               u = post.user
               unless @exported_user_ids.include?(u.id)
                 x = USER_ATTRS.inject({}) { |h, a| h[a] = u.send(a); h; }
    -            @export_data[:users] << x.merge({
    -              bio_raw: u.user_profile.bio_raw,
    -              website: u.user_profile.website,
    -              location: u.user_profile.location
    -            })
    +            @export_data[:users] << x.merge(bio_raw: u.user_profile.bio_raw,
    +                                            website: u.user_profile.website,
    +                                            location: u.user_profile.location)
                 @exported_user_ids << u.id
               end
             end
    @@ -46,7 +43,6 @@ module ImportExport
           self
         end
     
    -
         def export_topics
           @topic_ids.each do |topic_id|
             t = Topic.find(topic_id)
    @@ -56,7 +52,6 @@ module ImportExport
           puts ""
         end
     
    -
         TOPIC_ATTRS = [:id, :title, :created_at, :views, :category_id, :closed, :archived, :archetype]
         POST_ATTRS = [:id, :user_id, :post_number, :raw, :created_at, :reply_to_post_number,
                       :hidden, :hidden_reason_id, :wiki]
    @@ -81,8 +76,7 @@ module ImportExport
           self
         end
     
    -
    -    def save_to_file(filename=nil)
    +    def save_to_file(filename = nil)
           require 'json'
           output_basename = filename || File.join("topic-export-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json")
           File.open(output_basename, "w:UTF-8") do |f|
    diff --git a/lib/import_export/topic_importer.rb b/lib/import_export/topic_importer.rb
    index e7fc2d8f36..c681d39c2f 100644
    --- a/lib/import_export/topic_importer.rb
    +++ b/lib/import_export/topic_importer.rb
    @@ -18,7 +18,7 @@ module ImportExport
     
         def import_users
           @export_data[:users].each do |u|
    -        existing = User.where(email: u[:email]).first
    +        existing = User.with_email(email: u[:email]).first
             if existing
               if existing.custom_fields["import_id"] != u[:id]
                 existing.custom_fields["import_id"] = u[:id]
    @@ -36,14 +36,14 @@ module ImportExport
             puts ""
             print t[:title]
     
    -        first_post_attrs = t[:posts].first.merge( t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id])) )
    +        first_post_attrs = t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id])))
             first_post_attrs[:user_id] = new_user_id(first_post_attrs[:user_id])
             first_post_attrs[:category] = new_category_id(t[:category_id])
     
             first_post = PostCustomField.where(name: "import_id", value: first_post_attrs[:id]).first.try(:post)
     
             unless first_post
    -          first_post = create_post( first_post_attrs, first_post_attrs[:id] )
    +          first_post = create_post(first_post_attrs, first_post_attrs[:id])
             end
     
             topic_id = first_post.topic_id
    @@ -53,10 +53,8 @@ module ImportExport
               print "."
               existing = PostCustomField.where(name: "import_id", value: post_data[:id]).first.try(:post)
               unless existing
    -            create_post(post_data.merge({
    -              topic_id: topic_id,
    -              user_id: new_user_id(post_data[:user_id])
    -            }), post_data[:id]) # see ImportScripts::Base
    +            create_post(post_data.merge(topic_id: topic_id,
    +                                        user_id: new_user_id(post_data[:user_id])), post_data[:id]) # see ImportScripts::Base
               end
             end
           end
    diff --git a/lib/inline_oneboxer.rb b/lib/inline_oneboxer.rb
    new file mode 100644
    index 0000000000..f345bef4d3
    --- /dev/null
    +++ b/lib/inline_oneboxer.rb
    @@ -0,0 +1,72 @@
    +require_dependency 'retrieve_title'
    +
    +class InlineOneboxer
    +
    +  def initialize(urls, opts = nil)
    +    @urls = urls
    +    @opts = opts || {}
    +  end
    +
    +  def process
    +    @urls.map { |url| InlineOneboxer.lookup(url, @opts) }.compact
    +  end
    +
    +  def self.purge(url)
    +    Rails.cache.delete(cache_key(url))
    +  end
    +
    +  def self.cache_lookup(url)
    +    Rails.cache.read(cache_key(url))
    +  end
    +
    +  def self.lookup(url, opts = nil)
    +    opts ||= {}
    +
    +    unless opts[:skip_cache]
    +      cached = cache_lookup(url)
    +      return cached if cached.present?
    +    end
    +
    +    if route = Discourse.route_for(url)
    +      if route[:controller] == "topics" &&
    +        route[:action] == "show" &&
    +        topic = (Topic.where(id: route[:topic_id].to_i).first rescue nil)
    +
    +        return onebox_for(url, topic.title, opts) if Guardian.new.can_see?(topic)
    +      end
    +    end
    +
    +    if whitelist = SiteSetting.inline_onebox_domains_whitelist
    +      uri = URI(url) rescue nil
    +
    +      domains = whitelist.split('|')
    +      if uri.present? &&
    +        uri.hostname.present? &&
    +        domains.include?(uri.hostname) &&
    +        title = RetrieveTitle.crawl(url)
    +        return onebox_for(url, title, opts)
    +      end
    +    end
    +
    +    nil
    +  end
    +
    +  private
    +
    +    def self.onebox_for(url, title, opts)
    +      onebox = {
    +        url: url,
    +        title: Emoji.gsub_emoji_to_unicode(title)
    +      }
    +      unless opts[:skip_cache]
    +        Rails.cache.write(cache_key(url), onebox, expires_in: 1.day)
    +      end
    +
    +      onebox
    +    end
    +
    +    def self.cache_key(url)
    +      "inline_onebox:#{url}"
    +    end
    +
    +end
    diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb
    index 44a2fe537e..c352ee42d7 100644
    --- a/lib/js_locale_helper.rb
    +++ b/lib/js_locale_helper.rb
    @@ -16,7 +16,7 @@ module JsLocaleHelper
         end
       end
     
    -  def self.load_translations(locale, opts=nil)
    +  def self.load_translations(locale, opts = nil)
         opts ||= {}
     
         @loaded_translations = nil if opts[:force]
    @@ -149,7 +149,7 @@ module JsLocaleHelper
       end
     
       def self.generate_message_format(message_formats, locale_str)
    -    formats = message_formats.map { |k,v| k.inspect << " : " << compile_message_format(locale_str, v) }.join(", ")
    +    formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(locale_str, v) }.join(", ")
     
         filename = "#{Rails.root}/lib/javascripts/locale/#{locale_str}.js"
         filename = "#{Rails.root}/lib/javascripts/locale/en.js" unless File.exists?(filename)
    @@ -161,6 +161,7 @@ module JsLocaleHelper
       end
     
       def self.reset_context
    +    @ctx&.dispose
         @ctx = nil
       end
     
    diff --git a/lib/json_error.rb b/lib/json_error.rb
    index e52f18edf4..5dfc720506 100644
    --- a/lib/json_error.rb
    +++ b/lib/json_error.rb
    @@ -1,6 +1,6 @@
     module JsonError
     
    -  def create_errors_json(obj, type=nil)
    +  def create_errors_json(obj, type = nil)
         errors = create_errors_array obj
         errors[:error_type] = type if type
         errors
    diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb
    index 62915edcab..93766c4b2d 100644
    --- a/lib/letter_avatar.rb
    +++ b/lib/letter_avatar.rb
    @@ -89,7 +89,7 @@ class LetterAvatar
         end
     
         def to_rgb(color)
    -      r,g,b = color
    +      r, g, b = color
           "rgb(#{r},#{g},#{b})"
         end
     
    @@ -108,7 +108,7 @@ class LetterAvatar
           skip = File.basename(cache_path)
           parent_path = File.dirname(cache_path)
           Dir.entries(parent_path).each do |path|
    -        unless ['.','..'].include?(path) || path == skip
    +        unless ['.', '..'].include?(path) || path == skip
               FileUtils.rm_rf(parent_path + "/" + path)
             end
           end
    @@ -121,220 +121,220 @@ class LetterAvatar
       #   - H: 0 - 360
       #   - C: 0 - 2
       #   - L: 0.75 - 1.5
    -  COLORS = [[198,125,40],
    -      [61,155,243],
    -      [74,243,75],
    -      [238,89,166],
    -      [52,240,224],
    -      [177,156,155],
    -      [240,120,145],
    -      [111,154,78],
    -      [237,179,245],
    -      [237,101,95],
    -      [89,239,155],
    -      [43,254,70],
    -      [163,212,245],
    -      [65,152,142],
    -      [165,135,246],
    -      [181,166,38],
    -      [187,229,206],
    -      [77,164,25],
    -      [179,246,101],
    -      [234,93,37],
    -      [225,155,115],
    -      [142,140,188],
    -      [223,120,140],
    -      [249,174,27],
    -      [244,117,225],
    -      [137,141,102],
    -      [75,191,146],
    -      [188,239,142],
    -      [164,199,145],
    -      [173,120,149],
    -      [59,195,89],
    -      [222,198,220],
    -      [68,145,187],
    -      [236,204,179],
    -      [159,195,72],
    -      [188,121,189],
    -      [166,160,85],
    -      [181,233,37],
    -      [236,177,85],
    -      [121,147,160],
    -      [234,218,110],
    -      [241,157,191],
    -      [62,200,234],
    -      [133,243,34],
    -      [88,149,110],
    -      [59,228,248],
    -      [183,119,118],
    -      [251,195,45],
    -      [113,196,122],
    -      [197,115,70],
    -      [80,175,187],
    -      [103,231,238],
    -      [240,72,133],
    -      [228,149,241],
    -      [180,188,159],
    -      [172,132,85],
    -      [180,135,251],
    -      [236,194,58],
    -      [217,176,109],
    -      [88,244,199],
    -      [186,157,239],
    -      [113,230,96],
    -      [206,115,165],
    -      [244,178,163],
    -      [230,139,26],
    -      [241,125,89],
    -      [83,160,66],
    -      [107,190,166],
    -      [197,161,210],
    -      [198,203,245],
    -      [238,117,19],
    -      [228,119,116],
    -      [131,156,41],
    -      [145,178,168],
    -      [139,170,220],
    -      [233,95,125],
    -      [87,178,230],
    -      [157,200,119],
    -      [237,140,76],
    -      [229,185,186],
    -      [144,206,212],
    -      [236,209,158],
    -      [185,189,79],
    -      [34,208,66],
    -      [84,238,129],
    -      [133,140,134],
    -      [67,157,94],
    -      [168,179,25],
    -      [140,145,240],
    -      [151,241,125],
    -      [67,162,107],
    -      [200,156,21],
    -      [169,173,189],
    -      [226,116,189],
    -      [133,231,191],
    -      [194,161,63],
    -      [241,77,99],
    -      [241,217,53],
    -      [123,204,105],
    -      [210,201,119],
    -      [229,108,155],
    -      [240,91,72],
    -      [187,115,210],
    -      [240,163,100],
    -      [178,217,57],
    -      [179,135,116],
    -      [204,211,24],
    -      [186,135,57],
    -      [223,176,135],
    -      [204,148,151],
    -      [116,223,50],
    -      [95,195,46],
    -      [123,160,236],
    -      [181,172,131],
    -      [142,220,202],
    -      [240,140,112],
    -      [172,145,164],
    -      [228,124,45],
    -      [135,151,243],
    -      [42,205,125],
    -      [192,233,116],
    -      [119,170,114],
    -      [158,138,26],
    -      [73,190,183],
    -      [185,229,243],
    -      [227,107,55],
    -      [196,205,202],
    -      [132,143,60],
    -      [233,192,237],
    -      [62,150,220],
    -      [205,201,141],
    -      [106,140,190],
    -      [161,131,205],
    -      [135,134,158],
    -      [198,139,81],
    -      [115,171,32],
    -      [101,181,67],
    -      [149,137,119],
    -      [37,142,183],
    -      [183,130,175],
    -      [168,125,133],
    -      [124,142,87],
    -      [236,156,171],
    -      [232,194,91],
    -      [219,200,69],
    -      [144,219,34],
    -      [219,95,187],
    -      [145,154,217],
    -      [165,185,100],
    -      [127,238,163],
    -      [224,178,198],
    -      [119,153,120],
    -      [124,212,92],
    -      [172,161,105],
    -      [231,155,135],
    -      [157,132,101],
    -      [122,185,146],
    -      [53,166,51],
    -      [70,163,90],
    -      [150,190,213],
    -      [210,107,60],
    -      [166,152,185],
    -      [159,194,159],
    -      [39,141,222],
    -      [202,176,161],
    -      [95,140,229],
    -      [168,142,87],
    -      [93,170,203],
    -      [159,142,54],
    -      [14,168,39],
    -      [94,150,149],
    -      [187,206,136],
    -      [157,224,166],
    -      [235,158,208],
    -      [109,232,216],
    -      [141,201,87],
    -      [208,124,118],
    -      [142,125,214],
    -      [19,237,174],
    -      [72,219,41],
    -      [234,102,111],
    -      [168,142,79],
    -      [188,135,35],
    -      [95,155,143],
    -      [148,173,116],
    -      [223,112,95],
    -      [228,128,236],
    -      [206,114,54],
    -      [195,119,88],
    -      [235,140,94],
    -      [235,202,125],
    -      [233,155,153],
    -      [214,214,238],
    -      [246,200,35],
    -      [151,125,171],
    -      [132,145,172],
    -      [131,142,118],
    -      [199,126,150],
    -      [61,162,123],
    -      [58,176,151],
    -      [215,141,69],
    -      [225,154,220],
    -      [220,77,167],
    -      [233,161,64],
    -      [130,221,137],
    -      [81,191,129],
    -      [169,162,140],
    -      [174,177,222],
    -      [236,174,47],
    -      [233,188,180],
    -      [69,222,172],
    -      [71,232,93],
    -      [118,211,238],
    -      [157,224,83],
    -      [218,105,73],
    -      [126,169,36]]
    +  COLORS = [[198, 125, 40],
    +      [61, 155, 243],
    +      [74, 243, 75],
    +      [238, 89, 166],
    +      [52, 240, 224],
    +      [177, 156, 155],
    +      [240, 120, 145],
    +      [111, 154, 78],
    +      [237, 179, 245],
    +      [237, 101, 95],
    +      [89, 239, 155],
    +      [43, 254, 70],
    +      [163, 212, 245],
    +      [65, 152, 142],
    +      [165, 135, 246],
    +      [181, 166, 38],
    +      [187, 229, 206],
    +      [77, 164, 25],
    +      [179, 246, 101],
    +      [234, 93, 37],
    +      [225, 155, 115],
    +      [142, 140, 188],
    +      [223, 120, 140],
    +      [249, 174, 27],
    +      [244, 117, 225],
    +      [137, 141, 102],
    +      [75, 191, 146],
    +      [188, 239, 142],
    +      [164, 199, 145],
    +      [173, 120, 149],
    +      [59, 195, 89],
    +      [222, 198, 220],
    +      [68, 145, 187],
    +      [236, 204, 179],
    +      [159, 195, 72],
    +      [188, 121, 189],
    +      [166, 160, 85],
    +      [181, 233, 37],
    +      [236, 177, 85],
    +      [121, 147, 160],
    +      [234, 218, 110],
    +      [241, 157, 191],
    +      [62, 200, 234],
    +      [133, 243, 34],
    +      [88, 149, 110],
    +      [59, 228, 248],
    +      [183, 119, 118],
    +      [251, 195, 45],
    +      [113, 196, 122],
    +      [197, 115, 70],
    +      [80, 175, 187],
    +      [103, 231, 238],
    +      [240, 72, 133],
    +      [228, 149, 241],
    +      [180, 188, 159],
    +      [172, 132, 85],
    +      [180, 135, 251],
    +      [236, 194, 58],
    +      [217, 176, 109],
    +      [88, 244, 199],
    +      [186, 157, 239],
    +      [113, 230, 96],
    +      [206, 115, 165],
    +      [244, 178, 163],
    +      [230, 139, 26],
    +      [241, 125, 89],
    +      [83, 160, 66],
    +      [107, 190, 166],
    +      [197, 161, 210],
    +      [198, 203, 245],
    +      [238, 117, 19],
    +      [228, 119, 116],
    +      [131, 156, 41],
    +      [145, 178, 168],
    +      [139, 170, 220],
    +      [233, 95, 125],
    +      [87, 178, 230],
    +      [157, 200, 119],
    +      [237, 140, 76],
    +      [229, 185, 186],
    +      [144, 206, 212],
    +      [236, 209, 158],
    +      [185, 189, 79],
    +      [34, 208, 66],
    +      [84, 238, 129],
    +      [133, 140, 134],
    +      [67, 157, 94],
    +      [168, 179, 25],
    +      [140, 145, 240],
    +      [151, 241, 125],
    +      [67, 162, 107],
    +      [200, 156, 21],
    +      [169, 173, 189],
    +      [226, 116, 189],
    +      [133, 231, 191],
    +      [194, 161, 63],
    +      [241, 77, 99],
    +      [241, 217, 53],
    +      [123, 204, 105],
    +      [210, 201, 119],
    +      [229, 108, 155],
    +      [240, 91, 72],
    +      [187, 115, 210],
    +      [240, 163, 100],
    +      [178, 217, 57],
    +      [179, 135, 116],
    +      [204, 211, 24],
    +      [186, 135, 57],
    +      [223, 176, 135],
    +      [204, 148, 151],
    +      [116, 223, 50],
    +      [95, 195, 46],
    +      [123, 160, 236],
    +      [181, 172, 131],
    +      [142, 220, 202],
    +      [240, 140, 112],
    +      [172, 145, 164],
    +      [228, 124, 45],
    +      [135, 151, 243],
    +      [42, 205, 125],
    +      [192, 233, 116],
    +      [119, 170, 114],
    +      [158, 138, 26],
    +      [73, 190, 183],
    +      [185, 229, 243],
    +      [227, 107, 55],
    +      [196, 205, 202],
    +      [132, 143, 60],
    +      [233, 192, 237],
    +      [62, 150, 220],
    +      [205, 201, 141],
    +      [106, 140, 190],
    +      [161, 131, 205],
    +      [135, 134, 158],
    +      [198, 139, 81],
    +      [115, 171, 32],
    +      [101, 181, 67],
    +      [149, 137, 119],
    +      [37, 142, 183],
    +      [183, 130, 175],
    +      [168, 125, 133],
    +      [124, 142, 87],
    +      [236, 156, 171],
    +      [232, 194, 91],
    +      [219, 200, 69],
    +      [144, 219, 34],
    +      [219, 95, 187],
    +      [145, 154, 217],
    +      [165, 185, 100],
    +      [127, 238, 163],
    +      [224, 178, 198],
    +      [119, 153, 120],
    +      [124, 212, 92],
    +      [172, 161, 105],
    +      [231, 155, 135],
    +      [157, 132, 101],
    +      [122, 185, 146],
    +      [53, 166, 51],
    +      [70, 163, 90],
    +      [150, 190, 213],
    +      [210, 107, 60],
    +      [166, 152, 185],
    +      [159, 194, 159],
    +      [39, 141, 222],
    +      [202, 176, 161],
    +      [95, 140, 229],
    +      [168, 142, 87],
    +      [93, 170, 203],
    +      [159, 142, 54],
    +      [14, 168, 39],
    +      [94, 150, 149],
    +      [187, 206, 136],
    +      [157, 224, 166],
    +      [235, 158, 208],
    +      [109, 232, 216],
    +      [141, 201, 87],
    +      [208, 124, 118],
    +      [142, 125, 214],
    +      [19, 237, 174],
    +      [72, 219, 41],
    +      [234, 102, 111],
    +      [168, 142, 79],
    +      [188, 135, 35],
    +      [95, 155, 143],
    +      [148, 173, 116],
    +      [223, 112, 95],
    +      [228, 128, 236],
    +      [206, 114, 54],
    +      [195, 119, 88],
    +      [235, 140, 94],
    +      [235, 202, 125],
    +      [233, 155, 153],
    +      [214, 214, 238],
    +      [246, 200, 35],
    +      [151, 125, 171],
    +      [132, 145, 172],
    +      [131, 142, 118],
    +      [199, 126, 150],
    +      [61, 162, 123],
    +      [58, 176, 151],
    +      [215, 141, 69],
    +      [225, 154, 220],
    +      [220, 77, 167],
    +      [233, 161, 64],
    +      [130, 221, 137],
    +      [81, 191, 129],
    +      [169, 162, 140],
    +      [174, 177, 222],
    +      [236, 174, 47],
    +      [233, 188, 180],
    +      [69, 222, 172],
    +      [71, 232, 93],
    +      [118, 211, 238],
    +      [157, 224, 83],
    +      [218, 105, 73],
    +      [126, 169, 36]]
     end
    diff --git a/lib/markdown_linker.rb b/lib/markdown_linker.rb
    index b6bb1a2eb1..fab7c5a6b6 100644
    --- a/lib/markdown_linker.rb
    +++ b/lib/markdown_linker.rb
    @@ -17,7 +17,7 @@ class MarkdownLinker
     
       def references
         result = ""
    -    (@rendered..@index-1).each do |i|
    +    (@rendered..@index - 1).each do |i|
           result << "[#{i}]: #{@markdown_links[i]}\n"
         end
         @rendered = @index
    diff --git a/lib/memory_diagnostics.rb b/lib/memory_diagnostics.rb
    index a49480677a..3a3fcd60f5 100644
    --- a/lib/memory_diagnostics.rb
    +++ b/lib/memory_diagnostics.rb
    @@ -4,7 +4,7 @@ module MemoryDiagnostics
         File.exists?(snapshot_filename)
       end
     
    -  def self.compare(from=nil, to=nil)
    +  def self.compare(from = nil, to = nil)
     
         from ||= snapshot_filename
         if !to
    @@ -38,7 +38,7 @@ module MemoryDiagnostics
           end
         end
     
    -    report << summary.sort{|a,b| b[1] <=> a[1]}[0..50].map{|k,v|
    +    report << summary.sort { |a, b| b[1] <=> a[1] }[0..50].map { |k, v|
           "#{k}: #{v}"
         }.join("\n")
     
    @@ -59,9 +59,9 @@ module MemoryDiagnostics
         "#{snapshot_path}/#{Process.pid}.snapshot"
       end
     
    -  def self.snapshot_current_process(filename=nil)
    +  def self.snapshot_current_process(filename = nil)
         filename ||= snapshot_filename
    -    pid=fork do
    +    pid = fork do
           snapshot(filename)
         end
     
    @@ -86,7 +86,7 @@ module MemoryDiagnostics
         IO.binwrite(filename, Marshal::dump(object_ids))
       end
     
    -  def self.memory_report(opts={})
    +  def self.memory_report(opts = {})
         begin
           # ruby 2.1
           GC.start(full_mark: true)
    @@ -94,7 +94,6 @@ module MemoryDiagnostics
           GC.start
         end
     
    -
         classes = {}
         large_objects = []
     
    @@ -113,11 +112,11 @@ module MemoryDiagnostics
               classes[:unknown] += 1
             end
           end
    -      classes = classes.sort{|a,b| b[1] <=> a[1]}[0..40].map{|klass, count| "#{klass}: #{count}"}
    +      classes = classes.sort { |a, b| b[1] <=> a[1] }[0..40].map { |klass, count| "#{klass}: #{count}" }
     
    -      classes << "\nLarge Objects (#{large_objects.length} larger than 200 bytes total size #{large_objects.map{|x,_| x}.sum}):\n"
    +      classes << "\nLarge Objects (#{large_objects.length} larger than 200 bytes total size #{large_objects.map { |x, _| x }.sum}):\n"
     
    -      classes += large_objects.sort{|a,b| b[0] <=> a[0]}[0..800].map do |size,object|
    +      classes += large_objects.sort { |a, b| b[0] <=> a[0] }[0..800].map do |size, object|
             rval = "#{object.class}: size #{size}"
             rval << " " << object.to_s[0..500].gsub("\n", "") if (String === object) || (Regexp === object)
             rval << "\n"
    @@ -125,10 +124,8 @@ module MemoryDiagnostics
           end
         end
     
    -    stats = GC.stat.map{|k,v| "#{k}: #{v}"}
    -    counts = ObjectSpace.count_objects.sort{|a,b| b[1] <=> a[1] }.map{|k,v| "#{k}: #{v}"}
    -
    -
    +    stats = GC.stat.map { |k, v| "#{k}: #{v}" }
    +    counts = ObjectSpace.count_objects.sort { |a, b| b[1] <=> a[1] }.map { |k, v| "#{k}: #{v}" }
     
         < 'image/png' }, [ File.read(default_image)] ]
             end
           end
    -      
    +
           status, headers, response = @app.call(env)
           [status, headers, response]
         end
    diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb
    index 90b36890d0..a2dae84e97 100644
    --- a/lib/middleware/request_tracker.rb
    +++ b/lib/middleware/request_tracker.rb
    @@ -2,11 +2,11 @@ require_dependency 'middleware/anonymous_cache'
     
     class Middleware::RequestTracker
     
    -  def initialize(app, settings={})
    +  def initialize(app, settings = {})
         @app = app
       end
     
    -  def self.log_request_on_site(data,host)
    +  def self.log_request_on_site(data, host)
         RailsMultisite::ConnectionManagement.with_hostname(host) do
           log_request(data)
         end
    @@ -46,8 +46,8 @@ class Middleware::RequestTracker
     
       TRACK_VIEW = "HTTP_DISCOURSE_TRACK_VIEW".freeze
       CONTENT_TYPE = "Content-Type".freeze
    -  def self.get_data(env,result)
    -    status,headers = result
    +  def self.get_data(env, result)
    +    status, headers = result
         status = status.to_i
     
         helper = Middleware::AnonymousCache::Helper.new(env)
    @@ -74,21 +74,21 @@ class Middleware::RequestTracker
       ensure
     
         # we got to skip this on error ... its just logging
    -    data = self.class.get_data(env,result) rescue nil
    +    data = self.class.get_data(env, result) rescue nil
         host = RailsMultisite::ConnectionManagement.host(env)
     
         if data
    -      if result && (headers=result[1])
    +      if result && (headers = result[1])
             headers["X-Discourse-TrackView"] = "1" if data[:track_view]
           end
    -      log_later(data,host)
    +      log_later(data, host)
         end
     
       end
     
    -  def log_later(data,host)
    -    Scheduler::Defer.later("Track view", _db=nil) do
    -      self.class.log_request_on_site(data,host)
    +  def log_later(data, host)
    +    Scheduler::Defer.later("Track view", _db = nil) do
    +      self.class.log_request_on_site(data, host)
         end
       end
     
    diff --git a/lib/middleware/turbo_dev.rb b/lib/middleware/turbo_dev.rb
    index beda165224..53e81cb50d 100644
    --- a/lib/middleware/turbo_dev.rb
    +++ b/lib/middleware/turbo_dev.rb
    @@ -13,7 +13,7 @@ module Middleware
       #  config.middleware.insert 0, Middleware::TurboDev
       #
       class TurboDev
    -    def initialize(app, settings={})
    +    def initialize(app, settings = {})
           @app = app
         end
     
    @@ -27,7 +27,7 @@ module Middleware
             etag = etag.gsub "\"", ""
             asset = Rails.application.assets.find_asset(name)
             if asset && asset.digest == etag
    -          return [304,{},[]]
    +          return [304, {}, []]
             end
           end
     
    diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb
    index 537c7faff0..8777f1b8ff 100644
    --- a/lib/new_post_manager.rb
    +++ b/lib/new_post_manager.rb
    @@ -1,6 +1,7 @@
     require_dependency 'post_creator'
     require_dependency 'new_post_result'
     require_dependency 'post_enqueuer'
    +require_dependency 'word_watcher'
     
     # Determines what actions should be taken with new posts.
     #
    @@ -16,16 +17,16 @@ class NewPostManager
       end
     
       def self.handlers
    -    sorted_handlers.map {|h| h[:proc]}
    +    sorted_handlers.map { |h| h[:proc] }
       end
     
       def self.clear_handlers!
         @sorted_handlers = [{ priority: 0, proc: method(:default_handler) }]
       end
     
    -  def self.add_handler(priority=0, &block)
    +  def self.add_handler(priority = 0, &block)
         sorted_handlers << { priority: priority, proc: block }
    -    @sorted_handlers.sort_by! {|h| -h[:priority]}
    +    @sorted_handlers.sort_by! { |h| -h[:priority] }
       end
     
       def self.is_first_post?(manager)
    @@ -66,21 +67,25 @@ class NewPostManager
     
       end
     
    -  def self.user_needs_approval?(manager)
    +  def self.exempt_user?(user)
    +    user.staff? || user.staged
    +  end
    +
    +  def self.post_needs_approval?(manager)
         user = manager.user
     
    -    return false if user.staff? || user.staged
    +    return false if exempt_user?(user)
     
         (user.trust_level <= TrustLevel.levels[:basic] && user.post_count < SiteSetting.approve_post_count) ||
         (user.trust_level < SiteSetting.approve_unless_trust_level.to_i) ||
         (manager.args[:title].present? && user.trust_level < SiteSetting.approve_new_topics_unless_trust_level.to_i) ||
         is_fast_typer?(manager) ||
    -    matches_auto_block_regex?(manager)
    +    matches_auto_block_regex?(manager) ||
    +    WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval?
       end
     
       def self.default_handler(manager)
    -    if user_needs_approval?(manager)
    -
    +    if post_needs_approval?(manager)
           validator = Validators::PostValidator.new
           post = Post.new(raw: manager.args[:raw])
           post.user = manager.user
    @@ -118,17 +123,25 @@ class NewPostManager
         SiteSetting.approve_post_count > 0 ||
         SiteSetting.approve_unless_trust_level.to_i > 0 ||
         SiteSetting.approve_new_topics_unless_trust_level.to_i > 0 ||
    +    WordWatcher.words_for_action_exists?(:require_approval) ||
         handlers.size > 1
       end
     
       def initialize(user, args)
         @user = user
    -    @args = args.delete_if {|_, v| v.nil?}
    +    @args = args.delete_if { |_, v| v.nil? }
       end
     
       def perform
    +    if !self.class.exempt_user?(@user) && WordWatcher.new("#{@args[:title]} #{@args[:raw]}").should_block?
    +      result = NewPostResult.new(:created_post, false)
    +      result.errors[:base] << I18n.t('contains_blocked_words')
    +      return result
    +    end
    +
         # We never queue private messages
         return perform_create_post if @args[:archetype] == Archetype.private_message
    +
         if args[:topic_id] && Topic.where(id: args[:topic_id], archetype: Archetype.private_message).exists?
           return perform_create_post
         end
    @@ -145,11 +158,11 @@ class NewPostManager
       end
     
       # Enqueue this post in a queue
    -  def enqueue(queue, reason=nil)
    +  def enqueue(queue, reason = nil)
         result = NewPostResult.new(:enqueued)
         enqueuer = PostEnqueuer.new(@user, queue)
     
    -    queued_args = {post_options: @args.dup}
    +    queued_args = { post_options: @args.dup }
         queued_args[:raw] = queued_args[:post_options].delete(:raw)
         queued_args[:topic_id] = queued_args[:post_options].delete(:topic_id)
     
    diff --git a/lib/new_post_result.rb b/lib/new_post_result.rb
    index 63acab0b00..78725ea11f 100644
    --- a/lib/new_post_result.rb
    +++ b/lib/new_post_result.rb
    @@ -10,7 +10,7 @@ class NewPostResult
       attr_accessor :queued_post
       attr_accessor :pending_count
     
    -  def initialize(action, success=false)
    +  def initialize(action, success = false)
         @action = action
         @success = success
       end
    diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb
    index 1461894256..42862abe32 100644
    --- a/lib/oneboxer.rb
    +++ b/lib/oneboxer.rb
    @@ -18,16 +18,16 @@ module Oneboxer
       end
     
       def self.ignore_redirects
    -    @ignore_redirects ||= ['http://store.steampowered.com']
    +    @ignore_redirects ||= ['http://www.dropbox.com', 'http://store.steampowered.com', Discourse.base_url]
       end
     
    -  def self.preview(url, options=nil)
    +  def self.preview(url, options = nil)
         options ||= {}
         invalidate(url) if options[:invalidate_oneboxes]
         onebox_raw(url)[:preview]
       end
     
    -  def self.onebox(url, options=nil)
    +  def self.onebox(url, options = nil)
         options ||= {}
         invalidate(url) if options[:invalidate_oneboxes]
         onebox_raw(url)[:onebox]
    @@ -74,7 +74,7 @@ module Oneboxer
     
       def self.append_source_topic_id(url, topic_id)
         # hack urls to create proper expansions
    -    if url =~ Regexp.new("^#{Discourse.base_url.gsub(".","\\.")}.*$", true)
    +    if url =~ Regexp.new("^#{Discourse.base_url.gsub(".", "\\.")}.*$", true)
           uri = URI.parse(url) rescue nil
           if uri && uri.path
             route = Rails.application.routes.recognize_path(uri.path) rescue nil
    @@ -86,7 +86,7 @@ module Oneboxer
         url
       end
     
    -  def self.apply(string_or_doc, args=nil)
    +  def self.apply(string_or_doc, args = nil)
         doc = string_or_doc
         doc = Nokogiri::HTML::fragment(doc) if doc.is_a?(String)
         changed = false
    @@ -95,7 +95,7 @@ module Oneboxer
           if args && args[:topic_id]
             url = append_source_topic_id(url, args[:topic_id])
           end
    -      onebox, _preview = yield(url,element)
    +      onebox, _preview = yield(url, element)
           if onebox
             parsed_onebox = Nokogiri::HTML::fragment(onebox)
             next unless parsed_onebox.children.count > 0
    @@ -146,7 +146,6 @@ module Oneboxer
         end
     
         def self.onebox_raw(url)
    -
           Rails.cache.fetch(onebox_cache_key(url), expires_in: 1.day) do
             fd = FinalDestination.new(url, ignore_redirects: ignore_redirects)
             uri = fd.resolve
    diff --git a/lib/pbkdf2.rb b/lib/pbkdf2.rb
    index f21cc8aebc..1032b4a1ad 100644
    --- a/lib/pbkdf2.rb
    +++ b/lib/pbkdf2.rb
    @@ -20,18 +20,18 @@ class Pbkdf2
         u = ret = prf(h, password, salt + [1].pack("N"))
     
         2.upto(iterations) do
    -     u = prf(h, password, u)
    +      u = prf(h, password, u)
          ret.xor!(u)
         end
     
    -    ret.bytes.map{|b| ("0" + b.to_s(16))[-2..-1]}.join("")
    +    ret.bytes.map { |b| ("0" + b.to_s(16))[-2..-1] }.join("")
       end
     
       protected
     
       # fallback xor in case we need it for jruby ... way slower
    -  def self.xor(x,y)
    -    x.bytes.zip(y.bytes).map{|x,y| x ^ y}.pack('c*')
    +  def self.xor(x, y)
    +    x.bytes.zip(y.bytes).map { |x, y| x ^ y }.pack('c*')
       end
     
       def self.prf(hash_function, password, data)
    diff --git a/lib/pinned_check.rb b/lib/pinned_check.rb
    index b1a25a4d5d..54543499b3 100644
    --- a/lib/pinned_check.rb
    +++ b/lib/pinned_check.rb
    @@ -2,16 +2,16 @@
     # taking into account anonymous users and users who have dismissed it
     class PinnedCheck
     
    -  def self.unpinned?(topic,topic_user=nil)
    +  def self.unpinned?(topic, topic_user = nil)
         topic.pinned_at &&
         topic_user &&
         topic_user.cleared_pinned_at &&
         topic_user.cleared_pinned_at > topic.pinned_at
       end
     
    -  def self.pinned?(topic, topic_user=nil)
    +  def self.pinned?(topic, topic_user = nil)
         !!topic.pinned_at &&
    -    !unpinned?(topic,topic_user)
    +    !unpinned?(topic, topic_user)
       end
     
     end
    diff --git a/lib/plugin/auth_provider.rb b/lib/plugin/auth_provider.rb
    index 570a245b08..996d9ba041 100644
    --- a/lib/plugin/auth_provider.rb
    +++ b/lib/plugin/auth_provider.rb
    @@ -12,7 +12,7 @@ class Plugin::AuthProvider
       end
     
       def to_json
    -    result = {name: name}
    +    result = { name: name }
         result['customUrl'] = custom_url if custom_url
         result['titleOverride'] = title if title
         result['titleSetting'] = title_setting if title_setting
    diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb
    index ca74990107..877a1ed9c7 100644
    --- a/lib/plugin/instance.rb
    +++ b/lib/plugin/instance.rb
    @@ -2,7 +2,6 @@ require 'digest/sha1'
     require 'fileutils'
     require_dependency 'plugin/metadata'
     require_dependency 'plugin/auth_provider'
    -require_dependency 'plugin/theme'
     
     class Plugin::CustomEmoji
       def self.cache_key
    @@ -58,14 +57,14 @@ class Plugin::Instance
         }
       end
     
    -  def initialize(metadata=nil, path=nil)
    +  def initialize(metadata = nil, path = nil)
         @metadata = metadata
         @path = path
         @idx = 0
       end
     
       def add_admin_route(label, location)
    -    @admin_route = {label: label, location: location}
    +    @admin_route = { label: label, location: location }
       end
     
       def enabled?
    @@ -74,7 +73,7 @@ class Plugin::Instance
     
       delegate :name, to: :metadata
     
    -  def add_to_serializer(serializer, attr, define_include_method=true, &block)
    +  def add_to_serializer(serializer, attr, define_include_method = true, &block)
         klass = "#{serializer.to_s.classify}Serializer".constantize rescue "#{serializer.to_s}Serializer".constantize
     
         klass.attributes(attr) unless attr.to_s.start_with?("include_")
    @@ -164,7 +163,7 @@ class Plugin::Instance
       def delete_extra_automatic_assets(good_paths)
         return unless Dir.exists? auto_generated_path
     
    -    filenames = good_paths.map{|f| File.basename(f)}
    +    filenames = good_paths.map { |f| File.basename(f) }
         # nuke old files
         Dir.foreach(auto_generated_path) do |p|
           next if [".", ".."].include?(p)
    @@ -242,13 +241,13 @@ class Plugin::Instance
         DiscoursePluginRegistry.register_html_builder(name, &block)
       end
     
    -  def register_asset(file, opts=nil)
    +  def register_asset(file, opts = nil)
         full_path = File.dirname(path) << "/assets/" << file
         assets << [full_path, opts]
       end
     
       def register_color_scheme(name, colors)
    -    color_schemes << {name: name, colors: colors}
    +    color_schemes << { name: name, colors: colors }
       end
     
       def register_seed_data(key, value)
    @@ -259,14 +258,6 @@ class Plugin::Instance
         Plugin::CustomEmoji.register(name, url)
       end
     
    -  def register_theme(name)
    -    return unless enabled?
    -
    -    theme = Plugin::Theme.new(self, name)
    -    yield theme
    -    themes << theme
    -  end
    -
       def automatic_assets
         css = styles.join("\n")
         js = javascripts.join("\n")
    @@ -362,14 +353,13 @@ JS
           target = Rails.root.to_s + "/public/plugins/"
     
           Discourse::Utils.execute_command('mkdir', '-p', target)
    -      target << name.gsub(/\s/,"_")
    +      target << name.gsub(/\s/, "_")
           # TODO a cleaner way of registering and unregistering
           Discourse::Utils.execute_command('rm', '-f', target)
           Discourse::Utils.execute_command('ln', '-s', public_data, target)
         end
       end
     
    -
       def auth_provider(opts)
         provider = Plugin::AuthProvider.new
     
    @@ -379,7 +369,6 @@ JS
         auth_providers << provider
       end
     
    -
       # shotgun approach to gem loading, in future we need to hack bundler
       #  to at least determine dependencies do not clash before loading
       #
    @@ -390,7 +379,7 @@ JS
         PluginGem.load(path, name, version, opts)
       end
     
    -  def enabled_site_setting(setting=nil)
    +  def enabled_site_setting(setting = nil)
         if setting
           @enabled_site_setting = setting
         else
    @@ -421,9 +410,9 @@ JS
     
           Dir.glob("#{root_path}/**/*") do |f|
             if File.directory?(f)
    -          yield [f,true]
    +          yield [f, true]
             elsif f.to_s.ends_with?(".js.es6") || f.to_s.ends_with?(".hbs")
    -          yield [f,false]
    +          yield [f, false]
             end
           end
         end
    @@ -442,7 +431,7 @@ JS
       def write_asset(path, contents)
         unless File.exists?(path)
           ensure_directory(path)
    -      File.open(path,"w") { |f| f.write(contents) }
    +      File.open(path, "w") { |f| f.write(contents) }
         end
       end
     
    diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb
    index bcf8954d2b..123fccc396 100644
    --- a/lib/plugin/metadata.rb
    +++ b/lib/plugin/metadata.rb
    @@ -25,7 +25,13 @@ class Plugin::Metadata
         "poll",
         "discourse-plugin-linkedin-auth",
         "discourse-plugin-office365-auth",
    -    "discourse-oauth2-basic"
    +    "discourse-oauth2-basic",
    +    "discourse-math",
    +    "discourse-bbcode-color",
    +    "discourse-bbcode",
    +    "discourse-affiliate",
    +    "discourse-translator",
    +    "discourse-patreon"
       ])
     
       FIELDS ||= [:name, :about, :version, :authors, :url, :required_version]
    diff --git a/lib/plugin/theme.rb b/lib/plugin/theme.rb
    deleted file mode 100644
    index 04128d6f35..0000000000
    --- a/lib/plugin/theme.rb
    +++ /dev/null
    @@ -1,30 +0,0 @@
    -class Plugin::Theme
    -  attr_reader :color_scheme
    -
    -  def initialize(plugin, name)
    -    @plugin = plugin
    -    @name = name
    -  end
    -
    -  def css(name)
    -    @plugin.register_asset("stylesheets/#{name}.scss")
    -  end
    -
    -  def set_color_scheme(scheme)
    -    @color_scheme = scheme
    -  end
    -
    -  def register_public
    -    public_dir = "#{@plugin.directory}/public"
    -    if File.exist?(public_dir)
    -      Rails.application.config.before_initialize do |app|
    -        app.middleware.insert_before(
    -          ::Rack::Runtime,
    -          ::ActionDispatch::Static,
    -          public_dir
    -        )
    -      end
    -    end
    -  end
    -end
    -
    diff --git a/lib/plugin_gem.rb b/lib/plugin_gem.rb
    index 48df5e57c8..6347cc8dce 100644
    --- a/lib/plugin_gem.rb
    +++ b/lib/plugin_gem.rb
    @@ -1,5 +1,5 @@
     module PluginGem
    -  def self.load(path, name, version, opts=nil)
    +  def self.load(path, name, version, opts = nil)
         opts ||= {}
     
         gems_path = File.dirname(path) + "/gems/#{RUBY_VERSION}"
    diff --git a/lib/post_creator.rb b/lib/post_creator.rb
    index 405a7e0537..42b7055823 100644
    --- a/lib/post_creator.rb
    +++ b/lib/post_creator.rb
    @@ -239,8 +239,8 @@ class PostCreator
         return unless post.reply_to_post_number.present?
     
         reply_info = Post.where(topic_id: post.topic_id, post_number: post.reply_to_post_number)
    -                     .select(:user_id, :post_type)
    -                     .first
    +      .select(:user_id, :post_type)
    +      .first
     
         if reply_info.present?
           post.reply_to_user_id ||= reply_info.user_id
    @@ -316,11 +316,11 @@ class PostCreator
     
       def handle_spam
         if @spam
    -      GroupMessage.create( Group[:moderators].name,
    +      GroupMessage.create(Group[:moderators].name,
                                :spam_post_blocked,
    -                           { user: @user,
    -                             limit_once_per: 24.hours,
    -                             message_params: {domains: @post.linked_hosts.keys.join(', ')} } )
    +                           user: @user,
    +                           limit_once_per: 24.hours,
    +                           message_params: { domains: @post.linked_hosts.keys.join(', ') })
         elsif @post && errors.blank? && !skip_validations?
           SpamRulesEnforcer.enforce!(@post)
         end
    @@ -339,7 +339,7 @@ class PostCreator
         unless @topic.topic_allowed_users.where(user_id: @user.id).exists?
           unless @topic.topic_allowed_groups.where('group_id IN (
                                                   SELECT group_id FROM group_users where user_id = ?
    -                                           )',@user.id).exists?
    +                                           )', @user.id).exists?
             @topic.topic_allowed_users.create!(user_id: @user.id)
           end
         end
    @@ -363,11 +363,13 @@ class PostCreator
       def find_category_id
         @opts.delete(:category) if @opts[:archetype].present? && @opts[:archetype] == Archetype.private_message
     
    -    category = if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /^\d+$/)
    -                 Category.find_by(id: @opts[:category])
    -               else
    -                 Category.find_by(name_lower: @opts[:category].try(:downcase))
    -               end
    +    category =
    +      if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /^\d+$/)
    +        Category.find_by(id: @opts[:category])
    +      else
    +        Category.find_by(name_lower: @opts[:category].try(:downcase))
    +      end
    +
         category&.id
       end
     
    @@ -491,7 +493,6 @@ class PostCreator
                            last_read_post_number: @post.post_number,
                            highest_seen_post_number: @post.post_number)
     
    -
           # assume it took us 5 seconds of reading time to make a post
           PostTiming.record_timing(topic_id: @post.topic_id,
                                    user_id: @post.user_id,
    diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb
    index 07f34e85ee..3f7cdbffab 100644
    --- a/lib/post_destroyer.rb
    +++ b/lib/post_destroyer.rb
    @@ -6,10 +6,10 @@ class PostDestroyer
     
       def self.destroy_old_hidden_posts
         Post.where(deleted_at: nil, hidden: true)
    -        .where("hidden_at < ?", 30.days.ago)
    -        .find_each do |post|
    -        PostDestroyer.new(Discourse.system_user, post).destroy
    -      end
    +      .where("hidden_at < ?", 30.days.ago)
    +      .find_each do |post|
    +      PostDestroyer.new(Discourse.system_user, post).destroy
    +    end
       end
     
       def self.destroy_stubs
    @@ -17,26 +17,26 @@ class PostDestroyer
     
         # exclude deleted topics and posts that are actively flagged
         Post.where(deleted_at: nil, user_deleted: true)
    -        .where("NOT EXISTS (
    +      .where("NOT EXISTS (
                 SELECT 1 FROM topics t
                 WHERE t.deleted_at IS NOT NULL AND
                       t.id = posts.topic_id
             )")
    -        .where("updated_at < ? AND post_number > 1", SiteSetting.delete_removed_posts_after.hours.ago)
    -        .where("NOT EXISTS (
    +      .where("updated_at < ? AND post_number > 1", SiteSetting.delete_removed_posts_after.hours.ago)
    +      .where("NOT EXISTS (
                       SELECT 1
                       FROM post_actions pa
                       WHERE pa.post_id = posts.id AND
                             pa.deleted_at IS NULL AND
                             pa.post_action_type_id IN (?)
                   )", PostActionType.notify_flag_type_ids)
    -        .find_each do |post|
    +      .find_each do |post|
     
           PostDestroyer.new(Discourse.system_user, post, context: context).destroy
         end
       end
     
    -  def initialize(user, post, opts={})
    +  def initialize(user, post, opts = {})
         @user = user
         @post = post
         @topic = post.topic if post
    diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb
    index 3227f76c3c..cb8fdb56c1 100644
    --- a/lib/post_jobs_enqueuer.rb
    +++ b/lib/post_jobs_enqueuer.rb
    @@ -1,5 +1,5 @@
     class PostJobsEnqueuer
    -  def initialize(post, topic, new_topic, opts={})
    +  def initialize(post, topic, new_topic, opts = {})
         @post = post
         @topic = topic
         @new_topic = new_topic
    diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb
    index 9b9ef68d54..f443a64d93 100644
    --- a/lib/post_revisor.rb
    +++ b/lib/post_revisor.rb
    @@ -42,7 +42,7 @@ class PostRevisor
     
       attr_reader :category_changed
     
    -  def initialize(post, topic=nil)
    +  def initialize(post, topic = nil)
         @post = post
         @topic = topic || post.topic
       end
    @@ -112,7 +112,7 @@ class PostRevisor
       # - bypass_bump: do not bump the topic, even if last post
       # - skip_validations: ask ActiveRecord to skip validations
       # - skip_revision: do not create a new PostRevision record
    -  def revise!(editor, fields, opts={})
    +  def revise!(editor, fields, opts = {})
         @editor = editor
         @fields = fields.with_indifferent_access
         @opts = opts
    @@ -197,7 +197,7 @@ class PostRevisor
       end
     
       def topic_changed?
    -    PostRevisor.tracked_topic_fields.keys.any? {|f| @fields.has_key?(f)}
    +    PostRevisor.tracked_topic_fields.keys.any? { |f| @fields.has_key?(f) }
       end
     
       def revise_post
    @@ -253,15 +253,15 @@ class PostRevisor
           # UserActionCreator will create new UserAction records for the new owner
     
           UserAction.where(target_post_id: @post.id)
    -                .where(user_id: prev_owner.id)
    -                .where(action_type: USER_ACTIONS_TO_REMOVE)
    -                .destroy_all
    +        .where(user_id: prev_owner.id)
    +        .where(action_type: USER_ACTIONS_TO_REMOVE)
    +        .destroy_all
     
           if @post.post_number == 1
             UserAction.where(target_topic_id: @post.topic_id)
    -                  .where(user_id: prev_owner.id)
    -                  .where(action_type: UserAction::NEW_TOPIC)
    -                  .destroy_all
    +          .where(user_id: prev_owner.id)
    +          .where(action_type: UserAction::NEW_TOPIC)
    +          .destroy_all
           end
         end
     
    @@ -283,9 +283,9 @@ class PostRevisor
         # post owner changed
         if prev_owner && new_owner && prev_owner != new_owner
           likes = UserAction.where(target_post_id: @post.id)
    -                        .where(user_id: prev_owner.id)
    -                        .where(action_type: UserAction::WAS_LIKED)
    -                        .update_all(user_id: new_owner.id)
    +        .where(user_id: prev_owner.id)
    +        .where(action_type: UserAction::WAS_LIKED)
    +        .update_all(user_id: new_owner.id)
     
           private_message = @post.topic.private_message?
     
    @@ -412,8 +412,8 @@ class PostRevisor
     
       def is_last_post?
         !Post.where(topic_id: @topic.id)
    -         .where("post_number > ?", @post.post_number)
    -         .exists?
    +      .where("post_number > ?", @post.post_number)
    +      .exists?
       end
     
       def plugin_callbacks
    @@ -442,11 +442,13 @@ class PostRevisor
         doc = Nokogiri::HTML.fragment(@post.cooked)
         doc.css("img").remove
     
    -    html = doc.css("p").first.inner_html.strip
    -    new_description = html unless html.starts_with?(Category.post_template[0..50])
    -
    -    category.update_column(:description, new_description)
    -    @category_changed = category
    +    if html = doc.css("p").first&.inner_html&.strip
    +      new_description = html unless html.starts_with?(Category.post_template[0..50])
    +      category.update_column(:description, new_description)
    +      @category_changed = category
    +    else
    +      @post.errors[:base] << I18n.t("category.errors.description_incomplete")
    +    end
       end
     
       def advance_draft_sequence
    diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
    index 3231301f53..cfe191f505 100644
    --- a/lib/pretty_text.rb
    +++ b/lib/pretty_text.rb
    @@ -80,7 +80,7 @@ module PrettyText
         ctx_load(ctx, "#{Rails.root}/app/assets/javascripts/discourse-loader.js")
         ctx_load(ctx, "vendor/assets/javascripts/lodash.js")
         ctx_load_manifest(ctx, "pretty-text-bundle.js")
    -
    +    ctx_load_manifest(ctx, "markdown-it-bundle.js")
         root_path = "#{Rails.root}/app/assets/javascripts/"
     
         apply_es6_file(ctx, root_path, "discourse/lib/utilities")
    @@ -123,11 +123,12 @@ module PrettyText
     
       def self.reset_context
         @ctx_init.synchronize do
    +      @ctx&.dispose
           @ctx = nil
         end
       end
     
    -  def self.markdown(text, opts={})
    +  def self.markdown(text, opts = {})
         # we use the exact same markdown converter as the client
         # TODO: use the same extensions on both client and server (in particular the template for mentions)
         baked = nil
    @@ -148,13 +149,6 @@ module PrettyText
             paths[:S3BaseUrl] = Discourse.store.absolute_base_url
           end
     
    -      if SiteSetting.enable_experimental_markdown_it
    -        # defer load markdown it
    -        unless context.eval("window.markdownit")
    -          ctx_load_manifest(context, "markdown-it-bundle.js")
    -        end
    -      end
    -
           custom_emoji = {}
           Emoji.custom.map { |e| custom_emoji[e.name] = e.url }
     
    @@ -170,6 +164,9 @@ module PrettyText
             __optInput.mentionLookup = __mentionLookup;
             __optInput.customEmoji = #{custom_emoji.to_json};
             __optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
    +        __optInput.lookupInlineOnebox = __lookupInlineOnebox;
    +        #{opts[:linkify] == false ? "__optInput.linkify = false;" : ""}
    +        __optInput.censoredWords = #{WordWatcher.words_for_action(:censor).join('|').to_json};
           JS
     
           if opts[:topicId]
    @@ -182,12 +179,13 @@ module PrettyText
     
           buffer << "__textOptions = __buildOptions(__optInput);\n"
     
    +      buffer << ("__pt = new __PrettyText(__textOptions);")
    +
           # Be careful disabling sanitization. We allow for custom emails
           if opts[:sanitize] == false
    -        buffer << ('__textOptions.sanitize = false;')
    +        buffer << ('__pt.disableSanitizer();')
           end
     
    -      buffer << ("__pt = new __PrettyText(__textOptions);")
           opts = context.eval(buffer)
     
           DiscourseEvent.trigger(:markdown_context, context)
    @@ -226,7 +224,7 @@ module PrettyText
         end
       end
     
    -  def self.cook(text, opts={})
    +  def self.cook(text, opts = {})
         options = opts.dup
     
         # we have a minor inconsistency
    @@ -282,7 +280,7 @@ module PrettyText
             if !uri.host.present? ||
                uri.host == site_uri.host ||
                uri.host.ends_with?("." << site_uri.host) ||
    -           whitelist.any?{|u| uri.host == u || uri.host.ends_with?("." << u)}
    +           whitelist.any? { |u| uri.host == u || uri.host.ends_with?("." << u) }
               # we are good no need for nofollow
             else
               l["rel"] = "nofollow noopener"
    @@ -329,7 +327,7 @@ module PrettyText
         links
       end
     
    -  def self.excerpt(html, max_length, options={})
    +  def self.excerpt(html, max_length, options = {})
         # TODO: properly fix this HACK in ExcerptParser without introducing XSS
         doc = Nokogiri::HTML.fragment(html)
         strip_image_wrapping(doc)
    @@ -343,7 +341,7 @@ module PrettyText
     
         # If the user is not basic, strip links from their bio
         fragment = Nokogiri::HTML.fragment(string)
    -    fragment.css('a').each {|a| a.replace(a.inner_html) }
    +    fragment.css('a').each { |a| a.replace(a.inner_html) }
         fragment.to_html
       end
     
    diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb
    index abcb5b6665..38abc29092 100644
    --- a/lib/pretty_text/helpers.rb
    +++ b/lib/pretty_text/helpers.rb
    @@ -1,3 +1,5 @@
    +require_dependency 'inline_oneboxer'
    +
     module PrettyText
       module Helpers
         extend self
    @@ -9,7 +11,7 @@ module PrettyText
             I18n.t(key)
           else
             str = I18n.t(key, Hash[opts.entries].symbolize_keys).dup
    -        opts.each { |k,v| str.gsub!("{{#{k.to_s}}}", v.to_s) }
    +        opts.each { |k, v| str.gsub!("{{#{k.to_s}}}", v.to_s) }
             str
           end
         end
    @@ -43,6 +45,10 @@ module PrettyText
           end
         end
     
    +    def lookup_inline_onebox(url)
    +      InlineOneboxer.lookup(url)
    +    end
    +
         def get_topic_info(topic_id)
           return unless topic_id.is_a?(Integer)
           # TODO this only handles public topics, secured one do not get this
    diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js
    index afb1a199a3..dc0ede53de 100644
    --- a/lib/pretty_text/shims.js
    +++ b/lib/pretty_text/shims.js
    @@ -7,8 +7,6 @@ __utils = require('discourse/lib/utilities');
     __emojiUnicodeReplacer = null;
     
     __setUnicode = function(replacements) {
    -  require('pretty-text/engines/discourse-markdown/emoji').setUnicodeReplacements(replacements);
    -  
       let unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g");
     
       __emojiUnicodeReplacer = function(text) {
    @@ -51,6 +49,10 @@ function __getURL(url) {
       return url;
     }
     
    +function __lookupInlineOnebox(url) {
    +  return __helpers.lookup_inline_onebox(url);
    +}
    +
     function __getTopicInfo(i) {
       return __helpers.get_topic_info(i);
     }
    diff --git a/lib/primary_group_lookup.rb b/lib/primary_group_lookup.rb
    index c61bb1b7e5..5f488dfedf 100644
    --- a/lib/primary_group_lookup.rb
    +++ b/lib/primary_group_lookup.rb
    @@ -1,5 +1,5 @@
     class PrimaryGroupLookup
    -  def initialize(user_ids=[])
    +  def initialize(user_ids = [])
         @user_ids = user_ids.tap(&:compact!).tap(&:uniq!).tap(&:flatten!)
       end
     
    @@ -20,13 +20,13 @@ class PrimaryGroupLookup
     
       def user_lookup_hash
         users_with_primary_group = User.where(id: @user_ids)
    -        .where.not(primary_group_id: nil)
    -        .select(:id, :primary_group_id)
    +      .where.not(primary_group_id: nil)
    +      .select(:id, :primary_group_id)
     
         group_lookup = {}
         group_ids = users_with_primary_group.map(&:primary_group_id).compact
         Group.where(id: group_ids).select(self.class.lookup_columns)
    -         .each { |g| group_lookup[g.id] = g }
    +      .each { |g| group_lookup[g.id] = g }
     
         hash = {}
         users_with_primary_group.each do |u|
    diff --git a/lib/promotion.rb b/lib/promotion.rb
    index 82dd5a998c..368032144d 100644
    --- a/lib/promotion.rb
    +++ b/lib/promotion.rb
    @@ -7,7 +7,6 @@ class Promotion
         @user = user
       end
     
    -
       # Review a user for a promotion. Delegates work to a review_#{trust_level} method.
       # Returns true if the user was promoted, false otherwise.
       def review
    @@ -17,7 +16,6 @@ class Promotion
         # Promotion beyond basic requires some expensive queries, so don't do that here.
         return false if @user.trust_level >= TrustLevel[2]
     
    -
         review_method = :"review_tl#{@user.trust_level}"
         return send(review_method) if respond_to?(review_method)
     
    @@ -43,7 +41,7 @@ class Promotion
         new_level = level
     
         if new_level < old_level && !@user.trust_level_locked
    -      next_up = new_level+1
    +      next_up = new_level + 1
           key = "tl#{next_up}_met?"
           if self.class.respond_to?(key) && self.class.send(key, @user)
             raise Discourse::InvalidAccess.new, I18n.t('trust_levels.change_failed_explanation',
    @@ -62,10 +60,10 @@ class Promotion
           if admin
             StaffActionLogger.new(admin).log_trust_level_change(@user, old_level, new_level)
           else
    -        UserHistory.create!( action: UserHistory.actions[:auto_trust_level_change],
    -                             target_user_id: @user.id,
    -                             previous_value: old_level,
    -                             new_value: new_level)
    +        UserHistory.create!(action: UserHistory.actions[:auto_trust_level_change],
    +                            target_user_id: @user.id,
    +                            previous_value: old_level,
    +                            new_value: new_level)
           end
           @user.save!
           @user.user_profile.recook_bio
    @@ -77,7 +75,6 @@ class Promotion
         true
       end
     
    -
       def self.tl2_met?(user)
         stat = user.user_stat
         return false if stat.topics_entered < SiteSetting.tl2_requires_topics_entered
    diff --git a/lib/rate_limiter.rb b/lib/rate_limiter.rb
    index 0985db9116..59359af107 100644
    --- a/lib/rate_limiter.rb
    +++ b/lib/rate_limiter.rb
    @@ -72,7 +72,7 @@ class RateLimiter
     
         arr = $redis.lrange(@key, 0, @max) || []
         t0 = Time.now.to_i
    -    arr.reject! {|a| (t0 - a.to_i) > @secs}
    +    arr.reject! { |a| (t0 - a.to_i) > @secs }
         @max - arr.size
       end
     
    diff --git a/lib/rate_limiter/limit_exceeded.rb b/lib/rate_limiter/limit_exceeded.rb
    index 787bcdd381..0836ecd3a7 100644
    --- a/lib/rate_limiter/limit_exceeded.rb
    +++ b/lib/rate_limiter/limit_exceeded.rb
    @@ -4,7 +4,7 @@ class RateLimiter
       class LimitExceeded < StandardError
         attr_reader :type
     
    -    def initialize(available_in, type=nil)
    +    def initialize(available_in, type = nil)
           @available_in = available_in
           @type = type
         end
    diff --git a/lib/rate_limiter/on_create_record.rb b/lib/rate_limiter/on_create_record.rb
    index 4232ab9a7f..14374de6a1 100644
    --- a/lib/rate_limiter/on_create_record.rb
    +++ b/lib/rate_limiter/on_create_record.rb
    @@ -13,7 +13,7 @@ class RateLimiter
           return @rate_limiter if @rate_limiter.present?
     
           limit_key = "create_#{self.class.name.underscore}"
    -      max_setting = if user && user.new_user? and SiteSetting.has_setting?("rate_limit_new_user_#{limit_key}")
    +      max_setting = if user && user.new_user? && SiteSetting.has_setting?("rate_limit_new_user_#{limit_key}")
             SiteSetting.send("rate_limit_new_user_#{limit_key}")
           else
             SiteSetting.send("rate_limit_#{limit_key}")
    @@ -31,7 +31,7 @@ class RateLimiter
         end
     
         module ClassMethods
    -      def rate_limit(limiter_method=nil)
    +      def rate_limit(limiter_method = nil)
     
             limiter_method = limiter_method || :default_rate_limiter
     
    diff --git a/lib/retrieve_title.rb b/lib/retrieve_title.rb
    new file mode 100644
    index 0000000000..62d5498c15
    --- /dev/null
    +++ b/lib/retrieve_title.rb
    @@ -0,0 +1,70 @@
    +require_dependency 'final_destination'
    +
    +module RetrieveTitle
    +  class ReadEnough < StandardError; end
    +
    +  def self.crawl(url)
    +    extract_title(fetch_beginning(url))
    +  rescue Exception
    +    # If there was a connection error, do nothing
    +  end
    +
    +  def self.extract_title(html)
    +    title = nil
    +    if doc = Nokogiri::HTML(html)
    +
    +      if node = doc.at('meta[property="og:title"]')
    +        title = node['content']
    +      end
    +
    +      title ||= doc.at('title')&.inner_text
    +    end
    +
    +    if title.present?
    +      title.gsub!(/\n/, ' ')
    +      title.gsub!(/ +/, ' ')
    +      title.strip!
    +      return title
    +    end
    +    nil
    +  end
    +
    +  private
    +
    +    def self.max_chunk_size(uri)
    +      # Amazon leaves the title until very late. Normally it's a bad idea to make an exception for
    +      # one host but amazon is a big one.
    +      return 80 if uri.host =~ /amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/
    +
    +      # default is 10k
    +      10
    +    end
    +
    +    # Fetch the beginning of a HTML document at a url
    +    def self.fetch_beginning(url)
    +      # Never crawl in test mode
    +      return if Rails.env.test?
    +
    +      fd = FinalDestination.new(url)
    +      uri = fd.resolve
    +      return "" unless uri
    +
    +      result = ""
    +      streamer = lambda do |chunk, _, _|
    +        result << chunk
    +
    +        # Using exceptions for flow control is really bad, but there really seems to
    +        # be no sane way to get a stream to stop reading in Excon (or Net::HTTP for
    +        # that matter!)
    +        raise ReadEnough.new if result.size > (max_chunk_size(uri) * 1024)
    +      end
    +      Excon.get(uri.to_s, response_block: streamer, read_timeout: 20, headers: fd.request_headers)
    +      result
    +
    +    rescue Excon::Errors::SocketError => ex
    +      return result if ex.socket_error.is_a?(ReadEnough)
    +      raise
    +    rescue ReadEnough
    +      result
    +    end
    +end
    diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb
    index 7fbfcc8d10..778808a701 100644
    --- a/lib/s3_helper.rb
    +++ b/lib/s3_helper.rb
    @@ -6,7 +6,7 @@ class S3Helper
     
       attr_reader :s3_bucket_name
     
    -  def initialize(s3_upload_bucket, tombstone_prefix='', options={})
    +  def initialize(s3_upload_bucket, tombstone_prefix = '', options = {})
         @s3_options = default_s3_options.merge(options)
     
         @s3_bucket_name, @s3_bucket_folder_path = begin
    @@ -24,14 +24,14 @@ class S3Helper
         check_missing_options
       end
     
    -  def upload(file, path, options={})
    +  def upload(file, path, options = {})
         path = get_path_for_s3_upload(path)
         obj = s3_bucket.object(path)
         obj.upload_file(file, options)
         path
       end
     
    -  def remove(s3_filename, copy_to_tombstone=false)
    +  def remove(s3_filename, copy_to_tombstone = false)
         bucket = s3_bucket
     
         # copy the file in tombstone
    @@ -50,9 +50,8 @@ class S3Helper
         return if @tombstone_prefix.blank?
     
         # cf. http://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html
    -    s3_resource.client.put_bucket_lifecycle({
    -      bucket: @s3_bucket_name,
    -      lifecycle_configuration: {
    +    s3_resource.client.put_bucket_lifecycle(bucket: @s3_bucket_name,
    +                                            lifecycle_configuration: {
             rules: [
               {
                 id: "purge-tombstone",
    @@ -61,8 +60,7 @@ class S3Helper
                 prefix: @tombstone_prefix
               }
             ]
    -      }
    -    })
    +      })
       end
     
       private
    diff --git a/lib/scheduler/defer.rb b/lib/scheduler/defer.rb
    index e055e7f920..7b4abf7d64 100644
    --- a/lib/scheduler/defer.rb
    +++ b/lib/scheduler/defer.rb
    @@ -22,7 +22,7 @@ module Scheduler
           @async = val
         end
     
    -    def later(desc = nil, db=RailsMultisite::ConnectionManagement.current_db, &blk)
    +    def later(desc = nil, db = RailsMultisite::ConnectionManagement.current_db, &blk)
           if @async
             start_thread unless (@thread && @thread.alive?) || @paused
             @queue << [db, blk, desc]
    @@ -43,7 +43,7 @@ module Scheduler
     
         def do_all_work
           while !@queue.empty?
    -        do_work(_non_block=true)
    +        do_work(_non_block = true)
           end
         end
     
    @@ -61,16 +61,16 @@ module Scheduler
         end
     
         # using non_block to match Ruby #deq
    -    def do_work(non_block=false)
    +    def do_work(non_block = false)
           db, job, desc = @queue.deq(non_block)
           begin
             RailsMultisite::ConnectionManagement.establish_connection(db: db) if db
             job.call
           rescue => ex
    -        Discourse.handle_job_exception(ex, {message: "Running deferred code '#{desc}'"})
    +        Discourse.handle_job_exception(ex, message: "Running deferred code '#{desc}'")
           end
         rescue => ex
    -      Discourse.handle_job_exception(ex, {message: "Processing deferred code queue"})
    +      Discourse.handle_job_exception(ex, message: "Processing deferred code queue")
         ensure
           ActiveRecord::Base.connection_handler.clear_active_connections!
         end
    diff --git a/lib/scheduler/manager.rb b/lib/scheduler/manager.rb
    index ec8e249e9b..6b6cf9fd0e 100644
    --- a/lib/scheduler/manager.rb
    +++ b/lib/scheduler/manager.rb
    @@ -34,16 +34,7 @@ module Scheduler
             end
             @thread = Thread.new do
               while !@stopped
    -            if @manager.enable_stats
    -              begin
    -                RailsMultisite::ConnectionManagement.establish_connection(db: "default")
    -                process_queue
    -              ensure
    -                ActiveRecord::Base.connection_handler.clear_active_connections!
    -              end
    -            else
    -              process_queue
    -            end
    +            process_queue
               end
             end
           end
    @@ -51,13 +42,13 @@ module Scheduler
           def keep_alive
             @manager.keep_alive
           rescue => ex
    -        Discourse.handle_job_exception(ex, {message: "Scheduling manager keep-alive"})
    +        Discourse.handle_job_exception(ex, message: "Scheduling manager keep-alive")
           end
     
           def reschedule_orphans
             @manager.reschedule_orphans!
           rescue => ex
    -        Discourse.handle_job_exception(ex, {message: "Scheduling manager orphan rescheduler"})
    +        Discourse.handle_job_exception(ex, message: "Scheduling manager orphan rescheduler")
           end
     
           def hostname
    @@ -69,6 +60,7 @@ module Scheduler
           end
     
           def process_queue
    +
             klass = @queue.deq
             return unless klass
     
    @@ -83,15 +75,23 @@ module Scheduler
             begin
               info.prev_result = "RUNNING"
               @mutex.synchronize { info.write! }
    +
               if @manager.enable_stats
    -            stat = SchedulerStat.create!(
    -              name: klass.to_s,
    -              hostname: hostname,
    -              pid: Process.pid,
    -              started_at: Time.zone.now,
    -              live_slots_start: GC.stat[:heap_live_slots]
    -            )
    +            begin
    +              RailsMultisite::ConnectionManagement.establish_connection(db: "default")
    +
    +              stat = SchedulerStat.create!(
    +                name: klass.to_s,
    +                hostname: hostname,
    +                pid: Process.pid,
    +                started_at: Time.zone.now,
    +                live_slots_start: GC.stat[:heap_live_slots]
    +              )
    +            ensure
    +              ActiveRecord::Base.connection_handler.clear_active_connections!
    +            end
               end
    +
               klass.new.perform
             rescue => e
               if e.class != Jobs::HandledExceptionWrapper
    @@ -117,24 +117,34 @@ module Scheduler
               @mutex.synchronize { info.write! }
             end
           rescue => ex
    -        Discourse.handle_job_exception(ex, {message: "Processing scheduled job queue"})
    +        Discourse.handle_job_exception(ex, message: "Processing scheduled job queue")
           ensure
             @running = false
    +        ActiveRecord::Base.connection_handler.clear_active_connections!
           end
     
           def stop!
    +        return if @stopped
    +
             @mutex.synchronize do
               @stopped = true
     
               @keep_alive_thread.kill
               @reschedule_orphans_thread.kill
     
    +          @keep_alive_thread.join
    +          @reschedule_orphans_thread.join
    +
               enq(nil)
     
    -          Thread.new do
    -            sleep 5
    +          kill_thread = Thread.new do
    +            sleep 0.5
                 @thread.kill
               end
    +
    +          @thread.join
    +          kill_thread.kill
    +          kill_thread.join
             end
           end
     
    @@ -165,11 +175,11 @@ module Scheduler
     
         end
     
    -    def self.without_runner(redis=nil)
    +    def self.without_runner(redis = nil)
           self.new(redis, skip_runner: true)
         end
     
    -    def initialize(redis = nil, options=nil)
    +    def initialize(redis = nil, options = nil)
           @redis = $redis || redis
           @random_ratio = 0.1
           unless options && options[:skip_runner]
    @@ -226,7 +236,7 @@ module Scheduler
           end
         end
     
    -    def reschedule_orphans_on!(hostname=nil)
    +    def reschedule_orphans_on!(hostname = nil)
           redis.zrange(Manager.queue_key(hostname), 0, -1).each do |key|
             klass = get_klass(key)
             next unless klass
    @@ -254,7 +264,7 @@ module Scheduler
           end
         end
     
    -    def schedule_next_job(hostname=nil)
    +    def schedule_next_job(hostname = nil)
           (key, due), _ = redis.zrange Manager.queue_key(hostname), 0, 0, withscores: true
           return unless key
     
    @@ -300,7 +310,6 @@ module Scheduler
           end
         end
     
    -
         def self.discover_schedules
           # hack for developemnt reloader is crazytown
           # multiple classes with same name can be in
    @@ -333,7 +342,7 @@ module Scheduler
           "_scheduler_lock_"
         end
     
    -    def self.queue_key(hostname=nil)
    +    def self.queue_key(hostname = nil)
           if hostname
             "_scheduler_queue_#{hostname}_"
           else
    @@ -341,7 +350,7 @@ module Scheduler
           end
         end
     
    -    def self.schedule_key(klass,hostname=nil)
    +    def self.schedule_key(klass, hostname = nil)
           if hostname
             "_scheduler_#{klass}_#{hostname}"
           else
    diff --git a/lib/scheduler/schedule.rb b/lib/scheduler/schedule.rb
    index a00fe1330f..05c8085bc1 100644
    --- a/lib/scheduler/schedule.rb
    +++ b/lib/scheduler/schedule.rb
    @@ -1,13 +1,13 @@
     module Scheduler::Schedule
     
    -  def daily(options=nil)
    +  def daily(options = nil)
         if options
           @daily = options
         end
         @daily
       end
     
    -  def every(duration=nil)
    +  def every(duration = nil)
         if duration
           @every = duration
           if manager = Scheduler::Manager.current
    diff --git a/lib/scheduler/schedule_info.rb b/lib/scheduler/schedule_info.rb
    index 755bc621a9..08c7756967 100644
    --- a/lib/scheduler/schedule_info.rb
    +++ b/lib/scheduler/schedule_info.rb
    @@ -28,6 +28,9 @@ module Scheduler
           @next_run = @prev_run = @prev_result = @prev_duration = @current_owner = nil
         end
     
    +    # this means the schedule is going to fire, it is setup correctly
    +    # invalid schedules are fixed by running "schedule!"
    +    # this happens automatically after if fire by the manager
         def valid?
           return false unless @next_run
           (!@prev_run && @next_run < Time.now.to_i + 5.minutes) || valid_every? || valid_daily?
    @@ -42,8 +45,9 @@ module Scheduler
     
         def valid_daily?
           return false unless @klass.daily
    +      return true if !@prev_run && @next_run && @next_run <= (Time.zone.now + 1.day).to_i
           !!@prev_run &&
    -        @prev_run <= Time.now.to_i &&
    +        @prev_run <= Time.zone.now.to_i &&
             @next_run < @prev_run + 1.day
         end
     
    @@ -63,7 +67,7 @@ module Scheduler
           return if valid?
     
           at = @klass.daily[:at] || 0
    -      today_begin = Time.now.midnight.to_i
    +      today_begin = Time.zone.now.midnight.to_i
           today_offset = DateTime.now.seconds_since_midnight
     
           # If it's later today
    diff --git a/lib/scheduler/web.rb b/lib/scheduler/web.rb
    index 6d414ce8e9..e9d573ea44 100644
    --- a/lib/scheduler/web.rb
    +++ b/lib/scheduler/web.rb
    @@ -15,8 +15,8 @@ module Scheduler
               return unless duration
               if duration < 1000
                 "#{duration}ms"
    -          elsif duration < 60*1000
    -            "#{'%.2f' % (duration/1000.0)} secs"
    +          elsif duration < 60 * 1000
    +            "#{'%.2f' % (duration / 1000.0)} secs"
               end
             end
           end
    @@ -24,7 +24,7 @@ module Scheduler
           app.get "/scheduler" do
             RailsMultisite::ConnectionManagement.with_connection("default") do
               @manager = Scheduler::Manager.without_runner
    -          @schedules = Scheduler::Manager.discover_schedules.sort do |a,b|
    +          @schedules = Scheduler::Manager.discover_schedules.sort do |a, b|
                 a_next = a.schedule_info.next_run
                 b_next = b.schedule_info.next_run
                 if a_next && b_next
    @@ -35,13 +35,13 @@ module Scheduler
                   1
                 end
               end
    -          erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: {view_path: VIEWS}
    +          erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: { view_path: VIEWS }
             end
           end
     
           app.get "/scheduler/history" do
             @scheduler_stats = SchedulerStat.order('started_at desc').limit(200)
    -        erb File.read(File.join(VIEWS, 'history.erb')), locals: {view_path: VIEWS}
    +        erb File.read(File.join(VIEWS, 'history.erb')), locals: { view_path: VIEWS }
           end
     
           app.post "/scheduler/:name/trigger" do
    diff --git a/lib/score_calculator.rb b/lib/score_calculator.rb
    index 8901c12e5a..baf93cb457 100644
    --- a/lib/score_calculator.rb
    +++ b/lib/score_calculator.rb
    @@ -11,19 +11,18 @@ class ScoreCalculator
         }
       end
     
    -  def initialize(weightings=nil)
    +  def initialize(weightings = nil)
         @weightings = weightings || ScoreCalculator.default_score_weights
       end
     
       # Calculate the score for all posts based on the weightings
    -  def calculate(opts=nil)
    +  def calculate(opts = nil)
         update_posts_score(opts)
         update_posts_rank(opts)
         update_topics_rank(opts)
         update_topics_percent_rank(opts)
       end
     
    -
       private
     
       def update_posts_score(opts)
    @@ -110,7 +109,6 @@ SQL
                   posts_required: SiteSetting.summary_posts_required,
                   score_required: SiteSetting.summary_score_threshold)
     
    -
         filter_topics(builder, opts)
     
         builder.exec
    @@ -131,7 +129,6 @@ SQL
         builder.exec
       end
     
    -
       def filter_topics(builder, opts)
         return builder unless opts
     
    diff --git a/lib/screening_model.rb b/lib/screening_model.rb
    index 26ed789c6c..62cc0eb8a1 100644
    --- a/lib/screening_model.rb
    +++ b/lib/screening_model.rb
    @@ -24,7 +24,7 @@ module ScreeningModel
       end
     
       def action_name=(arg)
    -    raise ArgumentError.new("Invalid action type #{arg}") if arg.nil? or !self.class.actions.has_key?(arg.to_sym)
    +    raise ArgumentError.new("Invalid action type #{arg}") if arg.nil? || !self.class.actions.has_key?(arg.to_sym)
         self.action_type = self.class.actions[arg.to_sym]
       end
     
    diff --git a/lib/search.rb b/lib/search.rb
    index b9cc5dde16..a8c8282c59 100644
    --- a/lib/search.rb
    +++ b/lib/search.rb
    @@ -20,32 +20,34 @@ class Search
         %w(topic category user private_messages)
       end
     
    -  def self.long_locale
    -    # if adding a language see:
    -    # /usr/share/postgresql/9.3/tsearch_data for possible options
    -    # Do not add languages that are missing without amending the
    +  def self.ts_config(locale = SiteSetting.default_locale)
    +    # if adding a text search configuration, you should check PG beforehand:
    +    # SELECT cfgname FROM pg_ts_config;
    +    # As an aside, dictionaries can be listed by `\dFd`, the
    +    # physical locations are in /usr/share/postgresql//tsearch_data.
    +    # But it may not appear there based on pg extension configuration.
         # base docker config
         #
    -    case SiteSetting.default_locale.to_sym
    -      when :da     then 'danish'
    -      when :de     then 'german'
    -      when :en     then 'english'
    -      when :es     then 'spanish'
    -      when :fr     then 'french'
    -      when :it     then 'italian'
    -      when :nl     then 'dutch'
    -      when :nb_NO  then 'norwegian'
    -      when :pt     then 'portuguese'
    -      when :pt_BR  then 'portuguese'
    -      when :sv     then 'swedish'
    -      when :ru     then 'russian'
    -      else 'simple' # use the 'simple' stemmer for other languages
    +    case locale.to_sym
    +    when :da     then 'danish'
    +    when :de     then 'german'
    +    when :en     then 'english'
    +    when :es     then 'spanish'
    +    when :fr     then 'french'
    +    when :it     then 'italian'
    +    when :nl     then 'dutch'
    +    when :nb_NO  then 'norwegian'
    +    when :pt     then 'portuguese'
    +    when :pt_BR  then 'portuguese'
    +    when :sv     then 'swedish'
    +    when :ru     then 'russian'
    +    else 'simple' # use the 'simple' stemmer for other languages
         end
       end
     
       def self.rebuild_problem_posts(limit = 10000)
         posts = Post.joins(:topic)
    -            .where('posts.id IN (
    +      .where('posts.id IN (
                    SELECT p2.id FROM posts p2
                    LEFT JOIN post_search_data pd ON locale = ? AND p2.id = pd.post_id
                    WHERE pd.post_id IS NULL
    @@ -58,7 +60,7 @@ class Search
         end
     
         posts = Post.joins(:topic)
    -            .where('posts.id IN (
    +      .where('posts.id IN (
                    SELECT p2.id FROM posts p2
                    LEFT JOIN topic_search_data pd ON locale = ? AND p2.topic_id = pd.topic_id
                    WHERE pd.topic_id IS NULL AND p2.post_number = 1
    @@ -73,21 +75,16 @@ class Search
         nil
       end
     
    -  def self.prepare_data(search_data)
    +  def self.prepare_data(search_data, purpose = :query)
         data = search_data.squish
    -    # TODO rmmseg is designed for chinese, we need something else for Korean / Japanese
    +    # TODO cppjieba_rb is designed for chinese, we need something else for Korean / Japanese
         if ['zh_TW', 'zh_CN', 'ja', 'ko'].include?(SiteSetting.default_locale) || SiteSetting.search_tokenize_chinese_japanese_korean
    -      unless defined? RMMSeg
    -        require 'rmmseg'
    -        RMMSeg::Dictionary.load_dictionaries
    -      end
    -
    -      algo = RMMSeg::Algorithm.new(search_data)
    -
    -      data = ""
    -      while token = algo.next_token
    -        data << token.text << " "
    +      unless defined? CppjiebaRb
    +        require 'cppjieba_rb'
           end
    +      mode = (purpose == :query ? :query : :mix)
    +      data = CppjiebaRb.segment(search_data, mode: mode)
    +      data = CppjiebaRb.filter_stop_word(data).join(' ')
         end
     
         data.force_encoding("UTF-8")
    @@ -105,7 +102,7 @@ class Search
           month = $2 ? $3.to_i : 1
           day = $4 ? $5.to_i : 1
     
    -      return if day==0 || month==0 || day > 31 || month > 12
    +      return if day == 0 || month == 0 || day > 31 || month > 12
     
           return Time.zone.parse("#{year}-#{month}-#{day}") rescue nil
         end
    @@ -131,17 +128,16 @@ class Search
       def self.min_post_id_no_cache
         return 0 unless SiteSetting.search_prefer_recent_posts?
     
    -
         offset, has_more = Post.unscoped
    -                           .order('id desc')
    -                           .offset(SiteSetting.search_recent_posts_size-1)
    -                           .limit(2)
    -                           .pluck(:id)
    +      .order('id desc')
    +      .offset(SiteSetting.search_recent_posts_size - 1)
    +      .limit(2)
    +      .pluck(:id)
     
         has_more ? offset : 0
       end
     
    -  def self.min_post_id(opts=nil)
    +  def self.min_post_id(opts = nil)
         return 0 unless SiteSetting.search_prefer_recent_posts?
     
         # It can be quite slow to count all the posts so let's cache it
    @@ -153,14 +149,14 @@ class Search
       attr_accessor :term
       attr_reader :clean_term
     
    -  def initialize(term, opts=nil)
    +  def initialize(term, opts = nil)
         @opts = opts || {}
         @guardian = @opts[:guardian] || Guardian.new
         @search_context = @opts[:search_context]
         @include_blurbs = @opts[:include_blurbs] || false
         @blurb_length = @opts[:blurb_length]
    -    @limit = Search.per_facet
         @valid = true
    +    @page = @opts[:page]
     
         # Removes any zero-width characters from search terms
         term.to_s.gsub!(/[\u200B-\u200D\uFEFF]/, '')
    @@ -178,27 +174,54 @@ class Search
           @search_context = @guardian.user
         end
     
    -    if @opts[:type_filter].present?
    -      @limit = Search.per_filter
    -    end
    +    @results = GroupedSearchResults.new(
    +      @opts[:type_filter],
    +      clean_term,
    +      @search_context,
    +      @include_blurbs,
    +      @blurb_length
    +    )
    +  end
     
    -    @results = GroupedSearchResults.new(@opts[:type_filter], clean_term, @search_context, @include_blurbs, @blurb_length)
    +  def limit
    +    if @opts[:type_filter].present?
    +      Search.per_filter + 1
    +    else
    +      Search.per_facet + 1
    +    end
    +  end
    +
    +  def offset
    +    if @page && @opts[:type_filter].present?
    +      (@page - 1) * Search.per_filter
    +    else
    +      0
    +    end
       end
     
       def valid?
         @valid
       end
     
    -  def self.execute(term, opts=nil)
    +  def self.execute(term, opts = nil)
         self.new(term, opts).execute
       end
     
       # Query a term
       def execute
    +    if SiteSetting.log_search_queries?
    +      status, search_log_id = SearchLog.log(
    +        term: @term,
    +        search_type: @opts[:search_type],
    +        ip_address: @opts[:ip_address],
    +        user_id: @opts[:user_id]
    +      )
    +      @results.search_log_id = search_log_id unless status == :error
    +    end
     
    -    unless @filters.present?
    +    unless @filters.present? || @opts[:search_for_id]
           min_length = @opts[:min_search_term_length] || SiteSetting.min_search_term_length
    -      terms = (@term || '').split(/\s(?=(?:[^"]|"[^"]*")*$)/).reject {|t| t.length < min_length }
    +      terms = (@term || '').split(/\s(?=(?:[^"]|"[^"]*")*$)/).reject { |t| t.length < min_length }
     
           if terms.blank?
             @term = ''
    @@ -225,7 +248,7 @@ class Search
         @results
       end
     
    -  def self.advanced_filter(trigger,&block)
    +  def self.advanced_filter(trigger, &block)
         (@advanced_filters ||= {})[trigger] = block
       end
     
    @@ -277,11 +300,11 @@ class Search
         end
       end
     
    -  advanced_filter(/in:wiki/) do |posts,match|
    +  advanced_filter(/in:wiki/) do |posts, match|
         posts.where(wiki: true)
       end
     
    -  advanced_filter(/badge:(.*)/) do |posts,match|
    +  advanced_filter(/badge:(.*)/) do |posts, match|
         badge_id = Badge.where('name ilike ? OR id = ?', match, match.to_i).pluck(:id).first
         if badge_id
           posts.where('posts.user_id IN (SELECT ub.user_id FROM user_badges ub WHERE ub.badge_id = ?)', badge_id)
    @@ -308,7 +331,7 @@ class Search
         posts.where("posts.user_id = #{@guardian.user.id}") if @guardian.user
       end
     
    -  advanced_filter(/in:(watching|tracking)/) do |posts,match|
    +  advanced_filter(/in:(watching|tracking)/) do |posts, match|
         if @guardian.user
           level = TopicUser.notification_levels[match.to_sym]
           posts.where("posts.topic_id IN (
    @@ -343,7 +366,11 @@ class Search
         end
       end
     
    -  advanced_filter(/category:(.+)/) do |posts,match|
    +  advanced_filter(/with:images/) do |posts|
    +    posts.where("posts.image_url IS NOT NULL")
    +  end
    +
    +  advanced_filter(/category:(.+)/) do |posts, match|
         exact = false
     
         if match[0] == "="
    @@ -366,7 +393,7 @@ class Search
         end
       end
     
    -  advanced_filter(/^\#([a-zA-Z0-9\-:=]+)/) do |posts,match|
    +  advanced_filter(/^\#([a-zA-Z0-9\-:=]+)/) do |posts, match|
     
         exact = true
     
    @@ -407,7 +434,7 @@ class Search
         end
       end
     
    -  advanced_filter(/group:(.+)/) do |posts,match|
    +  advanced_filter(/group:(.+)/) do |posts, match|
         group_id = Group.where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck(:id).first
         if group_id
           posts.where("posts.user_id IN (select gu.user_id from group_users gu where gu.group_id = ?)", group_id)
    @@ -416,7 +443,7 @@ class Search
         end
       end
     
    -  advanced_filter(/user:(.+)/) do |posts,match|
    +  advanced_filter(/user:(.+)/) do |posts, match|
         user_id = User.where(staged: false).where('username_lower = ? OR id = ?', match.downcase, match.to_i).pluck(:id).first
         if user_id
           posts.where("posts.user_id = #{user_id}")
    @@ -425,7 +452,7 @@ class Search
         end
       end
     
    -  advanced_filter(/^\@([a-zA-Z0-9_\-.]+)/) do |posts,match|
    +  advanced_filter(/^\@([a-zA-Z0-9_\-.]+)/) do |posts, match|
         user_id = User.where(staged: false).where(username_lower: match.downcase).pluck(:id).first
         if user_id
           posts.where("posts.user_id = #{user_id}")
    @@ -434,7 +461,7 @@ class Search
         end
       end
     
    -  advanced_filter(/before:(.*)/) do |posts,match|
    +  advanced_filter(/before:(.*)/) do |posts, match|
         if date = Search.word_to_date(match)
           posts.where("posts.created_at < ?", date)
         else
    @@ -442,7 +469,7 @@ class Search
         end
       end
     
    -  advanced_filter(/after:(.*)/) do |posts,match|
    +  advanced_filter(/after:(.*)/) do |posts, match|
         if date = Search.word_to_date(match)
           posts.where("posts.created_at > ?", date)
         else
    @@ -459,7 +486,7 @@ class Search
           FROM topic_tags tt, tags
           WHERE tt.tag_id = tags.id
           GROUP BY tt.topic_id
    -      HAVING to_tsvector(#{query_locale}, array_to_string(array_agg(tags.name), ' ')) @@ to_tsquery(#{query_locale}, ?)
    +      HAVING to_tsvector(#{default_ts_config}, array_to_string(array_agg(tags.name), ' ')) @@ to_tsquery(#{default_ts_config}, ?)
           )", tags.join('&'))
         else
           tags = match.split(",")
    @@ -473,18 +500,30 @@ class Search
         end
       end
     
    -  private
    +  advanced_filter(/filetypes?:([a-zA-Z0-9,\-_]+)/) do |posts, match|
    +    file_extensions = match.split(",").map(&:downcase)
     
    +    posts.where("posts.id IN (
    +      SELECT post_id FROM topic_links
    +      WHERE extension IN (:file_extensions)
    +      UNION
    +      SELECT post_uploads.post_id FROM uploads
    +      JOIN post_uploads ON post_uploads.upload_id = uploads.id
    +      WHERE lower(uploads.extension) IN (:file_extensions)
    +      )", file_extensions: file_extensions)
    +  end
    +
    +  private
     
         def process_advanced_search!(term)
     
    -      term.to_s.scan(/(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/).to_a.map do |(word,_)|
    +      term.to_s.scan(/(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/).to_a.map do |(word, _)|
             next if word.blank?
     
             found = false
     
             Search.advanced_filters.each do |matcher, block|
    -          cleaned = word.gsub(/["']/,"")
    +          cleaned = word.gsub(/["']/, "")
               if cleaned =~ matcher
                 (@filters ||= []) << [block, $1]
                 found = true
    @@ -524,14 +563,12 @@ class Search
           end.compact.join(' ')
         end
     
    -
         def find_grouped_results
     
           if @results.type_filter.present?
             raise Discourse::InvalidAccess.new("invalid type filter") unless Search.facets.include?(@results.type_filter)
             send("#{@results.type_filter}_search")
           else
    -        @limit = Search.per_facet + 1
             unless @search_context
               user_search if @term.present?
               category_search if @term.present?
    @@ -585,11 +622,11 @@ class Search
           secure_category_ids
     
           categories = Category.includes(:category_search_data)
    -                           .where("category_search_data.search_data @@ #{ts_query}")
    -                           .references(:category_search_data)
    -                           .order("topics_month DESC")
    -                           .secured(@guardian)
    -                           .limit(@limit)
    +        .where("category_search_data.search_data @@ #{ts_query}")
    +        .references(:category_search_data)
    +        .order("topics_month DESC")
    +        .secured(@guardian)
    +        .limit(limit)
     
           categories.each do |category|
             @results.add(category)
    @@ -600,37 +637,37 @@ class Search
           return if SiteSetting.hide_user_profiles_from_public && !@guardian.user
     
           users = User.includes(:user_search_data)
    -                  .references(:user_search_data)
    -                  .where(active: true)
    -                  .where(staged: false)
    -                  .where("user_search_data.search_data @@ #{ts_query("simple")}")
    -                  .order("CASE WHEN username_lower = '#{@original_term.downcase}' THEN 0 ELSE 1 END")
    -                  .order("last_posted_at DESC")
    -                  .limit(@limit)
    +        .references(:user_search_data)
    +        .where(active: true)
    +        .where(staged: false)
    +        .where("user_search_data.search_data @@ #{ts_query("simple")}")
    +        .order("CASE WHEN username_lower = '#{@original_term.downcase}' THEN 0 ELSE 1 END")
    +        .order("last_posted_at DESC")
    +        .limit(limit)
     
           users.each do |user|
             @results.add(user)
           end
         end
     
    -    def posts_query(limit, opts=nil)
    +    def posts_query(limit, opts = nil)
           opts ||= {}
           posts = Post.where(post_type: Topic.visible_post_types(@guardian.user))
    -                  .joins(:post_search_data, :topic)
    -                  .joins("LEFT JOIN categories ON categories.id = topics.category_id")
    -                  .where("topics.deleted_at" => nil)
    -                  .where("topics.visible")
    +        .joins(:post_search_data, :topic)
    +        .joins("LEFT JOIN categories ON categories.id = topics.category_id")
    +        .where("topics.deleted_at" => nil)
    +        .where("topics.visible")
     
           is_topic_search = @search_context.present? && @search_context.is_a?(Topic)
     
           if opts[:private_messages] || (is_topic_search && @search_context.private_message?)
    -         posts = posts.where("topics.archetype =  ?", Archetype.private_message)
    +        posts = posts.where("topics.archetype =  ?", Archetype.private_message)
     
              unless @guardian.is_admin?
                posts = posts.private_posts_for_user(@guardian.user)
              end
           else
    -         posts = posts.where("topics.archetype <> ?", Archetype.private_message)
    +        posts = posts.where("topics.archetype <> ?", Archetype.private_message)
           end
     
           if @term.present?
    @@ -649,7 +686,6 @@ class Search
               posts = posts.where("posts.raw  || ' ' || u.username || ' ' || COALESCE(u.name, '') ilike ?", "%#{term_without_quote}%")
             else
               posts = posts.where("post_search_data.search_data @@ #{ts_query}")
    -
               exact_terms = @term.scan(/"([^"]+)"/).flatten
               exact_terms.each do |exact|
                 posts = posts.where("posts.raw ilike ?", "%#{exact}%")
    @@ -681,7 +717,7 @@ class Search
               posts = posts.where("topics.category_id in (?)", category_ids)
             elsif @search_context.is_a?(Topic)
               posts = posts.where("topics.id = #{@search_context.id}")
    -                       .order("posts.post_number #{@order == :latest ? "DESC" : ""}")
    +            .order("posts.post_number #{@order == :latest ? "DESC" : ""}")
             end
     
           end
    @@ -711,7 +747,7 @@ class Search
               posts = posts.order("posts.like_count DESC")
             end
           else
    -        posts = posts.order("TS_RANK_CD(TO_TSVECTOR(#{query_locale}, topics.title), #{ts_query}) DESC")
    +        posts = posts.order("TS_RANK_CD(TO_TSVECTOR(#{default_ts_config}, topics.title), #{ts_query}) DESC")
     
             data_ranking = "TS_RANK_CD(post_search_data.search_data, #{ts_query})"
             if opts[:aggregate_search]
    @@ -728,37 +764,37 @@ class Search
             posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)").references(:categories)
           end
     
    +      posts = posts.offset(offset)
           posts.limit(limit)
         end
     
    -    def self.query_locale
    -      "'#{Search.long_locale}'"
    +    def self.default_ts_config
    +      "'#{Search.ts_config}'"
         end
     
    -    def query_locale
    -      self.class.query_locale
    +    def default_ts_config
    +      self.class.default_ts_config
         end
     
    -    def self.ts_query(term, locale = nil, joiner = "&")
    +    def self.ts_query(term, ts_config = nil, joiner = "&")
     
    -      data = Post.exec_sql("SELECT to_tsvector(:locale, :term)",
    -                            locale: 'simple',
    -                            term: term
    -                          ).values[0][0]
    +      data = Post.exec_sql("SELECT TO_TSVECTOR(:config, :term)",
    +                           config: 'simple',
    +                           term: term).values[0][0]
     
    -      locale = Post.sanitize(locale) if locale
    +      ts_config = Post.sanitize(ts_config) if ts_config
           all_terms = data.scan(/'([^']+)'\:\d+/).flatten
           all_terms.map! do |t|
             t.split(/[\)\(&']/)[0]
           end.compact!
     
    -      query = Post.sanitize(all_terms.map {|t| "'#{PG::Connection.escape_string(t)}':*"}.join(" #{joiner} "))
    -      "TO_TSQUERY(#{locale || query_locale}, #{query})"
    +      query = Post.sanitize(all_terms.map { |t| "'#{PG::Connection.escape_string(t)}':*" }.join(" #{joiner} "))
    +      "TO_TSQUERY(#{ts_config || default_ts_config}, #{query})"
         end
     
    -    def ts_query(locale=nil)
    +    def ts_query(ts_config = nil)
           @ts_query_cache ||= {}
    -      @ts_query_cache[(locale || query_locale) + " " + @term] ||= Search.ts_query(@term, locale)
    +      @ts_query_cache["#{ts_config || default_ts_config} #{@term}"] ||= Search.ts_query(@term, ts_config)
         end
     
         def wrap_rows(query)
    @@ -771,10 +807,10 @@ class Search
           query =
             if @order == :likes
               # likes are a pain to aggregate so skip
    -          posts_query(@limit, private_messages: opts[:private_messages])
    +          posts_query(limit, private_messages: opts[:private_messages])
                 .select('topics.id', "posts.post_number")
             else
    -          posts_query(@limit, aggregate_search: true, private_messages: opts[:private_messages])
    +          posts_query(limit, aggregate_search: true, private_messages: opts[:private_messages])
                 .select('topics.id', "#{min_or_max}(posts.post_number) post_number")
                 .group('topics.id')
             end
    @@ -809,8 +845,8 @@ class Search
             added += 1
           end
     
    -      if added < @limit
    -        aggregate_posts(post_sql[:remaining]).each {|p| @results.add(p) }
    +      if added < limit
    +        aggregate_posts(post_sql[:remaining]).each { |p| @results.add(p) }
           end
         end
     
    @@ -822,7 +858,7 @@ class Search
     
         def topic_search
           if @search_context.is_a?(Topic)
    -        posts = posts_eager_loads(posts_query(@limit))
    +        posts = posts_eager_loads(posts_query(limit))
               .where('posts.topic_id = ?', @search_context.id)
     
             posts.each do |post|
    diff --git a/lib/search/grouped_search_results.rb b/lib/search/grouped_search_results.rb
    index 47e1325488..787c279c04 100644
    --- a/lib/search/grouped_search_results.rb
    +++ b/lib/search/grouped_search_results.rb
    @@ -9,10 +9,21 @@ class Search
           extend ActionView::Helpers::TextHelper
         end
     
    -    attr_reader :type_filter,
    -                :posts, :categories, :users,
    -                :more_posts, :more_categories, :more_users,
    -                :term, :search_context, :include_blurbs
    +    attr_reader(
    +      :type_filter,
    +      :posts,
    +      :categories,
    +      :users,
    +      :more_posts,
    +      :more_categories,
    +      :more_users,
    +      :term,
    +      :search_context,
    +      :include_blurbs,
    +      :more_full_page_results
    +    )
    +
    +    attr_accessor :search_log_id
     
         def initialize(type_filter, term, search_context, include_blurbs, blurb_length)
           @type_filter = type_filter
    @@ -40,15 +51,16 @@ class Search
         def add(object)
           type = object.class.to_s.downcase.pluralize
     
    -      if !@type_filter.present? && send(type).length == Search.per_facet
    +      if @type_filter.present? && send(type).length == Search.per_filter
    +        @more_full_page_results = true
    +      elsif !@type_filter.present? && send(type).length == Search.per_facet
             instance_variable_set("@more_#{type}".to_sym, true)
           else
             (send type) << object
           end
         end
     
    -
    -    def self.blurb_for(cooked, term=nil, blurb_length=200)
    +    def self.blurb_for(cooked, term = nil, blurb_length = 200)
           cooked = SearchIndexer::HtmlScrubber.scrub(cooked).squish
     
           blurb = nil
    diff --git a/lib/secure_session.rb b/lib/secure_session.rb
    index bbe9a71833..ecb46a6f67 100644
    --- a/lib/secure_session.rb
    +++ b/lib/secure_session.rb
    @@ -8,7 +8,7 @@ class SecureSession
         $redis.get("#{@prefix}#{key}")
       end
     
    -  def []=(key,val)
    +  def []=(key, val)
         if val == nil
           $redis.del("#{@prefix}#{key}")
         else
    diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb
    index b8a20a80eb..7578a8aabe 100644
    --- a/lib/single_sign_on.rb
    +++ b/lib/single_sign_on.rb
    @@ -43,7 +43,7 @@ class SingleSignOn
           sso.send("#{k}=", val)
         end
     
    -    decoded_hash.each do |k,v|
    +    decoded_hash.each do |k, v|
           if field = k[/^custom\.(.+)$/, 1]
             sso.custom_fields[field] = v
           end
    @@ -72,7 +72,7 @@ class SingleSignOn
         OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload)
       end
     
    -  def to_url(base_url=nil)
    +  def to_url(base_url = nil)
         base = "#{base_url || sso_url}"
         "#{base}#{base.include?('?') ? '&' : '?'}#{payload}"
       end
    @@ -86,7 +86,7 @@ class SingleSignOn
         payload = {}
     
         ACCESSORS.each do |k|
    -     next if (val = send k) == nil
    +      next if (val = send k) == nil
          payload[k] = val
         end
     
    diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
    index 20dd2818af..5b9fb8987d 100644
    --- a/lib/site_setting_extension.rb
    +++ b/lib/site_setting_extension.rb
    @@ -169,13 +169,13 @@ module SiteSettingExtension
       end
     
       def client_settings_json_uncached
    -    MultiJson.dump(Hash[*@client_settings.map{|n| [n, self.send(n)]}.flatten])
    +    MultiJson.dump(Hash[*@client_settings.map { |n| [n, self.send(n)] }.flatten])
       end
     
       # Retrieve all settings
    -  def all_settings(include_hidden=false)
    +  def all_settings(include_hidden = false)
         @defaults
    -      .reject{|s, _| hidden_settings.include?(s) && !include_hidden}
    +      .reject { |s, _| hidden_settings.include?(s) && !include_hidden }
           .map do |s, v|
             value = send(s)
             type = types[get_data_type(s, value)]
    @@ -190,11 +190,13 @@ module SiteSettingExtension
             }
     
             if type == :enum && enum_class(s)
    -          opts.merge!({valid_values: enum_class(s).values, translate_names: enum_class(s).translate_names?})
    +          opts.merge!(valid_values: enum_class(s).values, translate_names: enum_class(s).translate_names?)
             elsif type == :enum
    -          opts.merge!({valid_values: choices[s].map{|c| {name: c, value: c}}, translate_names: false})
    +          opts.merge!(valid_values: choices[s].map { |c| { name: c, value: c } }, translate_names: false)
             end
     
    +        opts[:textarea] = true if static_types[s] == :textarea
    +
             opts[:choices] = choices[s] if choices.has_key? s
             opts
           end
    @@ -217,7 +219,7 @@ module SiteSettingExtension
           ensure_listen_for_changes
           old = current
     
    -      new_hash =  Hash[*(provider.all.map { |s|
    +      new_hash = Hash[*(provider.all.map { |s|
             [s.name.intern, convert(s.value, s.data_type, s.name)]
           }.to_a.flatten)]
     
    @@ -282,6 +284,8 @@ module SiteSettingExtension
       def add_override!(name, val)
         type = get_data_type(name, defaults[name.to_sym])
     
    +    val = val.to_s if type == types[:string]
    +
         if type == types[:bool] && val != true && val != false
           val = (val == "t" || val == "true") ? 't' : 'f'
         end
    @@ -321,11 +325,11 @@ module SiteSettingExtension
       end
     
       def notify_changed!
    -    MessageBus.publish('/site_settings', {process: process_id})
    +    MessageBus.publish('/site_settings', process: process_id)
       end
     
       def notify_clients!(name)
    -    MessageBus.publish('/client_settings', {name: name, value: self.send(name)})
    +    MessageBus.publish('/client_settings', name: name, value: self.send(name))
       end
     
       def has_setting?(name)
    @@ -367,7 +371,7 @@ module SiteSettingExtension
         end
       end
     
    -  def set_and_log(name, value, user=Discourse.system_user)
    +  def set_and_log(name, value, user = Discourse.system_user)
         prev_value = send(name)
         set(name, value)
         StaffActionLogger.new(user).log_site_setting_change(name, prev_value, value) if has_setting?(name)
    @@ -385,14 +389,14 @@ module SiteSettingExtension
         deletions = []
     
         new_hash.each do |name, value|
    -      changes << [name,value] if !old.has_key?(name) || old[name] != value
    +      changes << [name, value] if !old.has_key?(name) || old[name] != value
         end
     
    -    old.each do |name,value|
    -      deletions << [name,value] unless new_hash.has_key?(name)
    +    old.each do |name, value|
    +      deletions << [name, value] unless new_hash.has_key?(name)
         end
     
    -    [changes,deletions]
    +    [changes, deletions]
       end
     
       def get_data_type(name, val)
    diff --git a/lib/site_settings/db_provider.rb b/lib/site_settings/db_provider.rb
    index 7757299fbd..4410e05eee 100644
    --- a/lib/site_settings/db_provider.rb
    +++ b/lib/site_settings/db_provider.rb
    @@ -33,8 +33,8 @@ class SiteSettings::DbProvider
         model ||= @model.new
     
         model.name = name
    -    model.value =  value
    -    model.data_type =  data_type
    +    model.value = value
    +    model.data_type = data_type
     
         # save! used to ensure after_commit is called
         model.save!
    diff --git a/lib/slug.rb b/lib/slug.rb
    index 0f3b62b3f0..5ee570198e 100644
    --- a/lib/slug.rb
    +++ b/lib/slug.rb
    @@ -5,11 +5,12 @@ module Slug
       CHAR_FILTER_REGEXP = /[:\/\?#\[\]@!\$&'\(\)\*\+,;=_\.~%\\`^\s|\{\}"<>]+/ # :/?#[]@!$&'()*+,;=_.~%\`^|{}"<>
     
       def self.for(string, default = 'topic')
    -    slug = case (SiteSetting.slug_generation_method || :ascii).to_sym
    -           when :ascii then self.ascii_generator(string)
    -           when :encoded then self.encoded_generator(string)
    -           when :none then self.none_generator(string)
    -           end
    +    slug =
    +      case (SiteSetting.slug_generation_method || :ascii).to_sym
    +      when :ascii then self.ascii_generator(string)
    +      when :encoded then self.encoded_generator(string)
    +      when :none then self.none_generator(string)
    +      end
         # Reject slugs that only contain numbers, because they would be indistinguishable from id's.
         slug = (slug =~ /[^\d]/ ? slug : '')
         slug.blank? ? default : slug
    @@ -23,8 +24,8 @@ module Slug
     
       def self.ascii_generator(string)
         string.tr("'", "")
    -          .parameterize
    -          .tr("_", "-")
    +      .parameterize
    +      .tr("_", "-")
       end
     
       def self.encoded_generator(string)
    @@ -32,10 +33,10 @@ module Slug
         # including reserved characters from RFC3986.
         # See also URI::REGEXP::PATTERN.
         string.strip
    -          .gsub(/\s+/, '-')
    -          .gsub(CHAR_FILTER_REGEXP, '')
    -          .gsub(/\A-+|-+\z/, '') # remove possible trailing and preceding dashes
    -          .squeeze('-') # squeeze continuous dashes to prettify slug
    +      .gsub(/\s+/, '-')
    +      .gsub(CHAR_FILTER_REGEXP, '')
    +      .gsub(/\A-+|-+\z/, '') # remove possible trailing and preceding dashes
    +      .squeeze('-') # squeeze continuous dashes to prettify slug
       end
     
       def self.none_generator(string)
    diff --git a/lib/source_url.rb b/lib/source_url.rb
    index 41d4fe5dec..90a4391f5b 100644
    --- a/lib/source_url.rb
    +++ b/lib/source_url.rb
    @@ -6,7 +6,7 @@ class SourceURL < Tilt::Template
         source = input[:data]
         context = input[:environment].context_class.new(input)
     
    -    result = new(filename){source}.render(context)
    +    result = new(filename) { source }.render(context)
         context.metadata.merge(data: result)
       end
     
    diff --git a/lib/spam_handler.rb b/lib/spam_handler.rb
    index b14532c27d..2b8253512c 100644
    --- a/lib/spam_handler.rb
    +++ b/lib/spam_handler.rb
    @@ -4,15 +4,15 @@ class SpamHandler
         return false if SiteSetting.max_new_accounts_per_registration_ip <= 0
     
         tl2_plus_accounts_with_same_ip = User.where("trust_level >= ?", TrustLevel[2])
    -                                         .where(ip_address: ip_address.to_s)
    -                                         .count
    +      .where(ip_address: ip_address.to_s)
    +      .count
     
         return false if tl2_plus_accounts_with_same_ip > 0
     
         staff_user_ids = Group[:staff].user_ids - [-1]
         staff_members_with_same_ip = User.where(id: staff_user_ids)
    -                                     .where(ip_address: ip_address.to_s)
    -                                     .count
    +      .where(ip_address: ip_address.to_s)
    +      .count
     
         return false if staff_members_with_same_ip > 0
     
    @@ -20,9 +20,9 @@ class SpamHandler
         return false if ip_whitelisted
     
         tl0_accounts_with_same_ip = User.unscoped
    -                                    .where(trust_level: TrustLevel[0])
    -                                    .where(ip_address: ip_address.to_s)
    -                                    .count
    +      .where(trust_level: TrustLevel[0])
    +      .where(ip_address: ip_address.to_s)
    +      .count
     
         tl0_accounts_with_same_ip >= SiteSetting.max_new_accounts_per_registration_ip
       end
    diff --git a/lib/sql_builder.rb b/lib/sql_builder.rb
    index 70e461095c..35598c5723 100644
    --- a/lib/sql_builder.rb
    +++ b/lib/sql_builder.rb
    @@ -1,13 +1,13 @@
     class SqlBuilder
     
    -  def initialize(template,klass=nil)
    +  def initialize(template, klass = nil)
         @args = {}
         @sql = template
         @sections = {}
         @klass = klass
       end
     
    -  [:set, :where2,:where,:order_by,:limit,:left_join,:join,:offset, :select].each do |k|
    +  [:set, :where2, :where, :order_by, :limit, :left_join, :join, :offset, :select].each do |k|
         define_method k do |data, args = {}|
           @args.merge!(args)
           @sections[k] ||= []
    @@ -18,7 +18,7 @@ class SqlBuilder
     
       def secure_category(secure_category_ids, category_alias = 'c')
         if secure_category_ids.present?
    -      where("NOT COALESCE(" << category_alias << ".read_restricted, false) OR " << category_alias <<  ".id IN (:secure_category_ids)", secure_category_ids: secure_category_ids)
    +      where("NOT COALESCE(" << category_alias << ".read_restricted, false) OR " << category_alias << ".id IN (:secure_category_ids)", secure_category_ids: secure_category_ids)
         else
           where("NOT COALESCE(" << category_alias << ".read_restricted, false)")
         end
    @@ -28,17 +28,17 @@ class SqlBuilder
       def to_sql
         sql = @sql.dup
     
    -    @sections.each do |k,v|
    +    @sections.each do |k, v|
           joined = nil
           case k
           when :select
             joined = "SELECT " << v.join(" , ")
           when :where, :where2
    -        joined = "WHERE " << v.map{|c| "(" << c << ")" }.join(" AND ")
    +        joined = "WHERE " << v.map { |c| "(" << c << ")" }.join(" AND ")
           when :join
    -        joined = v.map{|item| "JOIN " << item }.join("\n")
    +        joined = v.map { |item| "JOIN " << item }.join("\n")
           when :left_join
    -        joined = v.map{|item| "LEFT JOIN " << item }.join("\n")
    +        joined = v.map { |item| "LEFT JOIN " << item }.join("\n")
           when :limit
             joined = "LIMIT " << v.last.to_s
           when :offset
    @@ -64,7 +64,7 @@ class SqlBuilder
           if @args == {}
             ActiveRecord::Base.exec_sql(sql)
           else
    -        ActiveRecord::Base.exec_sql(sql,@args)
    +        ActiveRecord::Base.exec_sql(sql, @args)
           end
         end
       end
    @@ -74,7 +74,7 @@ class SqlBuilder
       end
     
       class RailsDateTimeDecoder < PG::SimpleDecoder
    -    def decode(string, tuple=nil, field=nil)
    +    def decode(string, tuple = nil, field = nil)
           if Rails.version >= "4.2.0"
             @caster ||= ActiveRecord::Type::DateTime.new
             @caster.type_cast_from_database(string)
    @@ -84,14 +84,13 @@ class SqlBuilder
         end
       end
     
    -
       class ActiveRecordTypeMap < PG::BasicTypeMapForResults
         def initialize(connection)
           super(connection)
           rm_coder 0, 1114
           add_coder RailsDateTimeDecoder.new(name: "timestamp", oid: 1114, format: 0)
    -      # we don't need deprecations
    -     	self.default_type_map = PG::TypeMapInRuby.new
    +       # we don't need deprecations
    +       self.default_type_map = PG::TypeMapInRuby.new
         end
       end
     
    diff --git a/lib/stats_socket.rb b/lib/stats_socket.rb
    index 2b81db678f..e1afc677ba 100644
    --- a/lib/stats_socket.rb
    +++ b/lib/stats_socket.rb
    @@ -13,6 +13,14 @@ class StatsSocket < SocketServer
           case command
           when "gc_stat"
             GC.stat.to_json
    +      when "v8_stat"
    +        stats = {}
    +        ObjectSpace.each_object(MiniRacer::Context) do |context|
    +          context.heap_stats.each do |k, v|
    +            stats[k] = (stats[k] || 0) + v
    +          end
    +        end
    +        stats.to_json
           else
             "[\"UNKNOWN COMMAND\"]"
           end
    diff --git a/lib/stylesheet/compiler.rb b/lib/stylesheet/compiler.rb
    index e6a7530b88..e886c6bdce 100644
    --- a/lib/stylesheet/compiler.rb
    +++ b/lib/stylesheet/compiler.rb
    @@ -15,7 +15,7 @@ module Stylesheet
           footer:after { content: '#{error}' }"
         end
     
    -    def self.compile_asset(asset, options={})
    +    def self.compile_asset(asset, options = {})
     
           if Importer.special_imports[asset.to_s]
             filename = "theme.scss"
    @@ -26,12 +26,12 @@ module Stylesheet
             file = File.read path
           end
     
    -      compile(file,filename,options)
    +      compile(file, filename, options)
     
         end
     
    -    def self.compile(stylesheet, filename, options={})
    -      source_map_file = options[:source_map_file] || "#{filename.sub(".scss","")}.css.map";
    +    def self.compile(stylesheet, filename, options = {})
    +      source_map_file = options[:source_map_file] || "#{filename.sub(".scss", "")}.css.map";
     
           engine = SassC::Engine.new(stylesheet,
                                      importer: Importer,
    @@ -44,7 +44,6 @@ module Stylesheet
                                      theme_field: options[:theme_field],
                                      load_paths: [ASSET_ROOT])
     
    -
           result = engine.render
     
           if options[:rtl]
    diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb
    index ebceac3269..85cd6bd083 100644
    --- a/lib/stylesheet/manager.rb
    +++ b/lib/stylesheet/manager.rb
    @@ -16,7 +16,7 @@ class Stylesheet::Manager
       end
     
       def self.clear_theme_cache!
    -    cache.hash.keys.select{|k| k =~ /theme/}.each{|k|cache.delete(k)}
    +    cache.hash.keys.select { |k| k =~ /theme/ }.each { |k|cache.delete(k) }
       end
     
       def self.stylesheet_href(target = :desktop, theme_key = :missing)
    @@ -54,9 +54,9 @@ class Stylesheet::Manager
       end
     
       def self.precompile_css
    -    themes = Theme.where('user_selectable OR key = ?', SiteSetting.default_theme_key).pluck(:key,:name)
    +    themes = Theme.where('user_selectable OR key = ?', SiteSetting.default_theme_key).pluck(:key, :name)
         themes << nil
    -    themes.each do |key,name|
    +    themes.each do |key, name|
           [:desktop, :mobile, :desktop_rtl, :mobile_rtl].each do |target|
             theme_key = key || SiteSetting.default_theme_key
             cache_key = "#{target}_#{theme_key}"
    @@ -104,7 +104,7 @@ class Stylesheet::Manager
         @theme_key = theme_key
       end
     
    -  def compile(opts={})
    +  def compile(opts = {})
         unless opts[:force]
           if File.exists?(stylesheet_fullpath)
             unless StylesheetCache.where(target: qualified_target, digest: digest).exists?
    @@ -120,7 +120,7 @@ class Stylesheet::Manager
         end
     
         rtl = @target.to_s =~ /_rtl$/
    -    css,source_map = begin
    +    css, source_map = begin
           Stylesheet::Compiler.compile_asset(
             @target,
              rtl: rtl,
    @@ -216,7 +216,7 @@ class Stylesheet::Manager
       end
     
       def stylesheet_filename_no_digest
    -    stylesheet_filename(_with_digest=false)
    +    stylesheet_filename(_with_digest = false)
       end
     
       def is_theme?
    diff --git a/lib/stylesheet/watcher.rb b/lib/stylesheet/watcher.rb
    index 138d750fd6..e7c9a5ab9b 100644
    --- a/lib/stylesheet/watcher.rb
    +++ b/lib/stylesheet/watcher.rb
    @@ -3,7 +3,7 @@ require 'listen'
     module Stylesheet
       class Watcher
     
    -    def self.watch(paths=nil)
    +    def self.watch(paths = nil)
           watcher = new(paths)
           watcher.start
           watcher
    @@ -26,7 +26,6 @@ module Stylesheet
             end
           end
     
    -
           root = Rails.root.to_s
           @paths.each do |watch|
             Thread.new do
    @@ -34,7 +33,7 @@ module Stylesheet
                 listener = Listen.to("#{root}/#{watch}", ignore: /xxxx/) do |modified, added, _|
                   paths = [modified, added].flatten
                   paths.compact!
    -              paths.map!{|long| long[(root.length+1)..-1]}
    +              paths.map! { |long| long[(root.length + 1)..-1] }
                   process_change(paths)
                 end
               rescue => e
    @@ -55,7 +54,7 @@ module Stylesheet
           Stylesheet::Manager.cache.clear
     
           message = ["desktop", "mobile", "admin"].map do |name|
    -        {target: name, new_href: Stylesheet::Manager.stylesheet_href(name.to_sym) , theme_key: SiteSetting.default_theme_key}
    +        { target: name, new_href: Stylesheet::Manager.stylesheet_href(name.to_sym) , theme_key: SiteSetting.default_theme_key }
           end
     
           MessageBus.publish '/file-change', message
    diff --git a/lib/suggested_topics_builder.rb b/lib/suggested_topics_builder.rb
    index 9436bb5c5a..cd857875b1 100644
    --- a/lib/suggested_topics_builder.rb
    +++ b/lib/suggested_topics_builder.rb
    @@ -11,15 +11,14 @@ class SuggestedTopicsBuilder
         @results = []
       end
     
    -
    -  def add_results(results, priority=:low)
    +  def add_results(results, priority = :low)
     
         # WARNING .blank? will execute an Active Record query
         return unless results
     
         # Only add results if we don't have those topic ids already
         results = results.where('topics.id NOT IN (?)', @excluded_topic_ids)
    -                     .where(visible: true)
    +      .where(visible: true)
     
         # If limit suggested to category is enabled, restrict to that category
         if @category_id && SiteSetting.limit_suggested_to_category?
    @@ -29,8 +28,8 @@ class SuggestedTopicsBuilder
     
         unless results.empty?
           # Keep track of the ids we've added
    -      @excluded_topic_ids.concat results.map {|r| r.id}
    -      splice_results(results,priority)
    +      @excluded_topic_ids.concat results.map { |r| r.id }
    +      splice_results(results, priority)
         end
       end
     
    @@ -40,7 +39,7 @@ class SuggestedTopicsBuilder
           # Topics from category @category_id need to be first in the list, all others after.
     
           other_category_index = @results.index { |r| r.category_id != @category_id }
    -      category_results, other_category_results = results.partition{ |r| r.category_id == @category_id }
    +      category_results, other_category_results = results.partition { |r| r.category_id == @category_id }
     
           if other_category_index
             @results.insert other_category_index, *category_results
    @@ -66,7 +65,7 @@ class SuggestedTopicsBuilder
       end
     
       def category_results_left
    -    SiteSetting.suggested_topics - @results.count{|r| r.category_id == @category_id}
    +    SiteSetting.suggested_topics - @results.count { |r| r.category_id == @category_id }
       end
     
       def size
    diff --git a/lib/table_migration_helper.rb b/lib/table_migration_helper.rb
    index 4d0308341b..605555d49f 100644
    --- a/lib/table_migration_helper.rb
    +++ b/lib/table_migration_helper.rb
    @@ -45,7 +45,7 @@ SQL
                                             new_name: new_name,
                                             delay: "#{delay.to_i || 0} seconds",
                                             after_migration: after_migration).to_a.length > 0
    -        on_drop&.call
    +      on_drop&.call
     
             ActiveRecord::Base.exec_sql("DROP TABLE #{old_name}")
         end
    diff --git a/lib/tasks/add_topic_to_quotes.rake b/lib/tasks/add_topic_to_quotes.rake
    index 51d1cf2edd..ff36e46bf0 100644
    --- a/lib/tasks/add_topic_to_quotes.rake
    +++ b/lib/tasks/add_topic_to_quotes.rake
    @@ -6,4 +6,3 @@ task "add_topic_to_quotes" => :environment do
         Post.update_all ["raw = ?, cooked = ?", new_raw, new_cooked], ["id = ?", p.id]
       end
     end
    -
    diff --git a/lib/tasks/admin.rake b/lib/tasks/admin.rake
    index cd7e40d39f..c57e03b785 100644
    --- a/lib/tasks/admin.rake
    +++ b/lib/tasks/admin.rake
    @@ -1,6 +1,6 @@
     
     desc "invite an admin to this discourse instance"
    -task "admin:invite", [:email] => [:environment] do |_,args|
    +task "admin:invite", [:email] => [:environment] do |_, args|
       email = args[:email]
       if !email || email !~ /@/
         puts "ERROR: Expecting rake admin:invite[some@email.com]"
    @@ -20,7 +20,7 @@ task "admin:invite", [:email] => [:environment] do |_,args|
       puts "Granting admin!"
       user.grant_admin!
       user.change_trust_level!(4)
    -  user.email_tokens.update_all  confirmed: true
    +  user.email_tokens.update_all confirmed: true
     
       puts "Sending email!"
       email_token = user.email_tokens.create(email: user.email)
    @@ -42,8 +42,8 @@ task "admin:create" => :environment do
           reset_password = ask("User with this email already exists! Do you want to reset the password for this email? (Y/n)  ")
           if (reset_password == "" || reset_password.downcase == 'y')
             begin
    -          password = ask("Password:  ") {|q| q.echo = false}
    -          password_confirmation = ask("Repeat password:  ") {|q| q.echo = false}
    +          password = ask("Password:  ") { |q| q.echo = false }
    +          password_confirmation = ask("Repeat password:  ") { |q| q.echo = false }
             end while password != password_confirmation
             admin.password = password
           end
    @@ -53,8 +53,8 @@ task "admin:create" => :environment do
           admin.email = email
           admin.username = UserNameSuggester.suggest(admin.email)
           begin
    -        password = ask("Password:  ") {|q| q.echo = false}
    -        password_confirmation = ask("Repeat password:  ") {|q| q.echo = false}
    +        password = ask("Password:  ") { |q| q.echo = false }
    +        password_confirmation = ask("Repeat password:  ") { |q| q.echo = false }
           end while password != password_confirmation
           admin.password = password
         end
    @@ -82,7 +82,7 @@ task "admin:create" => :environment do
       if (grant_admin == "" || grant_admin.downcase == 'y')
         admin.grant_admin!
         admin.change_trust_level!(4)
    -    admin.email_tokens.update_all  confirmed: true
    +    admin.email_tokens.update_all confirmed: true
         admin.activate
     
         say("\nYour account now has Admin privileges!")
    diff --git a/lib/tasks/api.rake b/lib/tasks/api.rake
    index ef42e8a7bd..5a4ef6c85e 100644
    --- a/lib/tasks/api.rake
    +++ b/lib/tasks/api.rake
    @@ -1,6 +1,6 @@
     desc "generate api key if missing, return existing if already there"
     task "api_key:get" => :environment do
    -    api_key = ApiKey.create_master_key
    +  api_key = ApiKey.create_master_key
     
         puts api_key.key
     end
    diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake
    index 57235096c1..9029f9cf4b 100644
    --- a/lib/tasks/assets.rake
    +++ b/lib/tasks/assets.rake
    @@ -71,10 +71,10 @@ def assets_path
       "#{Rails.root}/public/assets"
     end
     
    -def compress_node(from,to)
    +def compress_node(from, to)
       to_path = "#{assets_path}/#{to}"
       assets = cdn_relative_path("/assets")
    -  source_map_root = assets + ((d=File.dirname(from)) == "." ? "" : "/#{d}")
    +  source_map_root = assets + ((d = File.dirname(from)) == "." ? "" : "/#{d}")
       source_map_url = cdn_path "/assets/#{to}.map"
     
       cmd = "uglifyjs '#{assets_path}/#{from}' -p relative -c -m -o '#{to_path}' --source-map-root '#{source_map_root}' --source-map '#{assets_path}/#{to}.map' --source-map-url '#{source_map_url}'"
    @@ -89,7 +89,7 @@ def compress_node(from,to)
       result
     end
     
    -def compress_ruby(from,to)
    +def compress_ruby(from, to)
       data = File.read("#{assets_path}/#{from}")
     
       uglified, map = Uglifier.new(comments: :none,
    @@ -99,7 +99,7 @@ def compress_ruby(from,to)
                                      output_filename: File.basename(to)
                                    }
                                   )
    -                          .compile_with_map(data)
    +    .compile_with_map(data)
       dest = "#{assets_path}/#{to}"
     
       File.write(dest, uglified << "\n//# sourceMappingURL=#{cdn_path "/assets/#{to}.map"}")
    @@ -121,11 +121,11 @@ def brotli(path)
       end
     end
     
    -def compress(from,to)
    +def compress(from, to)
       if $node_uglify
    -    compress_node(from,to)
    +    compress_node(from, to)
       else
    -    compress_ruby(from,to)
    +    compress_ruby(from, to)
       end
     end
     
    @@ -148,10 +148,10 @@ task 'assets:precompile' => 'assets:precompile:before' do
         concurrent? do |proc|
           to_skip = Rails.configuration.assets.skip_minification || []
           manifest.files
    -              .select{|k,v| k =~ /\.js$/}
    -              .each do |file, info|
    +        .select { |k, v| k =~ /\.js$/ }
    +        .each do |file, info|
     
    -          path = "#{assets_path}/#{file}"
    +        path = "#{assets_path}/#{file}"
               _file = (d = File.dirname(file)) == "." ? "_#{file}" : "#{d}/_#{File.basename(file)}"
               _path = "#{assets_path}/#{_file}"
     
    @@ -164,7 +164,7 @@ task 'assets:precompile' => 'assets:precompile:before' do
                   # We can specify some files to never minify
                   unless (ENV["DONT_MINIFY"] == "1") || to_skip.include?(info['logical_path'])
                     FileUtils.mv(path, _path)
    -                compress(_file,file)
    +                compress(_file, file)
                   end
     
                   info["size"] = File.size(path)
    diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake
    index dde0cc0364..617f473c9b 100644
    --- a/lib/tasks/auto_annotate_models.rake
    +++ b/lib/tasks/auto_annotate_models.rake
    @@ -1,36 +1,34 @@
     # NOTE: only doing this in development as some production environments (Heroku)
     # NOTE: are sensitive to local FS writes, and besides -- it's just not proper
     # NOTE: to have a dev-mode tool do its thing in production.
    -if(Rails.env.development? || Rails.env.test?)
    +if (Rails.env.development? || Rails.env.test?)
       task :set_annotation_options do
         # You can override any of these by setting an environment variable of the
         # same name.
    -    Annotate.set_defaults({
    -      'position_in_routes'   => "before",
    -      'position_in_class'    => "after",
    -      'position_in_test'     => "before",
    -      'position_in_fixture'  => "before",
    -      'position_in_factory'  => "before",
    -      'show_indexes'         => "true",
    -      'simple_indexes'       => "false",
    -      'model_dir'            => "app/models",
    -      'include_version'      => "false",
    -      'require'              => "",
    -      'exclude_tests'        => "true",
    -      'exclude_fixtures'     => "true",
    -      'exclude_helpers'      => "true",
    -      'exclude_factories'    => "true",
    -      'exclude_serializers'  => "true",
    -      'exclude_controllers'  => "true",
    -      'ignore_model_sub_dir' => "false",
    -      'skip_on_db_migrate'   => "true",
    -      'format_bare'          => "true",
    -      'format_rdoc'          => "false",
    -      'format_markdown'      => "false",
    -      'sort'                 => "false",
    -      'force'                => "false",
    -      'trace'                => "false",
    -    })
    +    Annotate.set_defaults('position_in_routes' => "before",
    +                          'position_in_class'    => "after",
    +                          'position_in_test'     => "before",
    +                          'position_in_fixture'  => "before",
    +                          'position_in_factory'  => "before",
    +                          'show_indexes'         => "true",
    +                          'simple_indexes'       => "false",
    +                          'model_dir'            => "app/models",
    +                          'include_version'      => "false",
    +                          'require'              => "",
    +                          'exclude_tests'        => "true",
    +                          'exclude_fixtures'     => "true",
    +                          'exclude_helpers'      => "true",
    +                          'exclude_factories'    => "true",
    +                          'exclude_serializers'  => "true",
    +                          'exclude_controllers'  => "true",
    +                          'ignore_model_sub_dir' => "false",
    +                          'skip_on_db_migrate'   => "true",
    +                          'format_bare'          => "true",
    +                          'format_rdoc'          => "false",
    +                          'format_markdown'      => "false",
    +                          'sort'                 => "false",
    +                          'force'                => "false",
    +                          'trace'                => "false")
       end
     
     end
    diff --git a/lib/tasks/autospec.rake b/lib/tasks/autospec.rake
    index 3df52d0f05..2f1de2c486 100644
    --- a/lib/tasks/autospec.rake
    +++ b/lib/tasks/autospec.rake
    @@ -6,7 +6,7 @@ desc "Run all specs automatically as needed"
     task "autospec" => :environment do
       require 'autospec/manager'
     
    -  debug = ARGV.any?{ |a|  a == "d" || a == "debug" } || ENV["DEBUG"]
    +  debug = ARGV.any? { |a|  a == "d" || a == "debug" } || ENV["DEBUG"]
       force_polling = ARGV.any? { |a| a == "p" || a == "polling" }
       latency = ((ARGV.find { |a| a =~ /l=|latency=/ } || "").split("=")[1] || 3).to_i
     
    diff --git a/lib/tasks/avatars.rake b/lib/tasks/avatars.rake
    index 4c1da8fd54..5b2df506cf 100644
    --- a/lib/tasks/avatars.rake
    +++ b/lib/tasks/avatars.rake
    @@ -24,7 +24,7 @@ task "avatars:clean" => :environment do
       OptimizedImage.where("upload_id IN (SELECT custom_upload_id FROM user_avatars) OR
                             upload_id IN (SELECT gravatar_upload_id FROM user_avatars) OR
                             upload_id IN (SELECT uploaded_avatar_id FROM users)")
    -                .find_each do |optimized_image|
    +    .find_each do |optimized_image|
         optimized_image.destroy!
         putc "." if (i += 1) % 10 == 0
       end
    diff --git a/lib/tasks/backfill.thor b/lib/tasks/backfill.thor
    index a69a58803d..44afebbda5 100644
    --- a/lib/tasks/backfill.thor
    +++ b/lib/tasks/backfill.thor
    @@ -1,7 +1,6 @@
     class Backfill < Thor
       desc "link_titles", "Backfills link titles"
     
    -
       def link_titles
         require './config/environment'
         topic_links = TopicLink.where(crawled_at: nil, internal: false)
    @@ -13,4 +12,3 @@ class Backfill < Thor
         end
       end
     end
    -
    diff --git a/lib/tasks/build_test_topic.rake b/lib/tasks/build_test_topic.rake
    index e135cabc92..7c64a76566 100644
    --- a/lib/tasks/build_test_topic.rake
    +++ b/lib/tasks/build_test_topic.rake
    @@ -4,7 +4,6 @@ desc 'create pushstate/replacestate test topic'
     task 'build_test_topic' => :environment do
       puts 'Creating topic'
     
    -
       # Acceptable options:
       #
       #   raw                     - raw text of post
    @@ -33,7 +32,7 @@ task 'build_test_topic' => :environment do
         links = []
         [-30, -10, 10, 30].each do |offset|
           where = (post_number + offset)
    -      if where >= 1 and where <= 100
    +      if where >= (1) && where <= (100)
             links << "Link to ##{where}: #{topic_url}/#{where}"
           end
         end
    diff --git a/lib/tasks/cdn.rake b/lib/tasks/cdn.rake
    index c9b793cbf3..0e13c3ae8c 100644
    --- a/lib/tasks/cdn.rake
    +++ b/lib/tasks/cdn.rake
    @@ -34,7 +34,7 @@ task 'assets:prestage' => :environment do |t|
           "id" => config["id"],
           "login" => config["login"],
           "passwd" => config["password"],
    -      "json" => {"prefetch_paths" => asset}.to_json
    +      "json" => { "prefetch_paths" => asset }.to_json
         )
     
         response = http.request(request)
    @@ -43,7 +43,7 @@ task 'assets:prestage' => :environment do |t|
           failed_assets.push(asset)
         end
       end
    -  
    +
       if failed_assets.length > 0
         raise "Failed to pre-stage #{failed_assets.length}/#{assets.length} files"
       end
    diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake
    index aba9e4df45..64a4514ae7 100644
    --- a/lib/tasks/docker.rake
    +++ b/lib/tasks/docker.rake
    @@ -1,3 +1,29 @@
    +# rake docker:test is designed to be used inside the discourse/docker_test image
    +# running it anywhere else will likely fail
    +#
    +# Environment Variables (specific to this rake task)
    +# => SKIP_CORE                 set to 1 to skip core tests (rspec and qunit)
    +# => SKIP_PLUGINS              set to 1 to skip plugin tests (rspec and qunit)
    +# => INSTALL_OFFICIAL_PLUGINS  set to 1 to install all core plugins before running tests
    +# => RUBY_ONLY                 set to 1 to skip all qunit tests
    +# => JS_ONLY                   set to 1 to skip all rspec tests
    +# => SINGLE_PLUGIN             set to plugin name to only run plugin-specific rspec tests (you'll probably want to SKIP_CORE as well)
    +# => BISECT                    set to 1 to run rspec --bisect (applies to core rspec tests only)
    +# => RSPEC_SEED                set to seed to use for rspec tests (applies to core rspec tests only)
    +#
    +# Other useful environment variables (not specific to this rake task)
    +# => COMMIT_HASH    used by the discourse_test docker image to load a specific commit of discourse
    +#                   this can also be set to a branch, e.g. "origin/tests-passed"
    +#
    +# Example usage:
    +#   Run all core and plugin tests:
    +#       docker run discourse/discourse_test:release
    +#   Run only rspec tests:
    +#       docker run -e RUBY_ONLY=1 discourse/discourse_test:release
    +#   Run all plugin tests (with a plugin mounted from host filesystem):
    +#       docker run -e SKIP_CORE=1 -v $(pwd)/my-awesome-plugin:/var/www/discourse/plugins/my-awesome-plugin discourse/discourse_test:release
    +#   Run tests for a specific plugin (with a plugin mounted from host filesystem):
    +#       docker run -e SKIP_CORE=1 SINGLE_PLUGIN='my-awesome-plugin' -v $(pwd)/my-awesome-plugin:/var/www/discourse/plugins/my-awesome-plugin discourse/discourse_test:release
     
     def run_or_fail(command)
       pid = Process.spawn(command)
    @@ -26,23 +52,55 @@ task 'docker:test' do
         puts "Starting postgres"
         @pg_pid = Process.spawn("#{@postgres_bin}postmaster -D tmp/test_data/pg")
     
    -
         ENV["RAILS_ENV"] = "test"
     
         @good = run_or_fail("bundle exec rake db:create db:migrate")
    -    unless ENV["JS_ONLY"]
    -      @good &&= run_or_fail("bundle exec rspec")
     
    -      if ENV["LOAD_PLUGINS"]
    -        @good &&= run_or_fail("bundle exec rake plugin:spec")
    -      end
    +    if ENV["INSTALL_OFFICIAL_PLUGINS"]
    +      @good &&= run_or_fail("bundle exec rake plugin:install_all_official")
         end
    +
    +    unless ENV["JS_ONLY"]
    +
    +      unless ENV["SKIP_CORE"]
    +        params = []
    +        if ENV["BISECT"]
    +          params << "--bisect"
    +        end
    +        if ENV["RSPEC_SEED"]
    +          params << "--seed #{ENV["RSPEC_SEED"]}"
    +        end
    +        @good &&= run_or_fail("bundle exec rspec #{params.join(' ')}".strip)
    +      end
    +
    +      unless ENV["SKIP_PLUGINS"]
    +        if ENV["SINGLE_PLUGIN"]
    +          @good &&= run_or_fail("bundle exec rake plugin:spec['#{ENV["SINGLE_PLUGIN"]}']")
    +        else
    +          @good &&= run_or_fail("bundle exec rake plugin:spec")
    +        end
    +      end
    +
    +    end
    +
         unless ENV["RUBY_ONLY"]
    -      @good &&= run_or_fail("eslint app/assets/javascripts")
    -      @good &&= run_or_fail("eslint --ext .es6 app/assets/javascripts")
    -      @good &&= run_or_fail("eslint --ext .es6 test/javascripts")
    -      @good &&= run_or_fail("eslint test/javascripts")
    -      @good &&= run_or_fail("bundle exec rake qunit:test['600000']")
    +      unless ENV["SKIP_CORE"]
    +        @good &&= run_or_fail("eslint app/assets/javascripts")
    +        @good &&= run_or_fail("eslint --ext .es6 app/assets/javascripts")
    +        @good &&= run_or_fail("eslint --ext .es6 test/javascripts")
    +        @good &&= run_or_fail("eslint test/javascripts")
    +        @good &&= run_or_fail("bundle exec rake qunit:test['600000']")
    +        @good &&= run_or_fail("bundle exec rake qunit:test['600000','/wizard/qunit']")
    +      end
    +
    +      unless ENV["SKIP_PLUGINS"]
    +        if ENV["SINGLE_PLUGIN"]
    +          @good &&= run_or_fail("bundle exec rake plugin:qunit['#{ENV['SINGLE_PLUGIN']}','600000']")
    +        else
    +          @good &&= run_or_fail("bundle exec rake plugin:qunit['*','600000']")
    +        end
    +      end
    +
         end
     
       ensure
    diff --git a/lib/tasks/emoji.rake b/lib/tasks/emoji.rake
    index e1efd1ef73..5ff933cab7 100644
    --- a/lib/tasks/emoji.rake
    +++ b/lib/tasks/emoji.rake
    @@ -4,7 +4,7 @@ require "json"
     require "nokogiri"
     require "open-uri"
     
    -EMOJI_GROUPS_PATH ||= "app/assets/javascripts/discourse/lib/emoji/groups.js.es6"
    +EMOJI_GROUPS_PATH ||= "lib/emoji/groups.json"
     
     EMOJI_DB_PATH ||= "lib/emoji/db.json"
     
    @@ -432,7 +432,7 @@ def fix_incomplete_sets(emojis)
       end
     end
     
    -def generate_emoji_groups(emojis)
    +def generate_emoji_groups(keywords)
       puts "Generating groups..."
     
       list = open(EMOJI_ORDERING_URL).read
    @@ -446,15 +446,15 @@ def generate_emoji_groups(emojis)
           emoji_list_section = title_section.first.parent.parent.next_element
           emoji_list_section.css("a.plain img").each do |link|
             emoji_code = link.attr("title")
    -                         .scan(/U\+(.{4,5})\b/)
    -                         .flatten
    -                         .map { |code| code.downcase.strip }
    -                         .join("_")
    +          .scan(/U\+(.{4,5})\b/)
    +          .flatten
    +          .map { |code| code.downcase.strip }
    +          .join("_")
     
             emoji_char = code_to_emoji(emoji_code)
     
    -        if emoji = emojis[emoji_char]
    -          group["icons"] << emoji["name"]
    +        if emoji = keywords[emoji_char]
    +          group["icons"] << { name: emoji["name"], diversity: emoji["fitzpatrick_scale"] }
             end
           end
         end
    @@ -489,16 +489,16 @@ def write_db_json(emojis)
       # skin tones variations of emojis shouldn’t appear in autocomplete
       emojis_without_tones = emojis
         .select { |char, config|
    -      !FITZPATRICK_SCALE.any? { |scale|
    -        codepoints_to_code(char.codepoints, config["fitzpatrick_scale"])[scale]
    -      }
    -    }
    +                           !FITZPATRICK_SCALE.any? { |scale|
    +                             codepoints_to_code(char.codepoints, config["fitzpatrick_scale"])[scale]
    +                           }
    +                         }
         .map { |char, config|
    -      {
    -        "code" => codepoints_to_code(char.codepoints, config["fitzpatrick_scale"]).tr("_", "-"),
    -        "name" => config["name"]
    -      }
    +    {
    +      "code" => codepoints_to_code(char.codepoints, config["fitzpatrick_scale"]).tr("_", "-"),
    +      "name" => config["name"]
         }
    +  }
     
       emoji_with_tones = emojis
         .select { |code, config| config["fitzpatrick_scale"] }
    @@ -518,15 +518,7 @@ def write_js_groups(emojis, groups)
     
       confirm_overwrite(EMOJI_GROUPS_PATH)
     
    -  template = <