diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdd8fbe047..5625e71c9e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -232,15 +232,15 @@ jobs: - name: Core QUnit 1 working-directory: ./app/assets/javascripts/discourse - run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}" + run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}" --random timeout-minutes: 20 - name: Core QUnit 2 working-directory: ./app/assets/javascripts/discourse - run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}" + run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}" --random timeout-minutes: 20 - name: Core QUnit 3 working-directory: ./app/assets/javascripts/discourse - run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}" + run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}" --random timeout-minutes: 20 diff --git a/Gemfile b/Gemfile index 9cdfbf21a9..ffff6f2437 100644 --- a/Gemfile +++ b/Gemfile @@ -105,9 +105,7 @@ gem 'omniauth-oauth2', require: false gem 'omniauth-google-oauth2' -# Pinning oj until https://github.com/ohler55/oj/issues/699 is resolved. -# Segfaults and stuck processes after upgrading. -gem 'oj', '3.13.2' +gem 'oj' gem 'pg' gem 'mini_sql' @@ -135,6 +133,14 @@ gem 'cose', require: false gem 'addressable' gem 'json_schemer' +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1") + # net-smtp, net-imap and net-pop were removed from default gems in Ruby 3.1 + gem "net-smtp", "~> 0.2.1", require: false + gem "net-imap", "~> 0.2.1", require: false + gem "net-pop", "~> 0.1.1", require: false + gem "digest", "3.0.0", require: false +end + # Gems used only for assets and not required in production environments by default. # Allow everywhere for now cause we are allowing asset debugging in production group :assets do diff --git a/Gemfile.lock b/Gemfile.lock index 3faaace264..bcc24686c0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,8 +48,8 @@ GEM zeitwerk (~> 2.3) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) aws-eventstream (1.2.0) @@ -80,8 +80,8 @@ GEM rack (>= 0.9.0) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.9.4) - msgpack (~> 1.0) + bootsnap (1.10.3) + msgpack (~> 1.2) builder (3.2.4) bullet (7.0.1) activesupport (>= 3.0.0) @@ -97,7 +97,7 @@ GEM cose (1.2.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) - cppjieba_rb (0.3.3) + cppjieba_rb (0.4.2) crack (0.4.5) rexml crass (1.0.6) @@ -129,10 +129,10 @@ GEM sprockets (>= 3.3, < 4.1) ember-source (2.18.2) erubi (1.10.0) - excon (0.89.0) + excon (0.91.0) execjs (2.8.1) exifr (1.3.9) - fabrication (2.24.0) + fabrication (2.27.0) faker (2.19.0) i18n (>= 1.6, < 2) fakeweb (1.3.0) @@ -175,7 +175,7 @@ GEM hkdf (0.3.0) htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.8.11) + i18n (1.9.1) concurrent-ruby (~> 1.0) image_optim (0.31.1) exifr (~> 1.2, >= 1.2.2) @@ -184,8 +184,8 @@ GEM in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) image_size (3.0.1) - in_threads (1.5.4) - ipaddr (1.2.3) + in_threads (1.6.0) + ipaddr (1.2.4) jmespath (1.5.0) jquery-rails (4.4.0) rails-dom-testing (>= 1, < 3) @@ -231,32 +231,34 @@ GEM rack (>= 1.1.3) method_source (1.0.0) mini_mime (1.1.2) - mini_portile2 (2.6.1) - mini_racer (0.6.1) + mini_portile2 (2.7.1) + mini_racer (0.6.2) libv8-node (~> 16.10.0.0) mini_scheduler (0.13.0) sidekiq (>= 4.2.3) - mini_sql (1.1.3) + mini_sql (1.3.0) mini_suffix (0.3.3) ffi (~> 1.9) minitest (5.15.0) mocha (1.13.0) mock_redis (0.29.0) ruby2_keywords - msgpack (1.4.2) + msgpack (1.4.4) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) mustache (1.1.1) nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + nokogiri (1.13.1) + mini_portile2 (~> 2.7.0) racc (~> 1.4) - nokogiri (1.12.5-arm64-darwin) + nokogiri (1.13.1-aarch64-linux) racc (~> 1.4) - nokogiri (1.12.5-x86_64-darwin) + nokogiri (1.13.1-arm64-darwin) racc (~> 1.4) - nokogiri (1.12.5-x86_64-linux) + nokogiri (1.13.1-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.13.1-x86_64-linux) racc (~> 1.4) oauth (0.5.8) oauth2 (1.4.7) @@ -265,7 +267,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oj (3.13.2) + oj (3.13.11) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -298,7 +300,7 @@ GEM parallel parser (3.1.0.0) ast (~> 2.4.1) - pg (1.2.3) + pg (1.3.1) progress (3.6.0) pry (0.13.1) coderay (~> 1.1) @@ -309,7 +311,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.5.2) + puma (5.6.1) nio4r (~> 2.0) r2 (0.2.7) racc (1.6.0) @@ -341,7 +343,7 @@ GEM rainbow (3.1.1) raindrops (0.20.0) rake (13.0.6) - rb-fsevent (0.11.0) + rb-fsevent (0.11.1) rb-inotify (0.10.1) ffi (~> 1.0) rbtrace (0.4.14) @@ -353,7 +355,7 @@ GEM redis-namespace (1.8.1) redis (>= 3.0.4) regexp_parser (2.2.0) - request_store (1.5.0) + request_store (1.5.1) rack (>= 1.4) rexml (3.2.5) rinku (2.0.6) @@ -362,22 +364,22 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.2) + rspec (3.11.0) + rspec-core (~> 3.11.0) + rspec-expectations (~> 3.11.0) + rspec-mocks (~> 3.11.0) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) + rspec-support (~> 3.11.0) rspec-html-matchers (0.9.4) nokogiri (~> 1) rspec (>= 3.0.0.a, < 4) - rspec-mocks (3.10.2) + rspec-mocks (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-support (~> 3.11.0) + rspec-rails (5.1.0) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -385,15 +387,15 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.10.3) + rspec-support (3.11.0) rss (0.2.9) rexml - rswag-specs (2.4.0) - activesupport (>= 3.1, < 7.0) + rswag-specs (2.5.1) + activesupport (>= 3.1, < 7.1) json-schema (~> 2.2) - railties (>= 3.1, < 7.0) + railties (>= 3.1, < 7.1) rtlit (0.0.5) - rubocop (1.25.0) + rubocop (1.25.1) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) @@ -407,7 +409,7 @@ GEM rubocop-discourse (2.5.0) rubocop (>= 1.1.0) rubocop-rspec (>= 2.0.0) - rubocop-rspec (2.7.0) + rubocop-rspec (2.8.0) rubocop (~> 1.19) ruby-prof (1.4.3) ruby-progressbar (1.11.0) @@ -433,7 +435,7 @@ GEM activesupport (>= 3.1) shoulda-matchers (5.1.0) activesupport (>= 5.2.0) - sidekiq (6.3.1) + sidekiq (6.4.1) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) @@ -477,7 +479,7 @@ GEM jwt (~> 2.0) xorcist (1.1.2) yaml-lint (0.0.10) - zeitwerk (2.5.3) + zeitwerk (2.5.4) PLATFORMS aarch64-linux @@ -558,7 +560,7 @@ DEPENDENCIES multi_json mustache nokogiri - oj (= 3.13.2) + oj omniauth omniauth-facebook omniauth-github diff --git a/app/assets/javascripts/admin/addon/components/color-input.js b/app/assets/javascripts/admin/addon/components/color-input.js index c7a8da4b78..2974b43871 100644 --- a/app/assets/javascripts/admin/addon/components/color-input.js +++ b/app/assets/javascripts/admin/addon/components/color-input.js @@ -1,8 +1,6 @@ import { action, computed } from "@ember/object"; -import loadScript, { loadCSS } from "discourse/lib/load-script"; import Component from "@ember/component"; import { observes } from "discourse-common/utils/decorators"; -import { schedule } from "@ember/runloop"; /** An input field for a color. @@ -22,13 +20,25 @@ export default Component.extend({ return this.onlyHex ? 6 : null; }), + normalizedHexValue: computed("hexValue", function () { + return this.normalize(this.hexValue); + }), + normalize(color) { - if (/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) { + if (this._valid(color)) { if (!color.startsWith("#")) { color = "#" + color; } + if (color.length === 4) { + color = + "#" + + color + .slice(1) + .split("") + .map((hex) => hex + hex) + .join(""); + } } - return color; }, @@ -39,49 +49,25 @@ export default Component.extend({ } }, + @action + onPickerInput(event) { + this.set("hexValue", event.target.value.replace("#", "")); + }, + @observes("hexValue", "brightnessValue", "valid") hexValueChanged() { const hex = this.hexValue; - let text = this.element.querySelector("input.hex-input"); if (this.attrs.onChangeColor) { this.attrs.onChangeColor(this.normalize(hex)); } - if (this.valid) { - this.styleSelection && - text.setAttribute( - "style", - "color: " + - (this.brightnessValue > 125 ? "black" : "white") + - "; background-color: #" + - hex + - ";" - ); - - if (this.pickerLoaded) { - $(this.element.querySelector(".picker")).spectrum({ - color: "#" + hex, - }); - } - } else { - this.styleSelection && text.setAttribute("style", ""); + if (this._valid()) { + this.element.querySelector(".picker").value = this.normalize(hex); } }, - didInsertElement() { - loadScript("/javascripts/spectrum.js").then(() => { - loadCSS("/javascripts/spectrum.css").then(() => { - schedule("afterRender", () => { - $(this.element.querySelector(".picker")) - .spectrum({ color: "#" + this.hexValue }) - .on("change.spectrum", (me, color) => { - this.set("hexValue", color.toHexString().replace("#", "")); - }); - this.set("pickerLoaded", true); - }); - }); - }); - schedule("afterRender", () => this.hexValueChanged()); + _valid(color = this.hexValue) { + return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color); }, }); diff --git a/app/assets/javascripts/admin/addon/components/simple-list.js b/app/assets/javascripts/admin/addon/components/simple-list.js index 96136c98e2..46b8691e39 100644 --- a/app/assets/javascripts/admin/addon/components/simple-list.js +++ b/app/assets/javascripts/admin/addon/components/simple-list.js @@ -1,7 +1,7 @@ import Component from "@ember/component"; import { action } from "@ember/object"; import { empty } from "@ember/object/computed"; -import { on } from "discourse-common/utils/decorators"; +import discourseComputed, { on } from "discourse-common/utils/decorators"; export default Component.extend({ classNameBindings: [":simple-list", ":value-list"], @@ -47,10 +47,32 @@ export default Component.extend({ this._onChange(); }, + @action + shift(operation, index) { + let futureIndex = index + operation; + + if (futureIndex > this.collection.length - 1) { + futureIndex = 0; + } else if (futureIndex < 0) { + futureIndex = this.collection.length - 1; + } + + const shiftedValue = this.collection[index]; + this.collection.removeAt(index); + this.collection.insertAt(futureIndex, shiftedValue); + + this._onChange(); + }, + _onChange() { this.attrs.onChange && this.attrs.onChange(this.collection); }, + @discourseComputed("collection") + showUpDownButtons(collection) { + return collection.length - 1 ? true : false; + }, + _splitValues(values, delimiter) { return values && values.length ? values.split(delimiter || "\n").filter(Boolean) diff --git a/app/assets/javascripts/admin/addon/components/site-settings/host-list.js b/app/assets/javascripts/admin/addon/components/site-settings/host-list.js new file mode 100644 index 0000000000..8cc320da8f --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/site-settings/host-list.js @@ -0,0 +1,21 @@ +import Component from "@ember/component"; +import { action, computed } from "@ember/object"; + +export default Component.extend({ + tokenSeparator: "|", + choices: null, + + @computed("value") + get settingValue() { + return this.value.toString().split(this.tokenSeparator).filter(Boolean); + }, + + @action + onChange(value) { + if (value.some((v) => v.includes("?") || v.includes("*"))) { + return; + } + + this.set("value", value.join(this.tokenSeparator)); + }, +}); diff --git a/app/assets/javascripts/admin/addon/components/value-list.js b/app/assets/javascripts/admin/addon/components/value-list.js index 2daacfd8a4..57197548cb 100644 --- a/app/assets/javascripts/admin/addon/components/value-list.js +++ b/app/assets/javascripts/admin/addon/components/value-list.js @@ -59,6 +59,22 @@ export default Component.extend({ selectChoice(choice) { this._addValue(choice); }, + + shift(operation, index) { + let futureIndex = index + operation; + + if (futureIndex > this.collection.length - 1) { + futureIndex = 0; + } else if (futureIndex < 0) { + futureIndex = this.collection.length - 1; + } + + const shiftedValue = this.collection[index]; + this.collection.removeAt(index); + this.collection.insertAt(futureIndex, shiftedValue); + + this._saveValues(); + }, }, _addValue(value) { @@ -99,6 +115,11 @@ export default Component.extend({ this.set("values", this.collection.join(this.inputDelimiter || "\n")); }, + @discourseComputed("collection") + showUpDownButtons(collection) { + return collection.length - 1 ? true : false; + }, + _splitValues(values, delimiter) { if (values && values.length) { return values.split(delimiter).filter((x) => x); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js b/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js index d4f354d321..9993a19fe5 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js @@ -5,6 +5,7 @@ import Permalink from "admin/models/permalink"; import bootbox from "bootbox"; import discourseDebounce from "discourse-common/lib/debounce"; import { observes } from "discourse-common/utils/decorators"; +import { clipboardCopy } from "discourse/lib/utilities"; export default Controller.extend({ loading: false, @@ -29,12 +30,7 @@ export default Controller.extend({ copyUrl(pl) { let linkElement = document.querySelector(`#admin-permalink-${pl.id}`); - let textArea = document.createElement("textarea"); - textArea.value = linkElement.textContent; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand("Copy"); - textArea.remove(); + clipboardCopy(linkElement.textContent); }, destroy(record) { diff --git a/app/assets/javascripts/admin/addon/models/report.js b/app/assets/javascripts/admin/addon/models/report.js index fb1eef051f..55c1012eb1 100644 --- a/app/assets/javascripts/admin/addon/models/report.js +++ b/app/assets/javascripts/admin/addon/models/report.js @@ -163,9 +163,23 @@ const Report = EmberObject.extend({ return this._computeTrend(prev, total, higherIsBetter); }, - @discourseComputed("prev30Days", "lastThirtyDaysCount", "higher_is_better") - thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) { - return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter); + @discourseComputed( + "prev30Days", + "prev_period", + "lastThirtyDaysCount", + "higher_is_better" + ) + thirtyDaysTrend( + prev30Days, + prev_period, + lastThirtyDaysCount, + higherIsBetter + ) { + return this._computeTrend( + prev30Days ?? prev_period, + lastThirtyDaysCount, + higherIsBetter + ); }, @discourseComputed("type") @@ -236,10 +250,15 @@ const Report = EmberObject.extend({ ); }, - @discourseComputed("prev30Days", "lastThirtyDaysCount") - thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) { + @discourseComputed("prev30Days", "prev_period") + canDisplayTrendIcon(prev30Days, prev_period) { + return prev30Days ?? prev_period; + }, + + @discourseComputed("prev30Days", "prev_period", "lastThirtyDaysCount") + thirtyDaysCountTitle(prev30Days, prev_period, lastThirtyDaysCount) { return this.changeTitle( - prev30Days, + prev30Days ?? prev_period, lastThirtyDaysCount, "in the previous 30 day period" ); diff --git a/app/assets/javascripts/admin/addon/templates/admin.hbs b/app/assets/javascripts/admin/addon/templates/admin.hbs index d0a6be8bc5..8a7c76a106 100644 --- a/app/assets/javascripts/admin/addon/templates/admin.hbs +++ b/app/assets/javascripts/admin/addon/templates/admin.hbs @@ -7,7 +7,7 @@ {{#if currentUser.admin}} {{nav-item route="adminSiteSettings" label="admin.site_settings.title"}} {{/if}} - {{nav-item route="adminUsersList" label="admin.users.title"}} + {{nav-item route="adminUsers" label="admin.users.title"}} {{#if showGroups}} {{nav-item route="groups" label="admin.groups.title"}} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs index 7377a782b9..3dce5e6299 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-editable-field.hbs @@ -1,7 +1,7 @@
{{i18n name}}
{{#if editing}} - {{text-field value=buffer autofocus="autofocus" autocomplete="discourse"}} + {{text-field value=buffer autofocus="autofocus" autocomplete="off"}} {{else}} {{value}} diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-report-counters.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-report-counters.hbs index 05d456fdeb..b411a2571b 100644 --- a/app/assets/javascripts/admin/addon/templates/components/admin-report-counters.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/admin-report-counters.hbs @@ -18,7 +18,7 @@
{{number model.lastThirtyDaysCount}} - {{#if model.prev30Days}} + {{#if model.canDisplayTrendIcon}} {{d-icon model.thirtyDaysTrendIcon}} {{/if}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/simple-list.hbs b/app/assets/javascripts/admin/addon/templates/components/simple-list.hbs index 3e78f77b05..fab64bc92d 100644 --- a/app/assets/javascripts/admin/addon/templates/components/simple-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/simple-list.hbs @@ -15,8 +15,23 @@ class="value-input" focus-out=(action "changeValue" index) }} + + {{#if showUpDownButtons}} + {{d-button + action=(action "shift" -1 index) + icon="arrow-up" + class="shift-up-value-btn btn-small" + }} + {{d-button + action=(action "shift" 1 index) + icon="arrow-down" + class="shift-down-value-btn btn-small" + }} + {{/if}}
+ {{/each}} + {{/if}} @@ -26,9 +41,10 @@ value=newValue placeholderKey="admin.site_settings.simple_list.add_item" class="add-value-input" - autocomplete="discourse" + autocomplete="off" autocorrect="off" - autocapitalize="off"}} + autocapitalize="off" + }} {{d-button action=(action "addValue") diff --git a/app/assets/javascripts/admin/addon/templates/components/site-settings/host-list.hbs b/app/assets/javascripts/admin/addon/templates/components/site-settings/host-list.hbs index d1486292dd..3460bf78df 100644 --- a/app/assets/javascripts/admin/addon/templates/components/site-settings/host-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/site-settings/host-list.hbs @@ -1,3 +1,12 @@ -{{value-list values=value addKey="admin.site_settings.add_host"}} +{{list-setting + value=settingValue + settingName=setting.setting + choices=settingValue + onChange=(action "onChange") + options=(hash + allowAny=allowAny + ) +}} + {{setting-validation-message message=validationMessage}}
{{html-safe setting.description}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/themes-list.hbs b/app/assets/javascripts/admin/addon/templates/components/themes-list.hbs index ab342b1b93..5639dc42c1 100644 --- a/app/assets/javascripts/admin/addon/templates/components/themes-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/themes-list.hbs @@ -20,7 +20,8 @@ {{input class="filter-input" placeholder=(i18n "admin.customize.theme.filter_placeholder") - autocomplete="discourse" + autocomplete="off" + type="search" value=(mut filterTerm) }} {{d-icon "search"}} diff --git a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs index 94b1d12325..518922c965 100644 --- a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs @@ -15,6 +15,19 @@ class="value-input" focus-out=(action "changeValue" index) }} + + {{#if showUpDownButtons}} + {{d-button + action=(action "shift" -1 index) + icon="arrow-up" + class="shift-up-value-btn btn-small" + }} + {{d-button + action=(action "shift" 1 index) + icon="arrow-down" + class="shift-down-value-btn btn-small" + }} + {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse-common/addon/lib/get-url.js b/app/assets/javascripts/discourse-common/addon/lib/get-url.js index fa1ba6b5c1..4a5a0f1715 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/get-url.js +++ b/app/assets/javascripts/discourse-common/addon/lib/get-url.js @@ -1,6 +1,8 @@ let cdn, baseUrl, baseUri, baseUriMatcher; let S3BaseUrl, S3CDN; +let snapshot; + export default function getURL(url) { if (baseUri === undefined) { setPrefix($('meta[name="discourse-base-uri"]').attr("content") || ""); @@ -59,15 +61,43 @@ export function setPrefix(configBaseUri) { baseUriMatcher = new RegExp(`^${baseUri}(/|$)`); } -export function setupURL(configCdn, configBaseUrl, configBaseUri) { +export function setupURL(configCdn, configBaseUrl, configBaseUri, opts) { + opts = opts || {}; cdn = configCdn; baseUrl = configBaseUrl; setPrefix(configBaseUri); + + if (opts?.snapshot) { + snapshot = { + cdn, + baseUri, + baseUrl, + configBaseUrl, + baseUriMatcher, + }; + } } -export function setupS3CDN(configS3BaseUrl, configS3CDN) { +// In a test environment we might change these values and, after tests, want to restore them. +export function restoreBaseUri() { + if (snapshot) { + cdn = snapshot.cdn; + baseUri = snapshot.baseUri; + baseUrl = snapshot.baseUrl; + baseUriMatcher = snapshot.baseUriMatcher; + S3BaseUrl = snapshot.S3BaseUrl; + S3CDN = snapshot.S3CDN; + } +} + +export function setupS3CDN(configS3BaseUrl, configS3CDN, opts) { S3BaseUrl = configS3BaseUrl; S3CDN = configS3CDN; + if (opts?.snapshot) { + snapshot = snapshot || {}; + snapshot.S3BaseUrl = S3BaseUrl; + snapshot.S3CDN = S3CDN; + } } // We can use this to identify when navigating on the same host but outside of the diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js index deadd61cc9..13035912ea 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -1,12 +1,4 @@ -import { - LATER_TODAY_CUTOFF_HOUR, - MOMENT_THURSDAY, - laterToday, - now, - parseCustomDatetime, - startOfDay, - tomorrow, -} from "discourse/lib/time-utils"; +import { now, parseCustomDatetime, startOfDay } from "discourse/lib/time-utils"; import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark"; import Component from "@ember/component"; import I18n from "I18n"; @@ -305,9 +297,7 @@ export default Component.extend({ id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE, label: "time_shortcut.post_local_date", time: this._postLocalDate(), - timeFormatted: this._postLocalDate().format( - I18n.t("dates.long_no_year") - ), + timeFormatKey: "dates.long_no_year", hidden: false, }); } @@ -330,38 +320,13 @@ export default Component.extend({ editingExistingBookmark, existingBookmarkHasReminder ) { - if (!editingExistingBookmark) { - return []; - } - - if (!existingBookmarkHasReminder) { + if (editingExistingBookmark && !existingBookmarkHasReminder) { return [TIME_SHORTCUT_TYPES.NONE]; } return []; }, - @discourseComputed() - additionalTimeShortcutOptions() { - let additional = []; - - if ( - !laterToday(this.userTimezone).isSame( - tomorrow(this.userTimezone), - "date" - ) && - now(this.userTimezone).hour() < LATER_TODAY_CUTOFF_HOUR - ) { - additional.push(TIME_SHORTCUT_TYPES.LATER_TODAY); - } - - if (now(this.userTimezone).day() < MOMENT_THURSDAY) { - additional.push(TIME_SHORTCUT_TYPES.LATER_THIS_WEEK); - } - - return additional; - }, - @discourseComputed("model.reminderAt") existingReminderAtFormatted(existingReminderAt) { return formattedReminderTime(existingReminderAt, this.userTimezone); diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 1912fded1c..eed18fe3b1 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -100,6 +100,7 @@ export function cleanUpComposerUploadMarkdownResolver() { export default Component.extend(ComposerUploadUppy, { classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"], + editorClass: ".d-editor", fileUploadElementId: "file-uploader", mobileFileUploaderId: "mobile-file-upload", eventPrefix: "composer", @@ -199,7 +200,10 @@ export default Component.extend(ComposerUploadUppy, { @discourseComputed() acceptsAllFormats() { - return authorizesAllExtensions(this.currentUser.staff, this.siteSettings); + return ( + this.capabilities.isIOS || + authorizesAllExtensions(this.currentUser.staff, this.siteSettings) + ); }, @discourseComputed() diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 16d3796e42..80e63ce8fb 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -477,7 +477,7 @@ export default Component.extend(TextareaTextManipulation, { key: "#", afterComplete: (value) => { this.set("value", value); - return this._focusTextArea(); + schedule("afterRender", this, this._focusTextArea); }, transformComplete: (obj) => { return obj.text; @@ -504,7 +504,7 @@ export default Component.extend(TextareaTextManipulation, { key: ":", afterComplete: (text) => { this.set("value", text); - this._focusTextArea(); + schedule("afterRender", this, this._focusTextArea); }, onKeyUp: (text, cp) => { @@ -821,7 +821,6 @@ export default Component.extend(TextareaTextManipulation, { 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.value, toggleDirection: () => this._toggleDirection(), }; diff --git a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js index 1c07aceb5c..f3d984bcd5 100644 --- a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js +++ b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js @@ -14,13 +14,9 @@ import I18n from "I18n"; import { action } from "@ember/object"; import Component from "@ember/component"; import { isEmpty } from "@ember/utils"; -import { - MOMENT_MONDAY, - now, - startOfDay, - thisWeekend, -} from "discourse/lib/time-utils"; +import { MOMENT_MONDAY, now, startOfDay } from "discourse/lib/time-utils"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; +import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut"; import ItsATrap from "@discourse/itsatrap"; export default Component.extend({ @@ -86,26 +82,20 @@ export default Component.extend({ @discourseComputed() customTimeShortcutOptions() { + const timezone = this.currentUser.resolvedTimezone(this.currentUser); return [ - { - icon: "bed", - id: "this_weekend", - label: "time_shortcut.this_weekend", - time: thisWeekend(), - timeFormatKey: "dates.time_short_day", - }, { icon: "far-clock", id: "two_weeks", label: "time_shortcut.two_weeks", - time: startOfDay(now().add(2, "weeks").day(MOMENT_MONDAY)), + time: startOfDay(now(timezone).add(2, "weeks").day(MOMENT_MONDAY)), timeFormatKey: "dates.long_no_year", }, { icon: "far-calendar-plus", id: "six_months", label: "time_shortcut.six_months", - time: startOfDay(now().add(6, "months").startOf("month")), + time: startOfDay(now(timezone).add(6, "months").startOf("month")), timeFormatKey: "dates.long_no_year", }, ]; @@ -113,7 +103,11 @@ export default Component.extend({ @discourseComputed hiddenTimeShortcutOptions() { - return ["none"]; + return [ + TIME_SHORTCUT_TYPES.NONE, + TIME_SHORTCUT_TYPES.LATER_TODAY, + TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, + ]; }, isCustom: equal("timerType", "custom"), diff --git a/app/assets/javascripts/discourse/app/components/future-date-input.js b/app/assets/javascripts/discourse/app/components/future-date-input.js index 3f17a595ec..1367267b48 100644 --- a/app/assets/javascripts/discourse/app/components/future-date-input.js +++ b/app/assets/javascripts/discourse/app/components/future-date-input.js @@ -79,7 +79,6 @@ export default Component.extend({ now, day: now.day(), includeWeekend: this.includeWeekend, - includeMidFuture: this.includeMidFuture || true, includeFarFuture: this.includeFarFuture, includeDateTime: this.includeDateTime, canScheduleNow: this.includeNow || false, diff --git a/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js b/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js index d9758d2c46..52c6d27ab9 100644 --- a/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js +++ b/app/assets/javascripts/discourse/app/components/group-smtp-email-settings.js @@ -37,6 +37,7 @@ export default Component.extend({ EmberObject.create({ email_username: this.group.email_username, email_password: this.group.email_password, + email_from_alias: this.group.email_from_alias, smtp_server: this.group.smtp_server, smtp_port: (this.group.smtp_port || "").toString(), smtp_ssl: this.group.smtp_ssl, @@ -73,6 +74,7 @@ export default Component.extend({ smtp_port: this.form.smtp_port, smtp_ssl: this.form.smtp_ssl, email_username: this.form.email_username, + email_from_alias: this.form.email_from_alias, email_password: this.form.email_password, }); }) diff --git a/app/assets/javascripts/discourse/app/components/popup-input-tip.js b/app/assets/javascripts/discourse/app/components/popup-input-tip.js index 0ee73dbdc6..096e941bed 100644 --- a/app/assets/javascripts/discourse/app/components/popup-input-tip.js +++ b/app/assets/javascripts/discourse/app/components/popup-input-tip.js @@ -5,7 +5,7 @@ import { getOwner } from "discourse-common/lib/get-owner"; export default Component.extend({ classNameBindings: [":popup-tip", "good", "bad", "lastShownAt::hide"], - attributeBindings: ["role"], + attributeBindings: ["role", "ariaLabel"], rerenderTriggers: ["validation.reason"], tipReason: null, lastShownAt: or("shownAt", "validation.lastShownAt"), @@ -19,6 +19,11 @@ export default Component.extend({ } }, + @discourseComputed("validation.reason") + ariaLabel(reason) { + return reason?.replace(/(<([^>]+)>)/gi, ""); + }, + click() { this.set("shownAt", null); const composer = getOwner(this).lookup("controller:composer"); diff --git a/app/assets/javascripts/discourse/app/components/quote-button.js b/app/assets/javascripts/discourse/app/components/quote-button.js index f3af1940fd..a8a4de73de 100644 --- a/app/assets/javascripts/discourse/app/components/quote-button.js +++ b/app/assets/javascripts/discourse/app/components/quote-button.js @@ -5,6 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import { postUrl, selectedElement, + selectedRange, selectedText, setCaretPosition, translateModKey, @@ -164,10 +165,14 @@ export default Component.extend(KeyEnterEscape, { const cooked = $selectedElement.find(".cooked")[0] || $selectedElement.closest(".cooked")[0]; - const postBody = toMarkdown(cooked.innerHTML); + // computing markdown takes a lot of time on long posts + // this code attempts to compute it only when we can't fast track let opts = { - full: _selectedText === postBody, + full: + selectedRange().startOffset > 0 + ? false + : _selectedText === toMarkdown(cooked.innerHTML), }; for ( @@ -192,22 +197,24 @@ export default Component.extend(KeyEnterEscape, { this.topic.postStream.findLoadedPost(postId)?.can_edit ); - const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi"); - const matches = postBody.match(regexp); + if (this._canEditPost) { + const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi"); + const matches = cooked.innerHTML.match(regexp); - if ( - quoteState.buffer.length < 1 || - quoteState.buffer.includes("|") || // tables are too complex - quoteState.buffer.match(/\n/g) || // linebreaks are too complex - matches?.length > 1 // duplicates are too complex - ) { - this.set("_isFastEditable", false); - this.set("_fastEditInitalSelection", null); - this.set("_fastEditNewSelection", null); - } else if (matches?.length === 1) { - this.set("_isFastEditable", true); - this.set("_fastEditInitalSelection", quoteState.buffer); - this.set("_fastEditNewSelection", quoteState.buffer); + if ( + quoteState.buffer.length < 1 || + quoteState.buffer.includes("|") || // tables are too complex + quoteState.buffer.match(/\n/g) || // linebreaks are too complex + matches?.length > 1 // duplicates are too complex + ) { + this.set("_isFastEditable", false); + this.set("_fastEditInitalSelection", null); + this.set("_fastEditNewSelection", null); + } else if (matches?.length === 1) { + this.set("_isFastEditable", true); + this.set("_fastEditInitalSelection", quoteState.buffer); + this.set("_fastEditNewSelection", quoteState.buffer); + } } } diff --git a/app/assets/javascripts/discourse/app/components/search-text-field.js b/app/assets/javascripts/discourse/app/components/search-text-field.js index cad882f85c..520a90c552 100644 --- a/app/assets/javascripts/discourse/app/components/search-text-field.js +++ b/app/assets/javascripts/discourse/app/components/search-text-field.js @@ -4,7 +4,7 @@ import TextField from "discourse/components/text-field"; import { applySearchAutocomplete } from "discourse/lib/search"; export default TextField.extend({ - autocomplete: "discourse-search", + autocomplete: "off", @discourseComputed("searchService.searchContextEnabled") placeholder(searchContextEnabled) { diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js index a2ff789e43..5839191f48 100644 --- a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js +++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js @@ -1,4 +1,7 @@ import { + LATER_TODAY_CUTOFF_HOUR, + MOMENT_FRIDAY, + MOMENT_THURSDAY, START_OF_DAY_HOUR, laterToday, now, @@ -57,7 +60,6 @@ export default Component.extend({ selectedDatetime: null, prefilledDatetime: null, - additionalOptionsToShow: null, hiddenOptions: null, customOptions: null, @@ -76,7 +78,6 @@ export default Component.extend({ this.setProperties({ customTime: this.defaultCustomReminderTime, userTimezone: this.currentUser.resolvedTimezone(this.currentUser), - additionalOptionsToShow: this.additionalOptionsToShow || [], hiddenOptions: this.hiddenOptions || [], customOptions: this.customOptions || [], customLabels: this.customLabels || {}, @@ -168,38 +169,18 @@ export default Component.extend({ }, @discourseComputed( - "additionalOptionsToShow", "hiddenOptions", "customOptions", "customLabels", "userTimezone" ) - options( - additionalOptionsToShow, - hiddenOptions, - customOptions, - customLabels, - userTimezone - ) { + options(hiddenOptions, customOptions, customLabels, userTimezone) { this._loadLastUsedCustomDatetime(); let options = defaultShortcutOptions(userTimezone); - - if (additionalOptionsToShow.length > 0) { - options.forEach((opt) => { - if (additionalOptionsToShow.includes(opt.id)) { - opt.hidden = false; - } - }); - } - - customOptions.forEach((opt) => { - if (!opt.timeFormatted && opt.time) { - opt.timeFormatted = opt.time.format(I18n.t(opt.timeFormatKey)); - } - }); - + this._hideDynamicOptions(options); options = options.concat(customOptions); + options.sort((a, b) => { if (a.time < b.time) { return -1; @@ -218,9 +199,7 @@ export default Component.extend({ TIME_SHORTCUT_TYPES.LAST_CUSTOM ); lastCustom.time = this.parsedLastCustomDatetime; - lastCustom.timeFormatted = this.parsedLastCustomDatetime.format( - I18n.t("dates.long_no_year") - ); + lastCustom.timeFormatKey = "dates.long_no_year"; lastCustom.hidden = false; } @@ -234,12 +213,8 @@ export default Component.extend({ }); } - options.forEach((option) => { - if (customLabels[option.id]) { - option.label = customLabels[option.id]; - } - }); - + this._applyCustomLabels(options, customLabels); + this._formatTime(options); return options; }, @@ -288,4 +263,39 @@ export default Component.extend({ this.onTimeSelected(type, dateTime); } }, + + _applyCustomLabels(options, customLabels) { + options.forEach((option) => { + if (customLabels[option.id]) { + option.label = customLabels[option.id]; + } + }); + }, + + _formatTime(options) { + options.forEach((option) => { + if (option.time && option.timeFormatKey) { + option.timeFormatted = option.time.format(I18n.t(option.timeFormatKey)); + } + }); + }, + + _hideDynamicOptions(options) { + if (now(this.userTimezone).hour() >= LATER_TODAY_CUTOFF_HOUR) { + this._hideOption(options, TIME_SHORTCUT_TYPES.LATER_TODAY); + } + + if (now(this.userTimezone).day() >= MOMENT_THURSDAY) { + this._hideOption(options, TIME_SHORTCUT_TYPES.LATER_THIS_WEEK); + } + + if (now(this.userTimezone).day() >= MOMENT_FRIDAY) { + this._hideOption(options, TIME_SHORTCUT_TYPES.THIS_WEEKEND); + } + }, + + _hideOption(options, optionId) { + const option = options.findBy("id", optionId); + option.hidden = true; + }, }); diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 21f11a04ba..7e3a46dec8 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -204,25 +204,44 @@ export default Component.extend({ } const topic = this.topic; - const target = $(e.target); - if (target.hasClass("bulk-select")) { + if (e.target.classList.contains("bulk-select")) { const selected = this.selected; - if (target.is(":checked")) { + if (e.target.checked) { selected.addObject(topic); + + if (this.lastChecked && e.shiftKey) { + const bulkSelects = Array.from( + document.querySelectorAll("input.bulk-select") + ), + from = bulkSelects.indexOf(e.target), + to = bulkSelects.findIndex((el) => el.id === this.lastChecked.id), + start = Math.min(from, to), + end = Math.max(from, to); + + bulkSelects + .slice(start, end) + .filter((el) => el.checked !== true) + .forEach((checkbox) => { + checkbox.click(); + }); + } + + this.set("lastChecked", e.target); } else { selected.removeObject(topic); + this.set("lastChecked", null); } } - if (target.hasClass("raw-topic-link")) { + if (e.target.classList.contains("raw-topic-link")) { if (wantsNewWindow(e)) { return true; } - return this.navigateToTopic(topic, target.attr("href")); + return this.navigateToTopic(topic, e.target.getAttribute("href")); } - if (target.closest("a.topic-status").length === 1) { + if (e.target.closest("a.topic-status")) { this.topic.togglePinnedForUser(); return false; } diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js index f40882cf87..9c2b2ef70a 100644 --- a/app/assets/javascripts/discourse/app/components/topic-navigation.js +++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js @@ -163,6 +163,10 @@ export default Component.extend(PanEvents, { }, panStart(e) { + if (e.originalEvent.target.classList.contains("docked")) { + return; + } + e.originalEvent.preventDefault(); const center = e.center; const $centeredElement = $(document.elementFromPoint(center.x, center.y)); diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index e7c5b6cec3..81daa0d7dc 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -6,7 +6,7 @@ import { authorizesOneOrMoreExtensions, uploadIcon, } from "discourse/lib/uploads"; -import { cancel, run } from "@ember/runloop"; +import { cancel, run, scheduleOnce } from "@ember/runloop"; import { cannotPostAgain, durationTextFromSeconds, @@ -396,6 +396,75 @@ export default Controller.extend({ return uploadIcon(this.currentUser.staff, this.siteSettings); }, + // Use this to open the composer when you are not sure whether it is + // already open and whether it already has a draft being worked on. Supports + // options to append text once the composer is open if required. + // + // opts: + // + // - topic: if this is present, the composer will be opened with the reply + // action and the current topic key and draft sequence + // - fallbackToNewTopic: if true, and there is no draft and no topic, + // the composer will be opened with the create_topic action and a new + // topic draft key + // - insertText: the text to append to the composer once it is opened + // - openOpts: this object will be passed to this.open if fallbackToNewTopic is + // true or topic is provided + @action + focusComposer(opts = {}) { + this._openComposerForFocus(opts).then(() => { + this._focusAndInsertText(opts.insertText); + }); + }, + + _openComposerForFocus(opts) { + if (this.get("model.viewOpen")) { + return Promise.resolve(); + } else { + const opened = this.openIfDraft(); + if (opened) { + return Promise.resolve(); + } + + if (opts.topic) { + return this.open( + Object.assign( + { + action: Composer.REPLY, + draftKey: opts.topic.get("draft_key"), + draftSequence: opts.topic.get("draft_sequence"), + topic: opts.topic, + }, + opts.openOpts || {} + ) + ); + } + + if (opts.fallbackToNewTopic) { + return this.open( + Object.assign( + { + action: Composer.CREATE_TOPIC, + draftKey: Composer.NEW_TOPIC_KEY, + }, + opts.openOpts || {} + ) + ); + } + } + }, + + _focusAndInsertText(insertText) { + scheduleOnce("afterRender", () => { + const input = document.querySelector("textarea.d-editor-input"); + input && input.focus(); + + if (insertText) { + this.model.appendText(insertText, null, { new_line: true }); + } + }); + }, + @action openIfDraft(event) { if (this.get("model.viewDraft")) { @@ -407,7 +476,10 @@ export default Controller.extend({ } this.set("model.composeState", Composer.OPEN); + return true; } + + return false; }, actions: { diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js index fbeb0ee8ab..4a6307d75c 100644 --- a/app/assets/javascripts/discourse/app/controllers/invites-show.js +++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js @@ -100,9 +100,19 @@ export default Controller.extend( ); }, - @discourseComputed("externalAuthsOnly", "discourseConnectEnabled") - showSocialLoginAvailable(externalAuthsOnly, discourseConnectEnabled) { - return !externalAuthsOnly && !discourseConnectEnabled; + @discourseComputed( + "externalAuthsEnabled", + "externalAuthsOnly", + "discourseConnectEnabled" + ) + showSocialLoginAvailable( + externalAuthsEnabled, + externalAuthsOnly, + discourseConnectEnabled + ) { + return ( + externalAuthsEnabled && !externalAuthsOnly && !discourseConnectEnabled + ); }, @discourseComputed( diff --git a/app/assets/javascripts/discourse/app/controllers/move-to-topic.js b/app/assets/javascripts/discourse/app/controllers/move-to-topic.js index d5e8709541..c5f80bcab2 100644 --- a/app/assets/javascripts/discourse/app/controllers/move-to-topic.js +++ b/app/assets/javascripts/discourse/app/controllers/move-to-topic.js @@ -169,7 +169,7 @@ export default Controller.extend(ModalFunctionality, { DiscourseURL.routeTo(result.url); }) .catch((xhr) => { - this.flash(extractError(xhr, I18n.t("topic.move_to.error"))); + this.flash(extractError(xhr, I18n.t("topic.move_to.error")), "error"); }) .finally(() => { this.set("saving", false); diff --git a/app/assets/javascripts/discourse/app/controllers/tag-show.js b/app/assets/javascripts/discourse/app/controllers/tag-show.js index dae9fe2894..2a50cf829d 100644 --- a/app/assets/javascripts/discourse/app/controllers/tag-show.js +++ b/app/assets/javascripts/discourse/app/controllers/tag-show.js @@ -127,6 +127,13 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, { ); }, + showInserted() { + const tracker = this.topicTrackingState; + this.list.loadBefore(tracker.get("newIncoming"), true); + tracker.resetTracking(); + return false; + }, + changeSort(order) { if (order === this.order) { this.toggleProperty("ascending"); diff --git a/app/assets/javascripts/discourse/app/helpers/border-color.js b/app/assets/javascripts/discourse/app/helpers/border-color.js index bbe9845650..7272a76586 100644 --- a/app/assets/javascripts/discourse/app/helpers/border-color.js +++ b/app/assets/javascripts/discourse/app/helpers/border-color.js @@ -1,3 +1,3 @@ import { htmlHelper } from "discourse-common/lib/helpers"; -export default htmlHelper((color) => `border-color: #${color}`); +export default htmlHelper((color) => `border-color: #${color}; `); diff --git a/app/assets/javascripts/discourse/app/helpers/category-color-variable.js b/app/assets/javascripts/discourse/app/helpers/category-color-variable.js new file mode 100644 index 0000000000..4ad1cdcea7 --- /dev/null +++ b/app/assets/javascripts/discourse/app/helpers/category-color-variable.js @@ -0,0 +1,3 @@ +import { htmlHelper } from "discourse-common/lib/helpers"; + +export default htmlHelper((color) => `--category-color: #${color};`); diff --git a/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js b/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js index 01c1db9ec9..3c827a9ccd 100644 --- a/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js +++ b/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js @@ -36,5 +36,6 @@ export function autoLoadModules(container, registry) { export default { name: "auto-load-modules", + after: "inject-objects", initialize: (container) => autoLoadModules(container, container.registry), }; diff --git a/app/assets/javascripts/discourse/app/initializers/clean-dom-on-route-change.js b/app/assets/javascripts/discourse/app/initializers/clean-dom-on-route-change.js new file mode 100644 index 0000000000..e7f0276658 --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/clean-dom-on-route-change.js @@ -0,0 +1,46 @@ +import { scheduleOnce } from "@ember/runloop"; + +function _clean(transition) { + if (window.MiniProfiler && transition.from) { + window.MiniProfiler.pageTransition(); + } + + // Close some elements that may be open + document.querySelectorAll("header ul.icons li").forEach((element) => { + element.classList.remove("active"); + }); + + document.querySelectorAll(`[data-toggle="dropdown"]`).forEach((element) => { + element.parentElement.classList.remove("open"); + }); + + // Close the lightbox + if ($.magnificPopup?.instance) { + $.magnificPopup.instance.close(); + document.body.classList.remove("mfp-zoom-out-cur"); + } + + // Remove any link focus + const { activeElement } = document; + if (activeElement && !activeElement.classList.contains("no-blur")) { + activeElement.blur(); + } + + this.lookup("route:application").send("closeModal"); + + this.lookup("service:app-events").trigger("dom:clean"); + this.lookup("service:document-title").updateContextCount(0); +} + +export default { + name: "clean-dom-on-route-change", + after: "inject-objects", + + initialize(container) { + const router = container.lookup("router:main"); + + router.on("routeDidChange", (transition) => { + scheduleOnce("afterRender", container, _clean, transition); + }); + }, +}; diff --git a/app/assets/javascripts/discourse/app/initializers/copy-codeblocks.js b/app/assets/javascripts/discourse/app/initializers/copy-codeblocks.js index b11925807c..efe8a4af21 100644 --- a/app/assets/javascripts/discourse/app/initializers/copy-codeblocks.js +++ b/app/assets/javascripts/discourse/app/initializers/copy-codeblocks.js @@ -1,61 +1,10 @@ import { cancel, later } from "@ember/runloop"; import I18n from "I18n"; -import { Promise } from "rsvp"; import { guidFor } from "@ember/object/internals"; +import { clipboardCopy } from "discourse/lib/utilities"; import { iconHTML } from "discourse-common/lib/icon-library"; import { withPluginApi } from "discourse/lib/plugin-api"; -// http://github.com/feross/clipboard-copy -function clipboardCopy(text) { - // Use the Async Clipboard API when available. - // Requires a secure browsing context (i.e. HTTPS) - if (navigator.clipboard) { - return navigator.clipboard.writeText(text).catch(function (err) { - throw err !== undefined - ? err - : new DOMException("The request is not allowed", "NotAllowedError"); - }); - } - - // ...Otherwise, use document.execCommand() fallback - - // Put the text to copy into a - const span = document.createElement("span"); - span.textContent = text; - - // Preserve consecutive spaces and newlines - span.style.whiteSpace = "pre"; - - // Add the to the page - document.body.appendChild(span); - - // Make a selection object representing the range of text selected by the user - const selection = window.getSelection(); - const range = window.document.createRange(); - selection.removeAllRanges(); - range.selectNode(span); - selection.addRange(range); - - // Copy text to the clipboard - let success = false; - try { - success = window.document.execCommand("copy"); - } catch (err) { - // eslint-disable-next-line no-console - console.log("error", err); - } - - // Cleanup - selection.removeAllRanges(); - window.document.body.removeChild(span); - - return success - ? Promise.resolve() - : Promise.reject( - new DOMException("The request is not allowed", "NotAllowedError") - ); -} - let _copyCodeblocksClickHandlers = {}; let _fadeCopyCodeblocksRunners = {}; @@ -79,6 +28,25 @@ export default { _fadeCopyCodeblocksRunners = {}; } + function _copyComplete(button) { + button.classList.add("copied"); + const state = button.innerHTML; + button.innerHTML = I18n.t("copy_codeblock.copied"); + + const commandId = guidFor(button); + + if (_fadeCopyCodeblocksRunners[commandId]) { + cancel(_fadeCopyCodeblocksRunners[commandId]); + delete _fadeCopyCodeblocksRunners[commandId]; + } + + _fadeCopyCodeblocksRunners[commandId] = later(() => { + button.classList.remove("copied"); + button.innerHTML = state; + delete _fadeCopyCodeblocksRunners[commandId]; + }, 3000); + } + function _handleClick(event) { if (!event.target.classList.contains("copy-cmd")) { return; @@ -96,24 +64,14 @@ export default { ) .trim(); - clipboardCopy(text).then(() => { - button.classList.add("copied"); - const state = button.innerHTML; - button.innerHTML = I18n.t("copy_codeblock.copied"); - - const commandId = guidFor(button); - - if (_fadeCopyCodeblocksRunners[commandId]) { - cancel(_fadeCopyCodeblocksRunners[commandId]); - delete _fadeCopyCodeblocksRunners[commandId]; - } - - _fadeCopyCodeblocksRunners[commandId] = later(() => { - button.classList.remove("copied"); - button.innerHTML = state; - delete _fadeCopyCodeblocksRunners[commandId]; - }, 3000); - }); + const result = clipboardCopy(text); + if (result.then) { + result.then(() => { + _copyComplete(button); + }); + } else if (result) { + _copyComplete(button); + } } } diff --git a/app/assets/javascripts/discourse/app/initializers/inject-objects.js b/app/assets/javascripts/discourse/app/initializers/inject-objects.js index 533e9134af..6f3c229da6 100644 --- a/app/assets/javascripts/discourse/app/initializers/inject-objects.js +++ b/app/assets/javascripts/discourse/app/initializers/inject-objects.js @@ -1,9 +1,54 @@ import { setDefaultOwner } from "discourse-common/lib/get-owner"; +import { isLegacyEmber } from "discourse-common/config/environment"; +import User from "discourse/models/user"; +import Site from "discourse/models/site"; +import deprecated from "discourse-common/lib/deprecated"; export default { name: "inject-objects", + after: isLegacyEmber() ? null : "export-application-global", initialize(container, app) { // This is required for Ember CLI tests to work setDefaultOwner(app.__container__); + + // Backwards compatibility for Discourse.SiteSettings and Discourse.User + if (!isLegacyEmber()) { + Object.defineProperty(app, "SiteSettings", { + get() { + deprecated( + `use injected siteSettings instead of Discourse.SiteSettings`, + { + since: "2.8", + dropFrom: "2.9", + } + ); + return container.lookup("site-settings:main"); + }, + }); + Object.defineProperty(app, "User", { + get() { + deprecated( + `import discourse/models/user instead of using Discourse.User`, + { + since: "2.8", + dropFrom: "2.9", + } + ); + return User; + }, + }); + Object.defineProperty(app, "Site", { + get() { + deprecated( + `import discourse/models/site instead of using Discourse.Site`, + { + since: "2.8", + dropFrom: "2.9", + } + ); + return Site; + }, + }); + } }, }; diff --git a/app/assets/javascripts/discourse/app/initializers/page-tracking.js b/app/assets/javascripts/discourse/app/initializers/page-tracking.js index df11851a6e..f640275e8c 100644 --- a/app/assets/javascripts/discourse/app/initializers/page-tracking.js +++ b/app/assets/javascripts/discourse/app/initializers/page-tracking.js @@ -3,7 +3,6 @@ import { resetPageTracking, startPageTracking, } from "discourse/lib/page-tracker"; -import { cleanDOM } from "discourse/lib/clean-dom"; import { viewTrackingRequired } from "discourse/lib/ajax"; export default { @@ -13,11 +12,7 @@ export default { initialize(container) { // Tell our AJAX system to track a page transition const router = container.lookup("router:main"); - router.on("routeWillChange", viewTrackingRequired); - router.on("routeDidChange", (transition) => { - cleanDOM(container, { skipMiniProfilerPageTransition: !transition.from }); - }); let appEvents = container.lookup("service:app-events"); let documentTitle = container.lookup("service:document-title"); diff --git a/app/assets/javascripts/discourse/app/lib/clean-dom.js b/app/assets/javascripts/discourse/app/lib/clean-dom.js deleted file mode 100644 index 00403b30f4..0000000000 --- a/app/assets/javascripts/discourse/app/lib/clean-dom.js +++ /dev/null @@ -1,34 +0,0 @@ -import { scheduleOnce } from "@ember/runloop"; - -function _clean(opts = {}) { - if (window.MiniProfiler && !opts.skipMiniProfilerPageTransition) { - window.MiniProfiler.pageTransition(); - } - - // Close some elements that may be open - $("header ul.icons li").removeClass("active"); - $('[data-toggle="dropdown"]').parent().removeClass("open"); - // close the lightbox - if ($.magnificPopup && $.magnificPopup.instance) { - $.magnificPopup.instance.close(); - $("body").removeClass("mfp-zoom-out-cur"); - } - - // Remove any link focus - // NOTE: the '.not("body")' is here to prevent a bug in IE10 on Win7 - // cf. https://stackoverflow.com/questions/5657371 - $(document.activeElement).not("body").not(".no-blur").blur(); - - this.lookup("route:application").send("closeModal"); - const hideDropDownFunction = $("html").data("hide-dropdown"); - if (hideDropDownFunction) { - hideDropDownFunction(); - } - - this.lookup("service:app-events").trigger("dom:clean"); - this.lookup("service:document-title").updateContextCount(0); -} - -export function cleanDOM(container, opts) { - scheduleOnce("afterRender", container, _clean, opts); -} diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index baa2cc69b8..a60af675ed 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -1,7 +1,7 @@ import { bind } from "discourse-common/utils/decorators"; import discourseDebounce from "discourse-common/lib/debounce"; import { isAppWebview } from "discourse/lib/utilities"; -import { later, run, schedule, throttle } from "@ember/runloop"; +import { later, run, throttle } from "@ember/runloop"; import { nextTopicUrl, previousTopicUrl, @@ -413,16 +413,11 @@ export default { focusComposer(event) { const composer = this.container.lookup("controller:composer"); - if (composer.get("model.viewOpen")) { - preventKeyboardEvent(event); - - schedule("afterRender", () => { - const input = document.querySelector("textarea.d-editor-input"); - input && input.focus(); - }); - } else { - composer.openIfDraft(event); + if (event) { + event.preventDefault(); + event.stopPropagation(); } + composer.focusComposer(event); }, fullscreenComposer() { diff --git a/app/assets/javascripts/discourse/app/lib/public-js-versions.js b/app/assets/javascripts/discourse/app/lib/public-js-versions.js index 4c3d3160d1..393052736c 100644 --- a/app/assets/javascripts/discourse/app/lib/public-js-versions.js +++ b/app/assets/javascripts/discourse/app/lib/public-js-versions.js @@ -11,6 +11,4 @@ export const PUBLIC_JS_VERSIONS = { "jquery.magnific-popup.min.js": "magnific-popup/1.1.0/jquery.magnific-popup.min.js", "pikaday.js": "pikaday/1.8.0/pikaday.js", - "spectrum.js": "spectrum-colorpicker/1.8.0/spectrum.js", - "spectrum.css": "spectrum-colorpicker/1.8.0/spectrum.css", }; diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js index 6e7def0ecd..635a5c44b2 100644 --- a/app/assets/javascripts/discourse/app/lib/text.js +++ b/app/assets/javascripts/discourse/app/lib/text.js @@ -23,6 +23,7 @@ function getOpts(opts) { formatUsername, watchedWordsReplace: context.site.watched_words_replace, watchedWordsLink: context.site.watched_words_link, + additionalOptions: context.site.markdown_additional_options, }, opts ); diff --git a/app/assets/javascripts/discourse/app/lib/time-shortcut.js b/app/assets/javascripts/discourse/app/lib/time-shortcut.js index d2eac0b9c7..37d6585cdf 100644 --- a/app/assets/javascripts/discourse/app/lib/time-shortcut.js +++ b/app/assets/javascripts/discourse/app/lib/time-shortcut.js @@ -1,17 +1,19 @@ import { MOMENT_MONDAY, + MOMENT_SUNDAY, laterThisWeek, laterToday, nextBusinessWeekStart, nextMonth, now, + thisWeekend, tomorrow, } from "discourse/lib/time-utils"; -import I18n from "I18n"; export const TIME_SHORTCUT_TYPES = { LATER_TODAY: "later_today", TOMORROW: "tomorrow", + THIS_WEEKEND: "this_weekend", NEXT_MONTH: "next_month", CUSTOM: "custom", RELATIVE: "relative", @@ -29,44 +31,46 @@ export function defaultShortcutOptions(timezone) { id: TIME_SHORTCUT_TYPES.LATER_TODAY, label: "time_shortcut.later_today", time: laterToday(timezone), - timeFormatted: laterToday(timezone).format(I18n.t("dates.time")), - hidden: true, + timeFormatKey: "dates.time", }, { icon: "far-sun", id: TIME_SHORTCUT_TYPES.TOMORROW, label: "time_shortcut.tomorrow", time: tomorrow(timezone), - timeFormatted: tomorrow(timezone).format(I18n.t("dates.time_short_day")), + timeFormatKey: "dates.time_short_day", }, { icon: "angle-double-right", id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, label: "time_shortcut.later_this_week", time: laterThisWeek(timezone), - timeFormatted: laterThisWeek(timezone).format( - I18n.t("dates.time_short_day") - ), - hidden: true, + timeFormatKey: "dates.time_short_day", + }, + { + icon: "bed", + id: TIME_SHORTCUT_TYPES.THIS_WEEKEND, + label: "time_shortcut.this_weekend", + time: thisWeekend(timezone), + timeFormatKey: "dates.time_short_day", }, { icon: "briefcase", id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK, label: - now(timezone).day() === MOMENT_MONDAY + now(timezone).day() === MOMENT_MONDAY || + now(timezone).day() === MOMENT_SUNDAY ? "time_shortcut.start_of_next_business_week_alt" : "time_shortcut.start_of_next_business_week", time: nextBusinessWeekStart(timezone), - timeFormatted: nextBusinessWeekStart(timezone).format( - I18n.t("dates.long_no_year") - ), + timeFormatKey: "dates.long_no_year", }, { icon: "far-calendar-plus", id: TIME_SHORTCUT_TYPES.NEXT_MONTH, label: "time_shortcut.next_month", time: nextMonth(timezone), - timeFormatted: nextMonth(timezone).format(I18n.t("dates.long_no_year")), + timeFormatKey: "dates.long_no_year", }, ]; } @@ -78,7 +82,6 @@ export function specialShortcutOptions() { id: TIME_SHORTCUT_TYPES.LAST_CUSTOM, label: "time_shortcut.last_custom", time: null, - timeFormatted: null, hidden: true, }, { @@ -86,7 +89,6 @@ export function specialShortcutOptions() { id: TIME_SHORTCUT_TYPES.CUSTOM, label: "time_shortcut.custom", time: null, - timeFormatted: null, isCustomTimeShortcut: true, }, { @@ -94,7 +96,6 @@ export function specialShortcutOptions() { id: TIME_SHORTCUT_TYPES.NONE, label: "time_shortcut.none", time: null, - timeFormatted: null, }, ]; } diff --git a/app/assets/javascripts/discourse/app/lib/time-utils.js b/app/assets/javascripts/discourse/app/lib/time-utils.js index 1f765ffdc0..5266362e5b 100644 --- a/app/assets/javascripts/discourse/app/lib/time-utils.js +++ b/app/assets/javascripts/discourse/app/lib/time-utils.js @@ -3,8 +3,10 @@ import { isPresent } from "@ember/utils"; export const START_OF_DAY_HOUR = 8; export const LATER_TODAY_CUTOFF_HOUR = 17; export const LATER_TODAY_MAX_HOUR = 18; +export const MOMENT_SUNDAY = 0; export const MOMENT_MONDAY = 1; export const MOMENT_THURSDAY = 4; +export const MOMENT_FRIDAY = 5; export const MOMENT_SATURDAY = 6; export function now(timezone) { diff --git a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js index cdfd0a3d2b..361ad2e336 100644 --- a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js +++ b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js @@ -68,7 +68,7 @@ const TIMEFRAMES = [ buildTimeframe({ id: "two_months", format: "MMM D", - enabled: (opts) => opts.includeMidFuture, + enabled: () => true, when: (time, timeOfDay) => time.add(2, "month").startOf("month").hour(timeOfDay).minute(0), icon: "briefcase", @@ -76,7 +76,7 @@ const TIMEFRAMES = [ buildTimeframe({ id: "three_months", format: "MMM D", - enabled: (opts) => opts.includeMidFuture, + enabled: () => true, when: (time, timeOfDay) => time.add(3, "month").startOf("month").hour(timeOfDay).minute(0), icon: "briefcase", @@ -84,7 +84,7 @@ const TIMEFRAMES = [ buildTimeframe({ id: "four_months", format: "MMM D", - enabled: (opts) => opts.includeMidFuture, + enabled: () => true, when: (time, timeOfDay) => time.add(4, "month").startOf("month").hour(timeOfDay).minute(0), icon: "briefcase", @@ -92,7 +92,7 @@ const TIMEFRAMES = [ buildTimeframe({ id: "six_months", format: "MMM D", - enabled: (opts) => opts.includeMidFuture, + enabled: () => true, when: (time, timeOfDay) => time.add(6, "month").startOf("month").hour(timeOfDay).minute(0), icon: "briefcase", diff --git a/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js b/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js index 5134788cb6..06f2888da1 100644 --- a/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js +++ b/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js @@ -1,8 +1,22 @@ import { Promise } from "rsvp"; let model, currentTopicId; +let lastTopicId, lastHighestRead; + export function setTopicList(incomingModel) { model = incomingModel; + + model?.topics?.forEach((topic) => { + let highestRead = getHighestReadCache(topic.id); + if (highestRead && highestRead >= topic.last_read_post_number) { + let count = Math.max(topic.highest_post_number - highestRead, 0); + topic.setProperties({ + unread_posts: count, + new_posts: count, + }); + resetHighestReadCache(); + } + }); currentTopicId = null; } @@ -14,6 +28,22 @@ export function previousTopicUrl() { return urlAt(-1); } +export function setHighestReadCache(topicId, postNumber) { + lastTopicId = topicId; + lastHighestRead = postNumber; +} + +export function getHighestReadCache(topicId) { + if (topicId === lastTopicId) { + return lastHighestRead; + } +} + +export function resetHighestReadCache() { + lastTopicId = undefined; + lastHighestRead = undefined; +} + function urlAt(delta) { if (!model || !model.topics) { return Promise.resolve(null); diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index dff0909873..9005d55320 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -207,9 +207,13 @@ export function selectedText() { } export function selectedElement() { + return selectedRange()?.commonAncestorContainer; +} + +export function selectedRange() { const selection = window.getSelection(); if (selection.rangeCount > 0) { - return selection.getRangeAt(0).commonAncestorContainer; + return selection.getRangeAt(0); } } @@ -499,5 +503,52 @@ export function translateModKey(string) { return string; } + +// http://github.com/feross/clipboard-copy +export function clipboardCopy(text) { + // Use the Async Clipboard API when available. + // Requires a secure browsing context (i.e. HTTPS) + if (navigator.clipboard) { + return navigator.clipboard.writeText(text).catch(function (err) { + throw err !== undefined + ? err + : new DOMException("The request is not allowed", "NotAllowedError"); + }); + } + + // ...Otherwise, use document.execCommand() fallback + + // Put the text to copy into a + const span = document.createElement("span"); + span.textContent = text; + + // Preserve consecutive spaces and newlines + span.style.whiteSpace = "pre"; + + // Add the to the page + document.body.appendChild(span); + + // Make a selection object representing the range of text selected by the user + const selection = window.getSelection(); + const range = window.document.createRange(); + selection.removeAllRanges(); + range.selectNode(span); + selection.addRange(range); + + // Copy text to the clipboard + let success = false; + try { + success = window.document.execCommand("copy"); + } catch (err) { + // eslint-disable-next-line no-console + console.log("error", err); + } + + // Cleanup + selection.removeAllRanges(); + window.document.body.removeChild(span); + return success; +} + // This prevents a mini racer crash export default {}; diff --git a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js index 64489d4384..4703993b33 100644 --- a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js +++ b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js @@ -11,6 +11,7 @@ export default Mixin.create({ bulkSelectEnabled: false, autoAddTopicsToBulkSelect: false, selected: null, + lastChecked: null, canBulkSelect: or("currentUser.staff", "showDismissRead", "showResetNew"), diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index 7ba98ad19e..0442cf9697 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -20,6 +20,7 @@ import { } from "discourse/lib/uploads"; import { cacheShortUploadUrl } from "pretty-text/upload-short-url"; import bootbox from "bootbox"; +import { run } from "@ember/runloop"; // Note: This mixin is used _in addition_ to the ComposerUpload mixin // on the composer-editor component. It overrides some, but not all, @@ -64,7 +65,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.fileInputEventListener ); - this.element.removeEventListener("paste", this.pasteEventListener); + this.editorEl?.removeEventListener("paste", this.pasteEventListener); this.appEvents.off(`${this.eventPrefix}:add-files`, this._addFiles); this.appEvents.off( @@ -92,6 +93,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.set("inProgressUploads", []); this.placeholders = {}; this._preProcessorStatus = {}; + this.editorEl = this.element.querySelector(this.editorClass); this.fileInputEl = document.getElementById(this.fileUploadElementId); const isPrivateMessage = this.get("composerModel.privateMessage"); @@ -106,7 +108,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.fileInputEl, this._addFiles ); - this.element.addEventListener("paste", this.pasteEventListener); + this.editorEl.addEventListener("paste", this.pasteEventListener); this._uppyInstance = new Uppy({ id: this.uppyId, @@ -206,112 +208,135 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { } this._uppyInstance.on("file-added", (file) => { - if (isPrivateMessage) { - file.meta.for_private_message = true; - } + run(() => { + if (isPrivateMessage) { + file.meta.for_private_message = true; + } + }); }); this._uppyInstance.on("progress", (progress) => { - if (this.isDestroying || this.isDestroyed) { - return; - } + run(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } - this.set("uploadProgress", progress); + this.set("uploadProgress", progress); + }); }); this._uppyInstance.on("file-removed", (file, reason) => { - // we handle the cancel-all event specifically, so no need - // to do anything here. this event is also fired when some files - // are handled by an upload handler - if (reason === "cancel-all") { - return; - } + run(() => { + // we handle the cancel-all event specifically, so no need + // to do anything here. this event is also fired when some files + // are handled by an upload handler + if (reason === "cancel-all") { + return; + } - file.meta.cancelled = true; - this._removeInProgressUpload(file.id); - this._resetUpload(file, { removePlaceholder: true }); - if (this.inProgressUploads.length === 0) { - this.set("userCancelled", true); - this._uppyInstance.cancelAll(); - } + file.meta.cancelled = true; + this._removeInProgressUpload(file.id); + this._resetUpload(file, { removePlaceholder: true }); + if (this.inProgressUploads.length === 0) { + this.set("userCancelled", true); + this._uppyInstance.cancelAll(); + } + }); }); this._uppyInstance.on("upload-progress", (file, progress) => { - if (this.isDestroying || this.isDestroyed) { - return; - } + run(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } - const upload = this.inProgressUploads.find((upl) => upl.id === file.id); - if (upload) { - const percentage = Math.round( - (progress.bytesUploaded / progress.bytesTotal) * 100 - ); - upload.set("progress", percentage); - } + const upload = this.inProgressUploads.find((upl) => upl.id === file.id); + if (upload) { + const percentage = Math.round( + (progress.bytesUploaded / progress.bytesTotal) * 100 + ); + upload.set("progress", percentage); + } + }); }); this._uppyInstance.on("upload", (data) => { - this._addNeedProcessing(data.fileIDs.length); + run(() => { + this._addNeedProcessing(data.fileIDs.length); - const files = data.fileIDs.map((fileId) => - this._uppyInstance.getFile(fileId) - ); - - this.setProperties({ - isProcessingUpload: true, - isCancellable: false, - }); - - files.forEach((file) => { - // The inProgressUploads is meant to be used to display these uploads - // in a UI, and Ember will only update the array in the UI if pushObject - // is used to notify it. - this.inProgressUploads.pushObject( - EmberObject.create({ - fileName: file.name, - id: file.id, - progress: 0, - extension: file.extension, - }) + const files = data.fileIDs.map((fileId) => + this._uppyInstance.getFile(fileId) ); - const placeholder = this._uploadPlaceholder(file); - this.placeholders[file.id] = { - uploadPlaceholder: placeholder, - }; - this.appEvents.trigger(`${this.eventPrefix}:insert-text`, placeholder); - this.appEvents.trigger(`${this.eventPrefix}:upload-started`, file.name); + + this.setProperties({ + isProcessingUpload: true, + isCancellable: false, + }); + + files.forEach((file) => { + // The inProgressUploads is meant to be used to display these uploads + // in a UI, and Ember will only update the array in the UI if pushObject + // is used to notify it. + this.inProgressUploads.pushObject( + EmberObject.create({ + fileName: file.name, + id: file.id, + progress: 0, + extension: file.extension, + }) + ); + const placeholder = this._uploadPlaceholder(file); + this.placeholders[file.id] = { + uploadPlaceholder: placeholder, + }; + this.appEvents.trigger( + `${this.eventPrefix}:insert-text`, + placeholder + ); + this.appEvents.trigger( + `${this.eventPrefix}:upload-started`, + file.name + ); + }); }); }); this._uppyInstance.on("upload-success", (file, response) => { - this._removeInProgressUpload(file.id); - let upload = response.body; - const markdown = this.uploadMarkdownResolvers.reduce( - (md, resolver) => resolver(upload) || md, - getUploadMarkdown(upload) - ); + run(() => { + if (!this._uppyInstance) { + return; + } + this._removeInProgressUpload(file.id); + let upload = response.body; + const markdown = this.uploadMarkdownResolvers.reduce( + (md, resolver) => resolver(upload) || md, + getUploadMarkdown(upload) + ); - cacheShortUploadUrl(upload.short_url, upload); + cacheShortUploadUrl(upload.short_url, upload); - this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, - this.placeholders[file.id].uploadPlaceholder.trim(), - markdown - ); + this.appEvents.trigger( + `${this.eventPrefix}:replace-text`, + this.placeholders[file.id].uploadPlaceholder.trim(), + markdown + ); - this._resetUpload(file, { removePlaceholder: false }); - this.appEvents.trigger( - `${this.eventPrefix}:upload-success`, - file.name, - upload - ); + this._resetUpload(file, { removePlaceholder: false }); + this.appEvents.trigger( + `${this.eventPrefix}:upload-success`, + file.name, + upload + ); + }); }); this._uppyInstance.on("upload-error", this._handleUploadError); this._uppyInstance.on("complete", () => { - this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`); - this._reset(); + run(() => { + this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`); + this._reset(); + }); }); this._uppyInstance.on("cancel-all", () => { @@ -319,11 +344,13 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { // only do the manual cancelling work if the user clicked cancel if (this.userCancelled) { Object.values(this.placeholders).forEach((data) => { - this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, - data.uploadPlaceholder, - "" - ); + run(() => { + this.appEvents.trigger( + `${this.eventPrefix}:replace-text`, + data.uploadPlaceholder, + "" + ); + }); }); this.set("userCancelled", false); @@ -415,21 +442,25 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this._onPreProcessComplete( (file) => { - let placeholderData = this.placeholders[file.id]; - this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, - placeholderData.processingPlaceholder, - placeholderData.uploadPlaceholder - ); + run(() => { + let placeholderData = this.placeholders[file.id]; + this.appEvents.trigger( + `${this.eventPrefix}:replace-text`, + placeholderData.processingPlaceholder, + placeholderData.uploadPlaceholder + ); + }); }, () => { - this.setProperties({ - isProcessingUpload: false, - isCancellable: true, + run(() => { + this.setProperties({ + isProcessingUpload: false, + isCancellable: true, + }); + this.appEvents.trigger( + `${this.eventPrefix}:uploads-preprocessing-complete` + ); }); - this.appEvents.trigger( - `${this.eventPrefix}:uploads-preprocessing-complete` - ); } ); }, @@ -520,12 +551,12 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { return; } - const { canUpload } = clipboardHelpers(event, { + const { canUpload, canPasteHtml, types } = clipboardHelpers(event, { siteSettings: this.siteSettings, canUpload: true, }); - if (!canUpload) { + if (!canUpload || canPasteHtml || types.includes("text/plain")) { return; } diff --git a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js index ffdad12a25..896cfd9aea 100644 --- a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js +++ b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js @@ -25,18 +25,16 @@ export default Mixin.create({ // ensures textarea scroll position is correct _focusTextArea() { - schedule("afterRender", () => { - if (!this.element || this.isDestroying || this.isDestroyed) { - return; - } + if (!this.element || this.isDestroying || this.isDestroyed) { + return; + } - if (!this._textarea) { - return; - } + if (!this._textarea) { + return; + } - this._textarea.blur(); - this._textarea.focus(); - }); + this._textarea.blur(); + this._textarea.focus(); }, _insertBlock(text) { @@ -171,7 +169,7 @@ export default Mixin.create({ this._$textarea.prop("selectionStart", (pre + text).length + 2); this._$textarea.prop("selectionEnd", (pre + text).length + 2); - this._focusTextArea(); + schedule("afterRender", this, this._focusTextArea); }, _addText(sel, text, options) { diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index 1636d8ddb5..fca4880d7d 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -542,6 +542,7 @@ Category.reopenClass({ search(term, opts) { let limit = 5; + let parentCategoryId; if (opts) { if (opts.limit === 0) { @@ -549,6 +550,9 @@ Category.reopenClass({ } else if (opts.limit) { limit = opts.limit; } + if (opts.parentCategoryId) { + parentCategoryId = opts.parentCategoryId; + } } const emptyTerm = term === ""; @@ -569,13 +573,21 @@ Category.reopenClass({ return data.length === limit; }; + const validCategoryParent = (category) => { + return ( + !parentCategoryId || + category.get("parent_category_id") === parentCategoryId + ); + }; + for (i = 0; i < length && !done(); i++) { const category = categories[i]; if ( - (emptyTerm && !category.get("parent_category_id")) || - (!emptyTerm && - (category.get("name").toLowerCase().indexOf(term) === 0 || - category.get("slug").toLowerCase().indexOf(slugTerm) === 0)) + ((emptyTerm && !category.get("parent_category_id")) || + (!emptyTerm && + (category.get("name").toLowerCase().indexOf(term) === 0 || + category.get("slug").toLowerCase().indexOf(slugTerm) === 0))) && + validCategoryParent(category) ) { data.push(category); } @@ -586,9 +598,10 @@ Category.reopenClass({ const category = categories[i]; if ( - !emptyTerm && - (category.get("name").toLowerCase().indexOf(term) > 0 || - category.get("slug").toLowerCase().indexOf(slugTerm) > 0) + ((!emptyTerm && + category.get("name").toLowerCase().indexOf(term) > 0) || + category.get("slug").toLowerCase().indexOf(slugTerm) > 0) && + validCategoryParent(category) ) { if (data.indexOf(category) === -1) { data.push(category); diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index 4f9194b414..68566db1cb 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -660,6 +660,14 @@ const Composer = RestModel.extend({ } } + if (opts && opts.new_line) { + if (before.length > 0) { + text = "\n\n" + text.trim(); + } else { + text = text.trim(); + } + } + this.set("reply", before + text + after); return before.length + text.length; diff --git a/app/assets/javascripts/discourse/app/models/group.js b/app/assets/javascripts/discourse/app/models/group.js index a360f8c3d8..2d48c03905 100644 --- a/app/assets/javascripts/discourse/app/models/group.js +++ b/app/assets/javascripts/discourse/app/models/group.js @@ -243,6 +243,7 @@ const Group = RestModel.extend({ imap_mailbox_name: this.imap_mailbox_name, imap_enabled: this.imap_enabled, email_username: this.email_username, + email_from_alias: this.email_from_alias, email_password: this.email_password, flair_icon: null, flair_upload_id: null, diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index 3791fb0708..270a2b1809 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -194,6 +194,7 @@ const TopicTrackingState = EmberObject.extend({ const filter = this.filter; const filterCategory = this.filterCategory; + const filterTag = this.filterTag; const categoryId = data.payload && data.payload.category_id; // if we have a filter category currently and it is not the @@ -209,6 +210,10 @@ const TopicTrackingState = EmberObject.extend({ } } + if (filterTag && !data.payload.tags.includes(filterTag)) { + return; + } + // always count a new_topic as incoming if ( ["all", "latest", "new", "unseen"].includes(filter) && @@ -275,25 +280,34 @@ const TopicTrackingState = EmberObject.extend({ * @method trackIncoming * @param {String} filter - Valid values are all, categories, and any topic list * filters e.g. latest, unread, new. As well as this - * specific category and tag URLs like /tag/test/l/latest - * or c/cat/subcat/6/l/latest. + * specific category and tag URLs like tag/test/l/latest, + * c/cat/subcat/6/l/latest or tags/c/cat/subcat/6/test/l/latest. */ trackIncoming(filter) { this.newIncoming = []; - if (filter.startsWith("c/")) { - const categoryId = filter.match(/\/(\d*)\//); - const category = Category.findById(parseInt(categoryId[1], 10)); - this.set("filterCategory", category); + let category, tag; + if (filter.startsWith("c/") || filter.startsWith("tags/c/")) { + const categoryId = filter.match(/\/(\d*)\//); + category = Category.findById(parseInt(categoryId[1], 10)); const split = filter.split("/"); + + if (filter.startsWith("tags/c/")) { + tag = split[split.indexOf(categoryId[1]) + 1]; + } + if (split.length >= 4) { filter = split[split.length - 1]; } - } else { - this.set("filterCategory", null); + } else if (filter.startsWith("tag/")) { + const split = filter.split("/"); + filter = split[split.length - 1]; + tag = split[1]; } + this.set("filterCategory", category); + this.set("filterTag", tag); this.set("filter", filter); this.set("incomingCount", 0); }, diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 83f36723b6..4f9cca7a4d 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -334,13 +334,16 @@ const User = RestModel.extend({ userFields.filter((uf) => !fields || fields.indexOf(uf) !== -1) ); + let filteredUserOptionFields = []; if (fields) { - userOptionFields = userOptionFields.filter( + filteredUserOptionFields = userOptionFields.filter( (uo) => fields.indexOf(uo) !== -1 ); + } else { + filteredUserOptionFields = userOptionFields; } - userOptionFields.forEach((s) => { + filteredUserOptionFields.forEach((s) => { data[s] = this.get(`user_option.${s}`); }); @@ -379,6 +382,10 @@ const User = RestModel.extend({ } }); + return this._saveUserData(data, updatedState); + }, + + _saveUserData(data, updatedState) { // TODO: We can remove this when migrated fully to rest model. this.set("isSaving", true); return ajax(userPath(`${this.username_lower}.json`), { diff --git a/app/assets/javascripts/discourse/app/services/screen-track.js b/app/assets/javascripts/discourse/app/services/screen-track.js index 647c3995f2..9b2ae7f740 100644 --- a/app/assets/javascripts/discourse/app/services/screen-track.js +++ b/app/assets/javascripts/discourse/app/services/screen-track.js @@ -2,6 +2,11 @@ import Service, { inject as service } from "@ember/service"; import { ajax } from "discourse/lib/ajax"; import { bind } from "discourse-common/utils/decorators"; import { isTesting } from "discourse-common/config/environment"; +import { + getHighestReadCache, + resetHighestReadCache, + setHighestReadCache, +} from "discourse/lib/topic-list-tracker"; // We use this class to track how long posts in a topic are on the screen. const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3; @@ -128,9 +133,19 @@ export default class ScreenTrack extends Service { this._consolidatedTimings.push({ timings, topicTime, topicId }); } + const highestRead = parseInt(Object.keys(timings).lastObject, 10); + const cachedHighestRead = this.highestReadFromCache(topicId); + if (!cachedHighestRead || cachedHighestRead < highestRead) { + setHighestReadCache(topicId, highestRead); + } + return this._consolidatedTimings; } + highestReadFromCache(topicId) { + return getHighestReadCache(topicId); + } + sendNextConsolidatedTiming() { if (this._consolidatedTimings.length === 0) { return; @@ -172,11 +187,19 @@ export default class ScreenTrack extends Service { if (topicController) { const postNumbers = Object.keys(timings).map((v) => parseInt(v, 10)); topicController.readPosts(topicId, postNumbers); + + const cachedHighestRead = this.highestReadFromCache(topicId); + if ( + cachedHighestRead && + cachedHighestRead <= postNumbers.lastObject + ) { + resetHighestReadCache(topicId); + } } this.appEvents.trigger("topic:timings-sent", data); }) .catch((e) => { - if (ALLOWED_AJAX_FAILURES.indexOf(e.jqXHR.status) > -1) { + if (e.jqXHR && ALLOWED_AJAX_FAILURES.indexOf(e.jqXHR.status) > -1) { const delay = AJAX_FAILURE_DELAYS[this._ajaxFailures]; this._ajaxFailures += 1; @@ -187,7 +210,7 @@ export default class ScreenTrack extends Service { } } - if (window.console && window.console.warn) { + if (window.console && window.console.warn && e.jqXHR) { window.console.warn( `Failed to update topic times for topic ${topicId} due to ${e.jqXHR.status} error` ); diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs index bc75391e1b..9d95a1ca0c 100644 --- a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs @@ -41,7 +41,6 @@ customOptions=customTimeShortcutOptions hiddenOptions=hiddenTimeShortcutOptions customLabels=customTimeShortcutLabels - additionalOptionsToShow=additionalTimeShortcutOptions _itsatrap=_itsatrap }} {{else}} diff --git a/app/assets/javascripts/discourse/app/templates/components/categories-boxes-with-topics.hbs b/app/assets/javascripts/discourse/app/templates/components/categories-boxes-with-topics.hbs index f11790e5c2..0a041d8cc0 100644 --- a/app/assets/javascripts/discourse/app/templates/components/categories-boxes-with-topics.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/categories-boxes-with-topics.hbs @@ -1,5 +1,5 @@ {{#each categories as |c|}} -
+
diff --git a/app/assets/javascripts/discourse/app/templates/components/categories-boxes.hbs b/app/assets/javascripts/discourse/app/templates/components/categories-boxes.hbs index 725497bbec..255d483880 100644 --- a/app/assets/javascripts/discourse/app/templates/components/categories-boxes.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/categories-boxes.hbs @@ -1,5 +1,6 @@ {{#each categories as |c|}} -
+ {{plugin-outlet name="category-box-before-each-box" args=(hash category=c)}} +
{{#unless c.isMuted}}
+ {{plugin-outlet name="category-box-after-each-box" args=(hash category=c)}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/templates/components/color-input.hbs b/app/assets/javascripts/discourse/app/templates/components/color-input.hbs index f8d8503a38..2d6a1bf703 100644 --- a/app/assets/javascripts/discourse/app/templates/components/color-input.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/color-input.hbs @@ -1,7 +1,7 @@ -{{text-field +{{#if onlyHex}}#{{/if}}{{text-field class="hex-input" value=hexValue maxlength=maxlength input=(action "onHexInput" value="target.value") }} - + diff --git a/app/assets/javascripts/discourse/app/templates/components/composer-title.hbs b/app/assets/javascripts/discourse/app/templates/components/composer-title.hbs index 56674525f1..55f7302f1e 100644 --- a/app/assets/javascripts/discourse/app/templates/components/composer-title.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/composer-title.hbs @@ -5,7 +5,7 @@ placeholderKey=composer.titlePlaceholder aria-label=(I18n composer.titlePlaceholder) disabled=disabled - autocomplete="discourse" + autocomplete="off" }} {{popup-input-tip validation=validation}} diff --git a/app/assets/javascripts/discourse/app/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/app/templates/components/d-editor.hbs index b424143c3d..c1be4f6978 100644 --- a/app/assets/javascripts/discourse/app/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/d-editor.hbs @@ -43,7 +43,7 @@ {{conditional-loading-spinner condition=loading}} {{d-textarea - autocomplete="discourse" + autocomplete="off" tabindex=tabindex value=value class="d-editor-input" diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs index ce5b09c68d..8e16ab00a9 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs @@ -64,7 +64,7 @@
{{i18n "category.background_color"}}:
- #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}} + {{color-input hexValue=category.color valid=category.colorValid}} {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}}
@@ -72,7 +72,7 @@
{{i18n "category.foreground_color"}}:
- #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}} + {{color-input hexValue=category.text_color}} {{color-picker colors=foregroundColors value=category.text_color id="edit-text-color"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/emoji-picker.hbs b/app/assets/javascripts/discourse/app/templates/components/emoji-picker.hbs index 5584b496d4..6de3a63fd3 100644 --- a/app/assets/javascripts/discourse/app/templates/components/emoji-picker.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/emoji-picker.hbs @@ -24,7 +24,8 @@ class="filter" name="filter" placeholder=(i18n "emoji_picker.filter_placeholder") - autocomplete="discourse" + autocomplete="off" + type="search" autocorrect="off" autocapitalize="off" input=(action "onFilter") diff --git a/app/assets/javascripts/discourse/app/templates/components/future-date-input.hbs b/app/assets/javascripts/discourse/app/templates/components/future-date-input.hbs index 5f3ee328b6..852c3915a2 100644 --- a/app/assets/javascripts/discourse/app/templates/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/future-date-input.hbs @@ -9,7 +9,6 @@ includeDateTime=includeDateTime includeWeekend=includeWeekend includeFarFuture=includeFarFuture - includeMidFuture=includeMidFuture includeNow=includeNow clearable=clearable onChangeInput=onChangeInput diff --git a/app/assets/javascripts/discourse/app/templates/components/group-manage-email-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/group-manage-email-settings.hbs index 2f20944a3a..7c8080f79d 100644 --- a/app/assets/javascripts/discourse/app/templates/components/group-manage-email-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/group-manage-email-settings.hbs @@ -45,5 +45,5 @@

- {{group-manage-save-button model=group disabled=(not emailSettingsValid) beforeSave=beforeSave afterSave=afterSave tabindex="14"}} + {{group-manage-save-button model=group disabled=(not emailSettingsValid) beforeSave=beforeSave afterSave=afterSave tabindex="15"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/group-smtp-email-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/group-smtp-email-settings.hbs index 2b807460c7..525b66091b 100644 --- a/app/assets/javascripts/discourse/app/templates/components/group-smtp-email-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/group-smtp-email-settings.hbs @@ -8,11 +8,11 @@
- {{input type="text" name="smtp_server" value=form.smtp_server tabindex="3" onChange=(action "resetSettingsValid")}} + {{input type="text" name="smtp_server" value=form.smtp_server tabindex="4" onChange=(action "resetSettingsValid")}}
@@ -25,7 +25,15 @@
- {{input type="text" name="smtp_port" value=form.smtp_port tabindex="4" onChange=(action "resetSettingsValid" form.smtp_port)}} + {{input type="text" name="smtp_port" value=form.smtp_port tabindex="5" onChange=(action "resetSettingsValid" form.smtp_port)}} +
+
+ +
+
+ + {{input type="text" name="from_alias" id="from_alias" value=form.email_from_alias onChange=(action "resetSettingsValid") tabindex="3"}} +

{{i18n "groups.manage.email.settings.from_alias_hint"}}

@@ -43,7 +51,7 @@ action=(action "testSmtpSettings") icon="cog" label="groups.manage.email.test_settings" - tabindex="6" + tabindex="7" title="groups.manage.email.settings_required" }} diff --git a/app/assets/javascripts/discourse/app/templates/components/invite-link-panel.hbs b/app/assets/javascripts/discourse/app/templates/components/invite-link-panel.hbs index 75bbb20e4a..8e5858a1bc 100644 --- a/app/assets/javascripts/discourse/app/templates/components/invite-link-panel.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/invite-link-panel.hbs @@ -35,7 +35,6 @@ {{future-date-input includeDateTime=true - includeMidFuture=true clearable=true onChangeInput=(action (mut inviteExpiresAt)) }} diff --git a/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs b/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs index 8c78be04e6..a00118be6e 100644 --- a/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/parent-category-row.hbs @@ -1,7 +1,7 @@ {{#unless isHidden}} {{plugin-outlet name="category-list-above-each-category" args=(hash category=category)}} - + {{category-title-link category=category}} {{plugin-outlet name="below-category-title-link" connectorTagName="div" args=(hash category=category)}} {{#if category.description_excerpt}} diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-list-item.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-list-item.hbs index 556c0177f9..216d3b1b12 100644 --- a/app/assets/javascripts/discourse/app/templates/components/topic-list-item.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/topic-list-item.hbs @@ -1 +1,6 @@ {{topicListItemContents}} + +{{plugin-outlet + name="after-topic-list-item" + args=(hash topic=topic) +}} diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs index 52ac41fbf6..3ff3f75731 100644 --- a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs @@ -41,6 +41,7 @@ expandAllPinned=expandAllPinned lastVisitedTopic=lastVisitedTopic selected=selected + lastChecked=lastChecked tagsForUser=tagsForUser}} {{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/templates/group-index.hbs b/app/assets/javascripts/discourse/app/templates/group-index.hbs index 86aaea824b..71c929e137 100644 --- a/app/assets/javascripts/discourse/app/templates/group-index.hbs +++ b/app/assets/javascripts/discourse/app/templates/group-index.hbs @@ -4,7 +4,7 @@ {{text-field value=filterInput placeholderKey=filterPlaceholder - autocomplete="discourse" + autocomplete="off" class="group-username-filter no-blur" }} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/invites/show.hbs b/app/assets/javascripts/discourse/app/templates/invites/show.hbs index cacd25edc7..cda2c35542 100644 --- a/app/assets/javascripts/discourse/app/templates/invites/show.hbs +++ b/app/assets/javascripts/discourse/app/templates/invites/show.hbs @@ -70,7 +70,7 @@ {{/if}}
- {{input value=accountUsername class=(value-entered accountUsername) id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}} + {{input value=accountUsername class=(value-entered accountUsername) id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}} + {{/if}} {{/if}} {{#if list.topics}} diff --git a/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs index 73eb1be666..f9afbbe552 100644 --- a/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs @@ -10,9 +10,11 @@ {{#d-section class="user-invite-buttons"}} {{d-button class="btn-default" icon="plus" action=(action "createInvite") label="user.invited.create"}} {{#if canBulkInvite}} - {{#unless site.mobileView}} - {{d-button class="btn-default" icon="upload" action=(action "createInviteCsv") label="user.invited.bulk_invite.text"}} - {{/unless}} + {{#if siteSettings.allow_bulk_invite}} + {{#unless site.mobileView}} + {{d-button class="btn-default" icon="upload" action=(action "createInviteCsv") label="user.invited.bulk_invite.text"}} + {{/unless}} + {{/if}} {{/if}} {{#if showBulkActionButtons}} {{#if inviteExpired}} diff --git a/app/assets/javascripts/discourse/app/templates/user.hbs b/app/assets/javascripts/discourse/app/templates/user.hbs index f4521ef0df..700e8d06b2 100644 --- a/app/assets/javascripts/discourse/app/templates/user.hbs +++ b/app/assets/javascripts/discourse/app/templates/user.hbs @@ -179,6 +179,7 @@ {{plugin-outlet name="user-profile-primary" tagName="span" connectorTagName="div" args=(hash model=model)}}
+ {{plugin-outlet name="user-profile-above-collapsed-info" args=(hash model=model collapsedInfo=collapsedInfo)}} {{#unless collapsedInfo}}
diff --git a/app/assets/javascripts/discourse/app/templates/user/bookmarks.hbs b/app/assets/javascripts/discourse/app/templates/user/bookmarks.hbs index 105ce3924d..d9d8583c66 100644 --- a/app/assets/javascripts/discourse/app/templates/user/bookmarks.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/bookmarks.hbs @@ -12,7 +12,9 @@ value=searchTerm placeholder=(i18n "bookmarks.search_placeholder") enter=(action "search") - id="bookmark-search" autocomplete="discourse"}} + id="bookmark-search" + autocomplete="off" + }} {{d-button class="btn-primary" action=(action "search") diff --git a/app/assets/javascripts/discourse/app/templates/user/summary.hbs b/app/assets/javascripts/discourse/app/templates/user/summary.hbs index 921342b40d..2c72dc540f 100644 --- a/app/assets/javascripts/discourse/app/templates/user/summary.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/summary.hbs @@ -4,48 +4,48 @@

{{i18n "user.summary.stats"}}

    -
  • +
  • {{user-stat value=model.days_visited label="user.summary.days_visited"}}
  • -
  • +
  • {{user-stat value=timeRead label="user.summary.time_read" type="string"}}
  • {{#if showRecentTimeRead}} -
  • +
  • {{user-stat value=recentTimeRead label="user.summary.recent_time_read" type="string"}}
  • {{/if}} -
  • +
  • {{user-stat value=model.topics_entered label="user.summary.topics_entered"}}
  • -
  • +
  • {{user-stat value=model.posts_read_count label="user.summary.posts_read"}}
  • -
  • +
  • {{#link-to "userActivity.likesGiven"}} {{user-stat value=model.likes_given icon="heart" label="user.summary.likes_given"}} {{/link-to}}
  • +
  • + {{user-stat value=model.likes_received icon="heart" label="user.summary.likes_received"}} +
  • {{#if model.bookmark_count}} -
  • +
  • {{#link-to "userActivity.bookmarks"}} {{user-stat value=model.bookmark_count label="user.summary.bookmark_count"}} {{/link-to}}
  • {{/if}} -
  • +
  • {{#link-to "userActivity.topics"}} {{user-stat value=model.topic_count label="user.summary.topic_count"}} {{/link-to}}
  • -
  • +
  • {{#link-to "userActivity.replies"}} {{user-stat value=model.post_count label="user.summary.post_count"}} {{/link-to}}
  • -
  • - {{user-stat value=model.likes_received icon="heart" label="user.summary.likes_received"}} -
  • {{plugin-outlet name="user-summary-stat" connectorTagName="li" args=(hash model=model)}}
diff --git a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js index e87ec97bbb..16b6fff5cd 100644 --- a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js +++ b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js @@ -3,6 +3,7 @@ import { Promise } from "rsvp"; import Session from "discourse/models/session"; import { createWidget } from "discourse/widgets/widget"; import { h } from "virtual-dom"; +import { postRNWebviewMessage } from "discourse/lib/utilities"; /** * This tries to enforce a consistent flow of fetching, caching, refreshing, @@ -75,6 +76,7 @@ export default createWidget("quick-access-panel", { markRead() { return this.markReadRequest().then(() => { this.refreshNotifications(this.state); + postRNWebviewMessage("markRead", "1"); }); }, diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 39387cf7b0..18b770d977 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -71,13 +71,7 @@ module.exports = function (defaults) { }); let tests = concat(appTestTrees, { - inputFiles: [ - "**/tests/acceptance/*.js", - "**/tests/integration/*.js", - "**/tests/integration/**/*.js", - "**/tests/unit/*.js", - "**/tests/unit/**/*.js", - ], + inputFiles: ["**/tests/**/*-test.js"], headerFiles: ["vendor/ember-cli/tests-prefix.js"], footerFiles: ["vendor/ember-cli/app-config.js"], outputFile: "/assets/core-tests.js", @@ -104,13 +98,17 @@ module.exports = function (defaults) { // For example: our very specific version of bootstrap-modal. app.import(vendorJs + "bootbox.js"); app.import(vendorJs + "bootstrap-modal.js"); - app.import(vendorJs + "jquery.ui.widget.js"); app.import(vendorJs + "caret_position.js"); app.import("node_modules/ember-source/dist/ember-template-compiler.js", { type: "test", }); app.import(discourseRoot + "/app/assets/javascripts/polyfills.js"); + app.import( + discourseRoot + + "/app/assets/javascripts/discourse/public/assets/scripts/module-shims.js" + ); + const mergedTree = mergeTrees([ discourseScss(`${discourseRoot}/app/assets/stylesheets`, "testem.scss"), createI18nTree(discourseRoot, vendorJs), diff --git a/app/assets/javascripts/discourse/lib/rfc176-shims/.npmrc b/app/assets/javascripts/discourse/lib/rfc176-shims/.npmrc new file mode 100644 index 0000000000..c42da845b4 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/rfc176-shims/.npmrc @@ -0,0 +1 @@ +engine-strict = true diff --git a/app/assets/javascripts/discourse/lib/rfc176-shims/index.js b/app/assets/javascripts/discourse/lib/rfc176-shims/index.js new file mode 100644 index 0000000000..df26adac62 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/rfc176-shims/index.js @@ -0,0 +1,57 @@ +"use strict"; + +// In core, babel-plugin-ember-modules-api-polyfill takes care of re-writing the new module +// syntax to the legacy Ember globals. For themes and plugins, we need to manually set up +// the modules. +// +// Eventually, Ember RFC176 will be implemented, and we can drop these shims. + +const RFC176Data = require("ember-rfc176-data"); + +module.exports = { + name: require("./package").name, + + isDevelopingAddon() { + return true; + }, + + contentFor: function (type) { + if (type !== "vendor-suffix") { + return; + } + + const modules = {}; + + for (const entry of RFC176Data) { + // Entries look like: + // { + // global: 'Ember.expandProperties', + // module: '@ember/object/computed', + // export: 'expandProperties', + // deprecated: false + // }, + + if (entry.deprecated) { + continue; + } + + let m = modules[entry.module]; + if (!m) { + m = modules[entry.module] = []; + } + + m.push(entry); + } + + let output = ""; + for (const moduleName of Object.keys(modules)) { + const exports = modules[moduleName]; + const rawExports = exports + .map((e) => `${e.export}:${e.global}`) + .join(","); + output += `define("${moduleName}", () => {return {${rawExports}}});\n`; + } + + return output; + }, +}; diff --git a/app/assets/javascripts/discourse/lib/rfc176-shims/package.json b/app/assets/javascripts/discourse/lib/rfc176-shims/package.json new file mode 100644 index 0000000000..60bf829f39 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/rfc176-shims/package.json @@ -0,0 +1,6 @@ +{ + "name": "rfc176-shims", + "keywords": [ + "ember-addon" + ] +} diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 056a33e088..7b234f6291 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -49,6 +49,7 @@ "ember-load-initializers": "^2.1.1", "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^5.1.2", + "ember-rfc176-data": "^0.3.17", "ember-source": "~3.15.0", "ember-test-selectors": "^6.0.0", "eslint": "^7.27.0", @@ -78,7 +79,8 @@ }, "ember-addon": { "paths": [ - "lib/bootstrap-json" + "lib/bootstrap-json", + "lib/rfc176-shims" ] }, "devDependencies": { diff --git a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js index a1fe1ebd48..1bc1786c8d 100644 --- a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js +++ b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js @@ -2,157 +2,6 @@ if (window.unsupportedBrowser) { throw "Unsupported browser detected"; } - // TODO: These are needed to load plugins because @ember has its own loader. - // We should find a nicer way to do this. - const EMBER_MODULES = { - "@ember/application": { - default: Ember.Application, - setOwner: Ember.setOwner, - getOwner: Ember.getOwner, - }, - "@ember/array": { - default: Ember.Array, - A: Ember.A, - isArray: Ember.isArray, - }, - "@ember/array/proxy": { - default: Ember.ArrayProxy, - }, - "@ember/component": { - default: Ember.Component, - }, - "@ember/component/helper": { - default: Ember.Helper, - }, - "@ember/component/text-field": { - default: Ember.TextField, - }, - "@ember/component/text-area": { - default: Ember.TextArea, - }, - "@ember/controller": { - default: Ember.Controller, - inject: Ember.inject.controller, - }, - "@ember/debug": { - warn: Ember.warn, - }, - "@ember/error": { - default: Ember.error, - }, - "@ember/object": { - action: Ember._action, - default: Ember.Object, - get: Ember.get, - getProperties: Ember.getProperties, - set: Ember.set, - setProperties: Ember.setProperties, - computed: Ember.computed, - defineProperty: Ember.defineProperty, - }, - "@ember/object/computed": { - alias: Ember.computed.alias, - and: Ember.computed.and, - bool: Ember.computed.bool, - collect: Ember.computed.collect, - deprecatingAlias: Ember.computed.deprecatingAlias, - empty: Ember.computed.empty, - equal: Ember.computed.equal, - filter: Ember.computed.filter, - filterBy: Ember.computed.filterBy, - gt: Ember.computed.gt, - gte: Ember.computed.gte, - intersect: Ember.computed.intersect, - lt: Ember.computed.lt, - lte: Ember.computed.lte, - map: Ember.computed.map, - mapBy: Ember.computed.mapBy, - match: Ember.computed.match, - max: Ember.computed.max, - min: Ember.computed.min, - none: Ember.computed.none, - not: Ember.computed.not, - notEmpty: Ember.computed.notEmpty, - oneWay: Ember.computed.oneWay, - or: Ember.computed.or, - readOnly: Ember.computed.readOnly, - reads: Ember.computed.reads, - setDiff: Ember.computed.setDiff, - sort: Ember.computed.sort, - sum: Ember.computed.sum, - union: Ember.computed.union, - uniq: Ember.computed.uniq, - uniqBy: Ember.computed.uniqBy, - }, - "@ember/object/internals": { - guidFor: Ember.guidFor, - }, - "@ember/object/mixin": { default: Ember.Mixin }, - "@ember/object/proxy": { default: Ember.ObjectProxy }, - "@ember/object/promise-proxy-mixin": { default: Ember.PromiseProxyMixin }, - "@ember/object/evented": { - default: Ember.Evented, - on: Ember.on, - }, - "@ember/routing/route": { default: Ember.Route }, - "@ember/routing/router": { default: Ember.Router }, - "@ember/runloop": { - bind: Ember.run.bind, - cancel: Ember.run.cancel, - debounce: Ember.testing ? Ember.run : Ember.run.debounce, - later: Ember.run.later, - next: Ember.run.next, - once: Ember.run.once, - run: Ember.run, - schedule: Ember.run.schedule, - scheduleOnce: Ember.run.scheduleOnce, - throttle: Ember.run.throttle, - }, - "@ember/service": { - default: Ember.Service, - inject: Ember.inject.service, - }, - "@ember/string": { - w: Ember.String.w, - dasherize: Ember.String.dasherize, - decamelize: Ember.String.decamelize, - camelize: Ember.String.camelize, - classify: Ember.String.classify, - underscore: Ember.String.underscore, - capitalize: Ember.String.capitalize, - }, - "@ember/template": { - htmlSafe: Ember.String.htmlSafe, - }, - "@ember/utils": { - isBlank: Ember.isBlank, - isEmpty: Ember.isEmpty, - isNone: Ember.isNone, - isPresent: Ember.isPresent, - }, - jquery: { default: $ }, - rsvp: { - asap: Ember.RSVP.asap, - all: Ember.RSVP.all, - allSettled: Ember.RSVP.allSettled, - race: Ember.RSVP.race, - hash: Ember.RSVP.hash, - hashSettled: Ember.RSVP.hashSettled, - rethrow: Ember.RSVP.rethrow, - defer: Ember.RSVP.defer, - denodeify: Ember.RSVP.denodeify, - resolve: Ember.RSVP.resolve, - reject: Ember.RSVP.reject, - map: Ember.RSVP.map, - filter: Ember.RSVP.filter, - default: Ember.RSVP, - Promise: Ember.RSVP.Promise, - EventTarget: Ember.RSVP.EventTarget, - }, - }; - Object.keys(EMBER_MODULES).forEach((mod) => { - define(mod, () => EMBER_MODULES[mod]); - }); // TODO: Remove this and have resolver find the templates const prefix = "discourse/templates/"; @@ -166,15 +15,6 @@ } }); - define("I18n", ["exports"], function (exports) { - return I18n; - }); - - define("htmlbars-inline-precompile", ["exports"], function (exports) { - exports.default = function tag(strings) { - return Ember.Handlebars.compile(strings[0]); - }; - }); window.__widget_helpers = require("discourse-widget-hbs/helpers").default; // TODO: Eliminate this global diff --git a/app/assets/javascripts/discourse/public/assets/scripts/module-shims.js b/app/assets/javascripts/discourse/public/assets/scripts/module-shims.js new file mode 100644 index 0000000000..cab696c012 --- /dev/null +++ b/app/assets/javascripts/discourse/public/assets/scripts/module-shims.js @@ -0,0 +1,9 @@ +define("I18n", ["exports"], function (exports) { + return I18n; +}); + +define("htmlbars-inline-precompile", ["exports"], function (exports) { + exports.default = function tag(strings) { + return Ember.Handlebars.compile(strings[0]); + }; +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js index d5ace60556..8a1c4458cc 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js @@ -13,6 +13,9 @@ import { } from "@ember/test-helpers"; import siteSettingFixture from "discourse/tests/fixtures/site-settings"; import { test } from "qunit"; +import pretender from "discourse/tests/helpers/create-pretender"; + +const ENTER_KEYCODE = 13; acceptance("Admin - Site Settings", function (needs) { let updatedTitle; @@ -105,7 +108,7 @@ acceptance("Admin - Site Settings", function (needs) { ); await fillIn(".input-setting-string", "Test"); - await triggerKeyEvent(".input-setting-string", "keydown", 13); // enter + await triggerKeyEvent(".input-setting-string", "keydown", ENTER_KEYCODE); assert.ok( exists(".row.setting.overridden"), "saving via Enter key marks setting as overriden" @@ -163,4 +166,30 @@ acceptance("Admin - Site Settings", function (needs) { "/admin/site_settings/category/all_results?filter=contact" ); }); + + test("filters * and ? for domain lists", async (assert) => { + pretender.put("/admin/site_settings/blocked_onebox_domains", () => [200]); + + await visit("/admin/site_settings"); + await fillIn("#setting-filter", "domains"); + + await click(".select-kit-header.multi-select-header"); + + await fillIn(".select-kit-filter input", "cat.?.domain"); + await triggerKeyEvent(".select-kit-filter input", "keydown", ENTER_KEYCODE); + + await fillIn(".select-kit-filter input", "*.domain"); + await triggerKeyEvent(".select-kit-filter input", "keydown", ENTER_KEYCODE); + + await fillIn(".select-kit-filter input", "proper.com"); + await triggerKeyEvent(".select-kit-filter input", "keydown", ENTER_KEYCODE); + + await click("button.ok"); + + assert.strictEqual( + pretender.handledRequests[pretender.handledRequests.length - 1] + .requestBody, + "blocked_onebox_domains=proper.com" + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js index 7672f1a8ad..a1704a7626 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js @@ -8,7 +8,7 @@ import { import { click, fillIn, visit } from "@ember/test-helpers"; import I18n from "I18n"; import selectKit from "discourse/tests/helpers/select-kit-helper"; -import { skip } from "qunit"; +import { test } from "qunit"; import topicFixtures from "discourse/tests/fixtures/topic"; import { cloneJSON } from "discourse-common/lib/object"; @@ -104,7 +104,7 @@ acceptance("Bookmarking", function (needs) { server.get("/t/280.json", () => helper.response(topicResponse)); }); - skip("Bookmarks modal opening", async function (assert) { + test("Bookmarks modal opening", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); assert.ok( @@ -113,7 +113,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Bookmarks modal selecting reminder type", async function (assert) { + test("Bookmarks modal selecting reminder type", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); @@ -133,7 +133,7 @@ acceptance("Bookmarking", function (needs) { await click("#save-bookmark"); }); - skip("Saving a bookmark with a reminder", async function (assert) { + test("Saving a bookmark with a reminder", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); await fillIn("input#bookmark-name", "Check this out later"); @@ -151,7 +151,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Opening the options panel and remembering the option", async function (assert) { + test("Opening the options panel and remembering the option", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); await click(".bookmark-options-button"); @@ -174,7 +174,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Saving a bookmark with no reminder or name", async function (assert) { + test("Saving a bookmark with no reminder or name", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); await click("#save-bookmark"); @@ -183,7 +183,7 @@ acceptance("Bookmarking", function (needs) { exists(".topic-post:first-child button.bookmark.bookmarked"), "it shows the bookmarked icon on the post" ); - assert.not( + assert.notOk( exists( ".topic-post:first-child button.bookmark.bookmarked > .d-icon-discourse-bookmark-clock" ), @@ -191,7 +191,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Deleting a bookmark with a reminder", async function (assert) { + test("Deleting a bookmark with a reminder", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); await click("#tap_tile_tomorrow"); @@ -215,23 +215,23 @@ acceptance("Bookmarking", function (needs) { await click(".bootbox.modal .btn-primary"); - assert.not( + assert.notOk( exists(".topic-post:first-child button.bookmark.bookmarked"), "it no longer shows the bookmarked icon on the post after bookmark is deleted" ); }); - skip("Cancelling saving a bookmark", async function (assert) { + test("Cancelling saving a bookmark", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(); await click(".d-modal-cancel"); - assert.not( + assert.notOk( exists(".topic-post:first-child button.bookmark.bookmarked"), "it does not show the bookmarked icon on the post because it is not saved" ); }); - skip("Editing a bookmark", async function (assert) { + test("Editing a bookmark", async function (assert) { await visit("/t/internationalization-localization/280"); let now = moment.tz(loggedInUser().resolvedTimezone(loggedInUser())); let tomorrow = now.add(1, "day").format("YYYY-MM-DD"); @@ -257,7 +257,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Using a post date for the reminder date", async function (assert) { + test("Using a post date for the reminder date", async function (assert) { await visit("/t/internationalization-localization/280"); let postDate = moment.tz( "2036-01-15", @@ -286,7 +286,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Cannot use the post date for a reminder when the post date is in the past", async function (assert) { + test("Cannot use the post date for a reminder when the post date is in the past", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(2); assert.notOk( @@ -295,7 +295,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("The topic level bookmark button deletes all bookmarks if several posts on the topic are bookmarked", async function (assert) { + test("The topic level bookmark button deletes all bookmarks if several posts on the topic are bookmarked", async function (assert) { const yesButton = "a.btn-primary"; const noButton = "a.btn-default"; @@ -341,7 +341,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("The topic level bookmark button opens the edit modal if only the first post on the topic is bookmarked", async function (assert) { + test("The topic level bookmark button opens the edit modal if only the first post on the topic is bookmarked", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(1); await click("#save-bookmark"); @@ -360,7 +360,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Creating and editing a topic level bookmark", async function (assert) { + test("Creating and editing a topic level bookmark", async function (assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-button-bookmark"); @@ -432,7 +432,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("Deleting a topic_level bookmark with a reminder", async function (assert) { + test("Deleting a topic_level bookmark with a reminder", async function (assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-button-bookmark"); await click("#save-bookmark"); @@ -467,7 +467,7 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("The topic level bookmark button opens the edit modal if only one post in the post stream is bookmarked", async function (assert) { + test("The topic level bookmark button opens the edit modal if only one post in the post stream is bookmarked", async function (assert) { await visit("/t/internationalization-localization/280"); await openBookmarkModal(2); await click("#save-bookmark"); @@ -486,12 +486,12 @@ acceptance("Bookmarking", function (needs) { ); }); - skip("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the first post", async function (assert) { + test("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the first post", async function (assert) { const postNumber = 1; await testTopicLevelBookmarkButtonIcon(assert, postNumber); }); - skip("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the second post", async function (assert) { + test("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the second post", async function (assert) { const postNumber = 2; await testTopicLevelBookmarkButtonIcon(assert, postNumber); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/click-track-test.js b/app/assets/javascripts/discourse/tests/acceptance/click-track-test.js index 00e1f45ccb..3fde9f90b2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/click-track-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/click-track-test.js @@ -4,8 +4,7 @@ import { exists, } from "discourse/tests/helpers/qunit-helpers"; import { click, currentURL, visit } from "@ember/test-helpers"; -import { skip } from "qunit"; -// import { test } from "qunit"; +import { test } from "qunit"; acceptance("Click Track", function (needs) { let tracked = false; @@ -16,7 +15,7 @@ acceptance("Click Track", function (needs) { }); }); - skip("Do not track mentions", async function (assert) { + test("Do not track mentions", async function (assert) { await visit("/t/internationalization-localization/280"); assert.ok(!exists(".user-card.show"), "card should not appear"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-actions-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-actions-test.js index e620fc8809..acc54f4a38 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-actions-test.js @@ -13,7 +13,7 @@ import { Promise } from "rsvp"; import { _clearSnapshots } from "select-kit/components/composer-actions"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import sinon from "sinon"; -import { skip, test } from "qunit"; +import { test } from "qunit"; import { toggleCheckDraftPopup } from "discourse/controllers/composer"; acceptance("Composer Actions", function (needs) { @@ -35,7 +35,7 @@ acceptance("Composer Actions", function (needs) { assert.ok(queryAll(".d-editor-input").val(), "this is the reply"); }); - skip("replying to post", async function (assert) { + test("replying to post", async function (assert) { const composerActions = selectKit(".composer-actions"); await visit("/t/internationalization-localization/280"); @@ -76,7 +76,7 @@ acceptance("Composer Actions", function (needs) { ); }); - skip("replying to post - reply_to_topic", async function (assert) { + test("replying to post - reply_to_topic", async function (assert) { const composerActions = selectKit(".composer-actions"); await visit("/t/internationalization-localization/280"); @@ -103,7 +103,7 @@ acceptance("Composer Actions", function (needs) { ); }); - skip("replying to post - toggle_whisper", async function (assert) { + test("replying to post - toggle_whisper", async function (assert) { const composerActions = selectKit(".composer-actions"); await visit("/t/internationalization-localization/280"); @@ -405,7 +405,7 @@ acceptance("Composer Actions", function (needs) { ); }); - skip("replying to post as TL3 user", async function (assert) { + test("replying to post as TL3 user", async function (assert) { const composerActions = selectKit(".composer-actions"); updateCurrentUser({ moderator: false, admin: false, trust_level: 3 }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-edit-conflict-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-edit-conflict-test.js index 651951bd0b..312a295489 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-edit-conflict-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-edit-conflict-test.js @@ -1,7 +1,6 @@ -import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers"; +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import { click, fillIn, visit } from "@ember/test-helpers"; -import I18n from "I18n"; -import { skip, test } from "qunit"; +import { test } from "qunit"; acceptance("Composer - Edit conflict", function (needs) { needs.user(); @@ -14,24 +13,6 @@ acceptance("Composer - Edit conflict", function (needs) { }); }); - skip("Edit a post that causes an edit conflict", async function (assert) { - await visit("/t/internationalization-localization/280"); - await click(".topic-post:nth-of-type(1) button.show-more-actions"); - await click(".topic-post:nth-of-type(1) button.edit"); - await fillIn(".d-editor-input", "this will 409"); - await click("#reply-control button.create"); - assert.strictEqual( - queryAll("#reply-control button.create").text().trim(), - I18n.t("composer.overwrite_edit"), - "it shows the overwrite button" - ); - assert.ok( - queryAll("#draft-status .d-icon-user-edit"), - "error icon should be there" - ); - await click(".modal .btn-primary"); - }); - test("Should not send originalText when posting a new reply", async function (assert) { await visit("/t/internationalization-localization/280"); await click(".topic-post:nth-of-type(1) button.reply"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index 25e4a0fa46..2006c85467 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -1,9 +1,12 @@ import { run } from "@ember/runloop"; -import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; +import { click, currentURL, fillIn, settled, visit } from "@ember/test-helpers"; import { toggleCheckDraftPopup } from "discourse/controllers/composer"; import LinkLookup from "discourse/lib/link-lookup"; import { withPluginApi } from "discourse/lib/plugin-api"; -import { CREATE_TOPIC, NEW_TOPIC_KEY } from "discourse/models/composer"; +import Composer, { + CREATE_TOPIC, + NEW_TOPIC_KEY, +} from "discourse/models/composer"; import Draft from "discourse/models/draft"; import { acceptance, @@ -43,7 +46,7 @@ acceptance("Composer", function (needs) { }); }); - skip("Tests the Composer controls", async function (assert) { + test("Tests the Composer controls", async function (assert) { await visit("/"); assert.ok(exists("#create-topic"), "the create button is visible"); @@ -58,13 +61,13 @@ acceptance("Composer", function (needs) { "body errors are hidden by default" ); - await click("a.toggle-preview"); + await click(".toggle-preview"); assert.ok( !exists(".d-editor-preview:visible"), "clicking the toggle hides the preview" ); - await click("a.toggle-preview"); + await click(".toggle-preview"); assert.ok( exists(".d-editor-preview:visible"), "clicking the toggle shows the preview again" @@ -116,9 +119,9 @@ acceptance("Composer", function (needs) { ); await click("#reply-control a.cancel"); - assert.ok(exists(".bootbox.modal"), "it pops up a confirmation dialog"); + assert.ok(exists(".d-modal"), "it pops up a confirmation dialog"); - await click(".modal-footer a:nth-of-type(2)"); + await click(".modal-footer .discard-draft"); assert.ok(!exists(".bootbox.modal"), "the confirmation can be cancelled"); }); @@ -234,7 +237,7 @@ acceptance("Composer", function (needs) { ); }); - skip("Posting on a different topic", async function (assert) { + test("Posting on a different topic", async function (assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .btn.create"); await fillIn( @@ -386,24 +389,6 @@ acceptance("Composer", function (needs) { assert.strictEqual(count(".topic-post.staged"), 0); }); - skip("Editing a post can rollback to old content", async function (assert) { - await visit("/t/internationalization-localization/280"); - await click(".topic-post:nth-of-type(1) button.show-more-actions"); - await click(".topic-post:nth-of-type(1) button.edit"); - - await fillIn(".d-editor-input", "this will 409"); - await fillIn("#reply-title", "This is the new text for the title"); - await click("#reply-control button.create"); - - assert.ok(!exists(".topic-post.staged")); - assert.strictEqual( - query(".topic-post .cooked").innerText, - "Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?" - ); - - await click(".bootbox.modal .btn-primary"); - }); - test("Composer can switch between edits", async function (assert) { await visit("/t/this-is-a-test-topic/9"); @@ -686,7 +671,7 @@ acceptance("Composer", function (needs) { } }); - skip("Can switch states without abandon popup", async function (assert) { + test("Can switch states without abandon popup", async function (assert) { try { toggleCheckDraftPopup(true); @@ -834,7 +819,7 @@ acceptance("Composer", function (needs) { ); }); - skip("Shows duplicate_link notice", async function (assert) { + test("Shows duplicate_link notice", async function (assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .create"); @@ -936,3 +921,101 @@ acceptance("Composer - Customizations", function (needs) { ); }); }); + +// all of these are broken on legacy ember qunit for...some reason. commenting +// until we are fully on ember cli. +acceptance("Composer - Focus Open and Closed", function (needs) { + needs.user(); + + skip("Focusing a composer which is not open with create topic", async function (assert) { + await visit("/t/internationalization-localization/280"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ fallbackToNewTopic: true }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual(composer.model.action, Composer.CREATE_TOPIC); + }); + + skip("Focusing a composer which is not open with create topic and append text", async function (assert) { + await visit("/t/internationalization-localization/280"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ + fallbackToNewTopic: true, + insertText: "this is appended", + }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual( + query("textarea.d-editor-input").value.trim(), + "this is appended" + ); + }); + + skip("Focusing a composer which is already open", async function (assert) { + await visit("/"); + await click("#create-topic"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer(); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + }); + + skip("Focusing a composer which is already open and append text", async function (assert) { + await visit("/"); + await click("#create-topic"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ insertText: "this is some appended text" }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual( + query("textarea.d-editor-input").value.trim(), + "this is some appended text" + ); + }); + + skip("Focusing a composer which is not open that has a draft", async function (assert) { + await visit("/t/this-is-a-test-topic/9"); + + await click(".topic-post:nth-of-type(1) button.edit"); + await fillIn(".d-editor-input", "This is a dirty reply"); + await click(".toggle-minimize"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ insertText: "this is some appended text" }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual( + query("textarea.d-editor-input").value.trim(), + "This is a dirty reply\n\nthis is some appended text" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-uncategorized-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-uncategorized-test.js index be2d7b96a1..03499f9f4d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-uncategorized-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-uncategorized-test.js @@ -72,18 +72,21 @@ acceptance( id: 1, name: "test won", slug: "test-won", + permission: 1, topic_template: null, }, { id: 2, name: "test too", slug: "test-too", + permission: 1, topic_template: "", }, { id: 3, name: "test free", slug: "test-free", + permission: 1, topic_template: null, }, ], diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js index 5ee335fe01..b976eb69c4 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js @@ -2,14 +2,16 @@ import { acceptance, createFile, loggedInUser, + paste, query, } from "discourse/tests/helpers/qunit-helpers"; import { withPluginApi } from "discourse/lib/plugin-api"; import bootbox from "bootbox"; import { authorizedExtensions } from "discourse/lib/uploads"; -import { click, fillIn, visit } from "@ember/test-helpers"; +import { click, fillIn, settled, visit } from "@ember/test-helpers"; import I18n from "I18n"; import { skip, test } from "qunit"; +import { Promise } from "rsvp"; function pretender(server, helper) { server.post("/uploads/lookup-urls", () => { @@ -52,6 +54,7 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { needs.pretender(pretender); needs.settings({ simultaneous_uploads: 2, + enable_rich_text_paste: true, }); test("should insert the Uploading placeholder then the complete image placeholder", async function (assert) { @@ -132,28 +135,17 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { appEvents.trigger("composer:add-files", [jsonFile]); }); - // TODO: Had to comment this out for now; it works fine in Ember CLI but lagging - // UI updates sink it for the old Ember for some reason. Will re-enable - // when we make Ember CLI the primary. - - skip("cancelling uploads clears the placeholders out", async function (assert) { + test("cancelling uploads clears the placeholders out", async function (assert) { await visit("/"); await click("#create-topic"); await fillIn(".d-editor-input", "The image:\n"); + + const image = createFile("avatar.png"); + const image2 = createFile("avatar2.png"); + const appEvents = loggedInUser().appEvents; - const done = assert.async(); - - appEvents.on("composer:uploads-cancelled", () => { - assert.strictEqual( - query(".d-editor-input").value, - "The image:\n", - "it should clear the cancelled placeholders" - ); - done(); - }); - let uploadStarted = 0; - appEvents.on("composer:upload-started", async () => { + appEvents.on("composer:upload-started", () => { uploadStarted++; if (uploadStarted === 2) { @@ -164,14 +156,21 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); } }); - - appEvents.on("composer:uploads-preprocessing-complete", async () => { - await click("#cancel-file-upload"); + appEvents.on("composer:uploads-cancelled", () => { + assert.strictEqual( + query(".d-editor-input").value, + "The image:\n", + "it should clear the cancelled placeholders" + ); }); - const image = createFile("avatar.png"); - const image2 = createFile("avatar2.png"); - appEvents.trigger("composer:add-files", [image, image2]); + await new Promise(function (resolve) { + appEvents.on("composer:uploads-preprocessing-complete", function () { + resolve(); + }); + appEvents.trigger("composer:add-files", [image, image2]); + }); + await click("#cancel-file-upload"); }); test("should insert a newline before and after an image when pasting in the end of the line", async function (assert) { @@ -313,6 +312,64 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const image = createFile("avatar.png"); appEvents.trigger("composer:add-files", image); }); + + skip("should place cursor properly after inserting a placeholder", async function (assert) { + const appEvents = loggedInUser().appEvents; + const done = assert.async(); + + await visit("/"); + await click("#create-topic"); + await fillIn(".d-editor-input", "The image:\ntext after image"); + const input = query(".d-editor-input"); + input.selectionStart = 10; + input.selectionEnd = 10; + + appEvents.on("composer:all-uploads-complete", () => { + // after uploading we have this in the textarea: + // "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\ntext after image" + // cursor should be just before "text after image": + assert.equal(input.selectionStart, 76); + assert.equal(input.selectionEnd, 76); + done(); + }); + + const image = createFile("avatar.png"); + appEvents.trigger("composer:add-files", image); + }); + + test("should be able to paste a table with files and not upload the files", async function (assert) { + await visit("/"); + await click("#create-topic"); + const appEvents = loggedInUser().appEvents; + const done = assert.async(); + + let uppyEventFired = false; + + appEvents.on("composer:upload-started", () => { + uppyEventFired = true; + }); + + let element = query(".d-editor"); + let inputElement = query(".d-editor-input"); + inputElement.focus(); + await paste(element, "\ta\tb\n1\t2\t3", { + types: ["text/plain", "Files"], + files: [createFile("avatar.png")], + }); + await settled(); + + assert.strictEqual( + inputElement.value, + "||a|b|\n|---|---|---|\n|1|2|3|\n", + "only the plain text table is pasted" + ); + assert.strictEqual( + uppyEventFired, + false, + "uppy does not start uploading the file" + ); + done(); + }); }); acceptance("Uppy Composer Attachment - Upload Error", function (needs) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-account-external-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-account-external-test.js index e6ec013d54..2c5e0677ef 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/create-account-external-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/create-account-external-test.js @@ -52,7 +52,10 @@ acceptance("Create Account - external auth", function (needs) { "it shows the registration modal" ); - assert.not(exists("#new-account-username"), "it does not show the fields"); + assert.notOk( + exists("#new-account-username"), + "it does not show the fields" + ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js index fed440044b..a520901015 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js @@ -13,7 +13,7 @@ acceptance("Emoji", function (needs) { await fillIn(".d-editor-input", "this is an emoji :blonde_woman:"); assert.strictEqual( queryAll(".d-editor-preview:visible").html().trim(), - `

this is an emoji :blonde_woman:

` + `

this is an emoji :blonde_woman:

` ); }); @@ -24,7 +24,7 @@ acceptance("Emoji", function (needs) { await fillIn(".d-editor-input", "this is an emoji :blonde_woman:t5:"); assert.strictEqual( queryAll(".d-editor-preview:visible").html().trim(), - `

this is an emoji :blonde_woman:t5:

` + `

this is an emoji :blonde_woman:t5:

` ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js b/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js index ea91320e45..0485dbd73b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js @@ -5,7 +5,7 @@ import { selectText, } from "discourse/tests/helpers/qunit-helpers"; import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { test } from "qunit"; import postFixtures from "discourse/tests/fixtures/post"; import { cloneJSON } from "discourse-common/lib/object"; @@ -62,7 +62,7 @@ acceptance("Fast Edit", function (needs) { assert.notOk(exists("#fast-edit-input"), "fast editor is closed"); }); - skip("Opens full composer for multi-line selection", async function (assert) { + test("Opens full composer for multi-line selection", async function (assert) { await visit("/t/internationalization-localization/280"); const textNode = query("#post_2 .cooked"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js index 49ae5dd9f2..cff0acf17b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js @@ -106,6 +106,8 @@ acceptance( await fillIn('input[name="username"]', "myusername@gmail.com"); await fillIn('input[name="password"]', "password@gmail.com"); + await fillIn("#from_alias", "akasomegroup@example.com"); + await click(".test-smtp-settings"); assert.ok(exists(".smtp-settings-ok"), "tested settings are ok"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js index cbdfbede6e..0247a8ea0b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js @@ -119,7 +119,7 @@ acceptance("Invite accept", function (needs) { ); await fillIn("#new-account-email", "john.doe@example.com"); - assert.not( + assert.notOk( exists(".invites-show .btn-primary:disabled"), "submit is enabled" ); @@ -151,7 +151,7 @@ acceptance("Invite accept", function (needs) { assert.ok(exists(".username-input .good"), "username is valid"); assert.ok(exists(".password-input .good"), "password is valid"); assert.ok(exists(".email-input .good"), "email is valid"); - assert.not( + assert.notOk( exists(".invites-show .btn-primary:disabled"), "submit is enabled" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js index 814e727756..5dde2646a7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js @@ -67,7 +67,7 @@ acceptance("Accept Invite - User Fields", function (needs) { ); await click(".user-field input[type=checkbox]"); - assert.not( + assert.notOk( exists(".invites-show .btn-primary:disabled"), "submit is enabled because field is checked" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/jump-to-test.js b/app/assets/javascripts/discourse/tests/acceptance/jump-to-test.js index 9b539b7deb..94d60b9ab8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/jump-to-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/jump-to-test.js @@ -1,6 +1,6 @@ import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers"; import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { test } from "qunit"; acceptance("Jump to", function (needs) { needs.user(); @@ -37,7 +37,7 @@ acceptance("Jump to", function (needs) { ); }); - skip("invalid date", async function (assert) { + test("invalid date", async function (assert) { await visit("/t/internationalization-localization/280"); await click("nav#topic-progress .nums"); await click("button.jump-to-post"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/modal-test.js index 08b193e953..6a33ab945b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/modal-test.js @@ -6,7 +6,7 @@ import { queryAll, } from "discourse/tests/helpers/qunit-helpers"; import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { test } from "qunit"; import I18n from "I18n"; import hbs from "htmlbars-inline-precompile"; import { run } from "@ember/runloop"; @@ -30,7 +30,7 @@ acceptance("Modal", function (needs) { I18n.translations = _translations; }); - skip("modal", async function (assert) { + test("modal", async function (assert) { await visit("/"); assert.ok(!exists(".d-modal:visible"), "there is no modal at first"); @@ -51,7 +51,7 @@ acceptance("Modal", function (needs) { await click(".login-button"); assert.strictEqual(count(".d-modal:visible"), 1, "modal should reappear"); - await triggerKeyEvent("#main-outlet", "keyup", 27); + await triggerKeyEvent("#main-outlet", "keydown", 27); assert.ok(!exists(".d-modal:visible"), "ESC should close the modal"); Ember.TEMPLATES[ diff --git a/app/assets/javascripts/discourse/tests/acceptance/post-history-test.js b/app/assets/javascripts/discourse/tests/acceptance/post-history-test.js index 2a715a7f0c..6167e0ff3a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/post-history-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/post-history-test.js @@ -4,7 +4,7 @@ import { count, query, } from "discourse/tests/helpers/qunit-helpers"; -import { skip } from "qunit"; +import { test } from "qunit"; acceptance("Post - History", function (needs) { needs.user(); @@ -51,7 +51,7 @@ acceptance("Post - History", function (needs) { }); }); - skip("Shows highlighted tag changes", async function (assert) { + test("Shows highlighted tag changes", async function (assert) { await visit("/t/internationalization-localization/280"); await click("article[data-post-id='419'] .edits button"); assert.equal(count(".discourse-tag"), 4); diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js index 19169cfd2a..8efa99a8e5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js @@ -5,10 +5,9 @@ import { queryAll, selectDate, visible, - waitFor, } from "discourse/tests/helpers/qunit-helpers"; -import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { click, fillIn, visit } from "@ember/test-helpers"; +import { test } from "qunit"; import { SEARCH_TYPE_CATS_TAGS, SEARCH_TYPE_DEFAULT, @@ -150,45 +149,6 @@ acceptance("Search - Full Page", function (needs) { ); }); - skip("update username through advanced search ui", async function (assert) { - await visit("/search"); - await fillIn(".search-query", "none"); - await fillIn(".search-advanced-options .user-selector", "admin"); - await click(".search-advanced-options .user-selector"); - await triggerKeyEvent( - ".search-advanced-options .user-selector", - "keydown", - 8 - ); - - waitFor(assert, async () => { - assert.ok( - visible(".search-advanced-options .autocomplete"), - '"autocomplete" popup is visible' - ); - assert.ok( - exists( - '.search-advanced-options .autocomplete ul li a span.username:contains("admin")' - ), - '"autocomplete" popup has an entry for "admin"' - ); - - await click( - ".search-advanced-options .autocomplete ul li a:nth-of-type(1)" - ); - - assert.ok( - exists('.search-advanced-options span:contains("admin")'), - 'has "admin" pre-populated' - ); - assert.strictEqual( - queryAll(".search-query").val(), - "none @admin", - 'has updated search term to "none user:admin"' - ); - }); - }); - test("update category through advanced search ui", async function (assert) { const categoryChooser = selectKit( ".search-advanced-options .category-chooser" @@ -254,7 +214,7 @@ acceptance("Search - Full Page", function (needs) { await fillIn(".search-query", "none in:titleasd"); - assert.not( + assert.notOk( exists(".search-advanced-options .in-title:checked"), "does not populate title only checkbox" ); @@ -294,7 +254,7 @@ acceptance("Search - Full Page", function (needs) { await fillIn(".search-query", "none in:personal-direct"); - assert.not( + assert.notOk( exists(".search-advanced-options .in-private:checked"), "does not populate messages checkbox" ); @@ -318,7 +278,7 @@ acceptance("Search - Full Page", function (needs) { await fillIn(".search-query", "none in:seenasdan"); - assert.not( + assert.notOk( exists(".search-advanced-options .in-seen:checked"), "does not populate seen checkbox" ); @@ -486,7 +446,7 @@ acceptance("Search - Full Page", function (needs) { await fillIn(".search-query", "in:likesasdas"); - assert.not( + assert.notOk( exists(".search-advanced-options .in-likes:checked"), "does not populate the likes checkbox" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js index 182abe9802..42fd908caa 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js @@ -5,7 +5,7 @@ import { queryAll, } from "discourse/tests/helpers/qunit-helpers"; import { click, fillIn, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { test } from "qunit"; acceptance("Signing In", function () { test("sign in", async function (assert) { @@ -18,7 +18,7 @@ acceptance("Signing In", function () { await fillIn("#login-account-password", "incorrect"); await click(".modal-footer .btn-primary"); assert.ok(exists("#modal-alert:visible"), "it displays the login error"); - assert.not( + assert.notOk( exists(".modal-footer .btn-primary:disabled"), "enables the login button" ); @@ -81,7 +81,7 @@ acceptance("Signing In", function () { ); }); - skip("second factor", async function (assert) { + test("second factor", async function (assert) { await visit("/"); await click("header .login-button"); @@ -91,8 +91,7 @@ acceptance("Signing In", function () { await fillIn("#login-account-password", "need-second-factor"); await click(".modal-footer .btn-primary"); - assert.not(exists("#modal-alert:visible"), "it hides the login error"); - assert.not( + assert.notOk( exists("#credentials:visible"), "it hides the username and password prompt" ); @@ -100,7 +99,7 @@ acceptance("Signing In", function () { exists("#second-factor:visible"), "it displays the second factor prompt" ); - assert.not( + assert.notOk( exists(".modal-footer .btn-primary:disabled"), "enables the login button" ); @@ -114,7 +113,7 @@ acceptance("Signing In", function () { ); }); - skip("security key", async function (assert) { + test("security key", async function (assert) { await visit("/"); await click("header .login-button"); @@ -124,12 +123,11 @@ acceptance("Signing In", function () { await fillIn("#login-account-password", "need-security-key"); await click(".modal-footer .btn-primary"); - assert.not(exists("#modal-alert:visible"), "it hides the login error"); - assert.not( + assert.notOk( exists("#credentials:visible"), "it hides the username and password prompt" ); - assert.not( + assert.notOk( exists("#login-second-factor:visible"), "it does not display the second factor prompt" ); @@ -137,7 +135,7 @@ acceptance("Signing In", function () { exists("#security-key:visible"), "it shows the security key prompt" ); - assert.not(exists("#login-button:visible"), "hides the login button"); + assert.notOk(exists("#login-button:visible"), "hides the login button"); }); test("create account", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sticky-avatars-test.js b/app/assets/javascripts/discourse/tests/acceptance/sticky-avatars-test.js deleted file mode 100644 index 042f8232f5..0000000000 --- a/app/assets/javascripts/discourse/tests/acceptance/sticky-avatars-test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; -import { skip } from "qunit"; -import { visit } from "@ember/test-helpers"; - -acceptance("Sticky Avatars", function () { - skip("Adds sticky avatars when scrolling up", async function (assert) { - const container = document.getElementById("ember-testing-container"); - container.scrollTo(0, 0); - - await visit("/t/internationalization-localization/280"); - container.scrollTo(0, 800); - container.scrollTo(0, 700); - - assert.ok( - query("#post_5").parentElement.classList.contains("sticky-avatar"), - "Sticky avatar is applied" - ); - }); -}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js index bfd8e811ce..f31f4cb272 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js @@ -4,7 +4,7 @@ import { queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; -import { click, visit } from "@ember/test-helpers"; +import { click, triggerEvent, visit } from "@ember/test-helpers"; import { test } from "qunit"; import I18n from "I18n"; @@ -121,4 +121,32 @@ acceptance("Topic - Bulk Actions", function (needs) { "it closes the bulk select modal" ); }); + + test("bulk select - Shift click selection", async function (assert) { + updateCurrentUser({ moderator: true }); + await visit("/latest"); + await click("button.bulk-select"); + + await click(queryAll("input.bulk-select")[0]); + await triggerEvent(queryAll("input.bulk-select")[3], "click", { + shiftKey: true, + }); + assert.equal( + queryAll("input.bulk-select:checked").length, + 4, + "Shift click selects a range" + ); + + await click("button.bulk-clear-all"); + + await click(queryAll("input.bulk-select")[5]); + await triggerEvent(queryAll("input.bulk-select")[1], "click", { + shiftKey: true, + }); + assert.equal( + queryAll("input.bulk-select:checked").length, + 5, + "Bottom-up Shift click range selection works" + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-move-posts-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-move-posts-test.js index 04a1e68ad5..87b8bbc592 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-move-posts-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-move-posts-test.js @@ -63,7 +63,7 @@ acceptance("Topic move posts", function (needs) { "it opens move to modal" ); - assert.not( + assert.notOk( queryAll(".choose-topic-modal .radios") .html() .includes(I18n.t("topic.split_topic.radio_label")), @@ -77,7 +77,7 @@ acceptance("Topic move posts", function (needs) { "it shows an option to move to existing topic" ); - assert.not( + assert.notOk( queryAll(".choose-topic-modal .radios") .html() .includes(I18n.t("topic.move_to_new_message.radio_label")), diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-quote-button-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-quote-button-test.js index d539c69373..6c9c7bb9c2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-quote-button-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-quote-button-test.js @@ -1,12 +1,13 @@ import { acceptance, + chromeTest, exists, queryAll, selectText, } from "discourse/tests/helpers/qunit-helpers"; import I18n from "I18n"; import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { test } from "qunit"; // This tests are flaky on Firefox. Fails with `calling set on destroyed object` acceptance("Topic - Quote button - logged in", function (needs) { @@ -16,42 +17,50 @@ acceptance("Topic - Quote button - logged in", function (needs) { share_quote_buttons: "twitter|email", }); - // All these skips were chromeTest - skip("Does not show the quote share buttons by default", async function (assert) { - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); - assert.ok(exists(".insert-quote"), "it shows the quote button"); - assert.ok(!exists(".quote-sharing"), "it does not show quote sharing"); - }); + chromeTest( + "Does not show the quote share buttons by default", + async function (assert) { + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); + assert.ok(exists(".insert-quote"), "it shows the quote button"); + assert.ok(!exists(".quote-sharing"), "it does not show quote sharing"); + } + ); - skip("Shows quote share buttons with the right site settings", async function (assert) { - this.siteSettings.share_quote_visibility = "all"; + chromeTest( + "Shows quote share buttons with the right site settings", + async function (assert) { + this.siteSettings.share_quote_visibility = "all"; - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); - assert.ok(exists(".quote-sharing"), "it shows the quote sharing options"); - assert.ok( - exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`), - "it includes the twitter share button" - ); - assert.ok( - exists(`.quote-sharing .btn[title='${I18n.t("share.email")}']`), - "it includes the email share button" - ); - }); + assert.ok(exists(".quote-sharing"), "it shows the quote sharing options"); + assert.ok( + exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`), + "it includes the twitter share button" + ); + assert.ok( + exists(`.quote-sharing .btn[title='${I18n.t("share.email")}']`), + "it includes the email share button" + ); + } + ); - skip("Quoting a Onebox should not copy the formatting of the rendered Onebox", async function (assert) { - await visit("/t/topic-for-group-moderators/2480"); - await selectText("#post_3 aside.onebox p"); - await click(".insert-quote"); + chromeTest( + "Quoting a Onebox should not copy the formatting of the rendered Onebox", + async function (assert) { + await visit("/t/topic-for-group-moderators/2480"); + await selectText("#post_3 aside.onebox p"); + await click(".insert-quote"); - assert.strictEqual( - queryAll(".d-editor-input").val().trim(), - '[quote="group_moderator, post:3, topic:2480"]\nhttps://example.com/57350945\n[/quote]', - "quote only contains a link" - ); - }); + assert.strictEqual( + queryAll(".d-editor-input").val().trim(), + '[quote="group_moderator, post:3, topic:2480"]\nhttps://example.com/57350945\n[/quote]', + "quote only contains a link" + ); + } + ); }); acceptance("Topic - Quote button - anonymous", function (needs) { @@ -60,48 +69,60 @@ acceptance("Topic - Quote button - anonymous", function (needs) { share_quote_buttons: "twitter|email", }); - skip("Shows quote share buttons with the right site settings", async function (assert) { - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); + chromeTest( + "Shows quote share buttons with the right site settings", + async function (assert) { + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); - assert.ok(queryAll(".quote-sharing"), "it shows the quote sharing options"); - assert.ok( - exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`), - "it includes the twitter share button" - ); - assert.ok( - exists(`.quote-sharing .btn[title='${I18n.t("share.email")}']`), - "it includes the email share button" - ); - assert.ok(!exists(".insert-quote"), "it does not show the quote button"); - }); + assert.ok( + queryAll(".quote-sharing"), + "it shows the quote sharing options" + ); + assert.ok( + exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`), + "it includes the twitter share button" + ); + assert.ok( + exists(`.quote-sharing .btn[title='${I18n.t("share.email")}']`), + "it includes the email share button" + ); + assert.ok(!exists(".insert-quote"), "it does not show the quote button"); + } + ); - skip("Shows single share button when site setting only has one item", async function (assert) { - this.siteSettings.share_quote_buttons = "twitter"; + chromeTest( + "Shows single share button when site setting only has one item", + async function (assert) { + this.siteSettings.share_quote_buttons = "twitter"; - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); - assert.ok(exists(".quote-sharing"), "it shows the quote sharing options"); - assert.ok( - exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`), - "it includes the twitter share button" - ); - assert.ok( - !exists(".quote-share-label"), - "it does not show the Share label" - ); - }); + assert.ok(exists(".quote-sharing"), "it shows the quote sharing options"); + assert.ok( + exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`), + "it includes the twitter share button" + ); + assert.ok( + !exists(".quote-share-label"), + "it does not show the Share label" + ); + } + ); - skip("Shows nothing when visibility is disabled", async function (assert) { - this.siteSettings.share_quote_visibility = "none"; + chromeTest( + "Shows nothing when visibility is disabled", + async function (assert) { + this.siteSettings.share_quote_visibility = "none"; - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); - assert.ok(!exists(".quote-sharing"), "it does not show quote sharing"); - assert.ok(!exists(".insert-quote"), "it does not show the quote button"); - }); + assert.ok(!exists(".quote-sharing"), "it does not show quote sharing"); + assert.ok(!exists(".insert-quote"), "it does not show the quote button"); + } + ); }); acceptance("Topic - Quote button - keyboard shortcut", function (needs) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js index 8d0a61ca49..da2305ba7d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js @@ -1,5 +1,6 @@ import { acceptance, + chromeTest, count, exists, query, @@ -16,7 +17,7 @@ import { } from "@ember/test-helpers"; import I18n from "I18n"; import selectKit from "discourse/tests/helpers/select-kit-helper"; -import { skip, test } from "qunit"; +import { test } from "qunit"; import { withPluginApi } from "discourse/lib/plugin-api"; import topicFixtures from "discourse/tests/fixtures/topic"; import { cloneJSON } from "discourse-common/lib/object"; @@ -354,59 +355,69 @@ acceptance("Topic featured links", function (needs) { assert.ok(!exists(".gap"), "it hides gap"); }); - // quote related skip tests were chromeTest before + chromeTest( + "Quoting a quote keeps the original poster name", + async function (assert) { + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); + await click(".quote-button .insert-quote"); - skip("Quoting a quote keeps the original poster name", async function (assert) { - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); - await click(".quote-button .insert-quote"); + assert.ok( + queryAll(".d-editor-input") + .val() + .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 + ); + } + ); - assert.ok( - queryAll(".d-editor-input") - .val() - .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 - ); - }); + chromeTest( + "Quoting a quote of a different topic keeps the original topic title", + async function (assert) { + await visit("/t/internationalization-localization/280"); + await selectText("#post_9 blockquote"); + await click(".quote-button .insert-quote"); - skip("Quoting a quote of a different topic keeps the original topic title", async function (assert) { - await visit("/t/internationalization-localization/280"); - await selectText("#post_9 blockquote"); - await click(".quote-button .insert-quote"); + assert.ok( + queryAll(".d-editor-input") + .val() + .indexOf( + 'quote="A new topic with a link to another topic, post:3, topic:62"' + ) !== -1 + ); + } + ); - assert.ok( - queryAll(".d-editor-input") - .val() - .indexOf( - 'quote="A new topic with a link to another topic, post:3, topic:62"' - ) !== -1 - ); - }); + chromeTest( + "Quoting a quote with the Reply button keeps the original poster name", + async function (assert) { + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); + await click(".reply"); - skip("Quoting a quote with the Reply button keeps the original poster name", async function (assert) { - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); - await click(".reply"); - - assert.ok( - queryAll(".d-editor-input") - .val() - .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 - ); - }); + assert.ok( + queryAll(".d-editor-input") + .val() + .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 + ); + } + ); // Using J/K on Firefox clean the text selection, so this won't work there - skip("Quoting a quote with replyAsNewTopic keeps the original poster name", async function (assert) { - await visit("/t/internationalization-localization/280"); - await selectText("#post_5 blockquote"); - await triggerKeyEvent(document, "keypress", "j".charCodeAt(0)); - await triggerKeyEvent(document, "keypress", "t".charCodeAt(0)); + chromeTest( + "Quoting a quote with replyAsNewTopic keeps the original poster name", + async function (assert) { + await visit("/t/internationalization-localization/280"); + await selectText("#post_5 blockquote"); + await triggerKeyEvent(document, "keypress", "j".charCodeAt(0)); + await triggerKeyEvent(document, "keypress", "t".charCodeAt(0)); - assert.ok( - queryAll(".d-editor-input") - .val() - .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 - ); - }); + assert.ok( + queryAll(".d-editor-input") + .val() + .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 + ); + } + ); test("Quoting by selecting text can mark the quote as full", async function (assert) { await visit("/t/internationalization-localization/280"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/unknown-test.js b/app/assets/javascripts/discourse/tests/acceptance/unknown-test.js index a0d15313ad..b14e9f08df 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/unknown-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/unknown-test.js @@ -1,6 +1,6 @@ import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers"; import { click, currentURL, visit } from "@ember/test-helpers"; -import { skip, test } from "qunit"; +import { test } from "qunit"; acceptance("Category 404", function (needs) { needs.pretender((server, helper) => { @@ -12,7 +12,7 @@ acceptance("Category 404", function (needs) { }); }); }); - skip("Navigating to a bad category link does not break the router", async function (assert) { + test("Navigating to a bad category link does not break the router", async function (assert) { await visit("/t/internationalization-localization/280"); await click('[data-for-test="category-404"]'); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-bookmarks-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-bookmarks-test.js index a0fd7072d3..faa7418de2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-bookmarks-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-bookmarks-test.js @@ -16,7 +16,7 @@ acceptance("User's bookmarks", function (needs) { await dropdown.expand(); await dropdown.selectRowByValue("remove"); - assert.not(exists(".bootbox.modal"), "it should not show the modal"); + assert.notOk(exists(".bootbox.modal"), "it should not show the modal"); }); test("it renders search controls if there are bookmarks", async function (assert) { @@ -46,7 +46,7 @@ acceptance("User's bookmarks - reminder", function (needs) { assert.ok(exists(".bootbox.modal"), "it asks for delete confirmation"); await click(".bootbox.modal a.btn-primary"); - assert.not(exists(".bootbox.modal")); + assert.notOk(exists(".bootbox.modal")); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js index 4b4a0c1aa3..95b7eb5b20 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js @@ -20,7 +20,7 @@ acceptance("User Card - Show Local Time", function (needs) { await visit("/t/internationalization-localization/280"); await click('a[data-user-card="charlie"]'); - assert.not( + assert.notOk( exists(".user-card .local-time"), "it does not show the local time if the user card returns a null/undefined timezone for another user" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js index cbed14814f..ace7abce1d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js @@ -55,7 +55,7 @@ acceptance("User Drafts", function (needs) { ); assert.strictEqual( query(".user-stream-item:nth-child(3) .excerpt").innerHTML.trim(), - `here goes a reply to a PM :slight_smile:` + `here goes a reply to a PM :slight_smile:` ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-test.js index 32375c88b3..121ccb9e48 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-test.js @@ -1,3 +1,7 @@ +import EmberObject from "@ember/object"; +import User from "discourse/models/user"; +import selectKit from "discourse/tests/helpers/select-kit-helper"; +import sinon from "sinon"; import { acceptance, exists, @@ -131,3 +135,45 @@ acceptance("User Routes - Moderator viewing warnings", function (needs) { assert.ok($("div.alert-info").length, "has the permissions alert"); }); }); + +acceptance("User - Saving user options", function (needs) { + needs.user({ + admin: false, + moderator: false, + username: "eviltrout", + id: 1, + user_option: EmberObject.create({}), + }); + + needs.settings({ + disable_mailing_list_mode: false, + }); + + needs.pretender((server, helper) => { + server.put("/u/eviltrout.json", () => { + return helper.response(200, { user: {} }); + }); + }); + + test("saving user options", async function (assert) { + const spy = sinon.spy(User.current(), "_saveUserData"); + + await visit("/u/eviltrout/preferences/emails"); + await click(".pref-mailing-list-mode input[type='checkbox']"); + await click(".save-changes"); + + assert.ok( + spy.calledWithMatch({ mailing_list_mode: true }), + "sends a PUT request to update the specified user option" + ); + + await selectKit("#user-email-messages-level").expand(); + await selectKit("#user-email-messages-level").selectRowByValue(2); // never option + await click(".save-changes"); + + assert.ok( + spy.calledWithMatch({ email_messages_level: 2 }), + "is able to save a different user_option on a subsequent request" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/fixtures/site-settings.js b/app/assets/javascripts/discourse/tests/fixtures/site-settings.js index f5fca9c661..8b3bbbee64 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/site-settings.js +++ b/app/assets/javascripts/discourse/tests/fixtures/site-settings.js @@ -64,7 +64,19 @@ export default { secret: false, type: "upload", plugin: "discourse-logo" - } + }, + { + category: "onebox", + default: "", + description: + "A list of domains that will never be oneboxed e.g. wikipedia.org\n(Wildcard symbols * ? not supported)", + placeholder: null, + preview: null, + secret: false, + setting: "blocked_onebox_domains", + type: "host_list", + value: "", + }, ], diags: { last_message_processed: null diff --git a/app/assets/javascripts/discourse/tests/helpers/create-store.js b/app/assets/javascripts/discourse/tests/helpers/create-store.js index 93a8520d6f..6933355dec 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-store.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-store.js @@ -5,6 +5,7 @@ import TopicListAdapter from "discourse/adapters/topic-list"; import TopicTrackingState from "discourse/models/topic-tracking-state"; import { buildResolver } from "discourse-common/resolver"; import { currentSettings } from "discourse/tests/helpers/site-settings"; +import Site from "discourse/models/site"; const CatAdapter = RestAdapter.extend({ primaryKey: "cat_id", @@ -13,6 +14,10 @@ const CatAdapter = RestAdapter.extend({ export default function (customLookup = () => {}) { const resolver = buildResolver("discourse").create(); + // Normally this would happen in inject-discourse-objects. + // However, `create-store` is used by unit tests which do not init the application. + Site.current(); + return Store.create({ register: { lookup(type) { diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index f99e83fea6..6c54d60780 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -1,4 +1,5 @@ import QUnit, { module, skip, test } from "qunit"; +import { deepMerge } from "discourse-common/lib/object"; import MessageBus from "message-bus-client"; import { clearCache as clearOutletCache, @@ -12,15 +13,15 @@ import { import { forceMobile, resetMobile } from "discourse/lib/mobile"; import { getApplication, getContext, settled } from "@ember/test-helpers"; import { getOwner } from "discourse-common/lib/get-owner"; -import { later, run } from "@ember/runloop"; +import { run } from "@ember/runloop"; import { setupApplicationTest } from "ember-qunit"; import { Promise } from "rsvp"; import Site from "discourse/models/site"; import User from "discourse/models/user"; import { _clearSnapshots } from "select-kit/components/composer-actions"; import { clearHTMLCache } from "discourse/helpers/custom-html"; -import createStore from "discourse/tests/helpers/create-store"; import deprecated from "discourse-common/lib/deprecated"; +import { restoreBaseUri } from "discourse-common/lib/get-url"; import { flushMap } from "discourse/services/store"; import { initSearchData } from "discourse/widgets/search-menu"; import { resetPostMenuExtraButtons } from "discourse/widgets/post-menu"; @@ -38,7 +39,10 @@ import { resetCardClickListenerSelector } from "discourse/mixins/card-contents-b import { resetComposerCustomizations } from "discourse/models/composer"; import { resetQuickSearchRandomTips } from "discourse/widgets/search-menu-results"; import sessionFixtures from "discourse/tests/fixtures/session-fixtures"; -import { setTopicList } from "discourse/lib/topic-list-tracker"; +import { + resetHighestReadCache, + setTopicList, +} from "discourse/lib/topic-list-tracker"; import sinon from "sinon"; import siteFixtures from "discourse/tests/fixtures/site-fixtures"; import { clearResolverOptions } from "discourse-common/resolver"; @@ -57,6 +61,7 @@ import { clearPresenceCallbacks, setTestPresence, } from "discourse/lib/user-presence"; +import PreloadStore from "discourse/lib/preload-store"; const LEGACY_ENV = !setupApplicationTest; @@ -115,9 +120,9 @@ export function resetSite(siteSettings, extras) { siteFixtures["site.json"].site, extras || {} ); - siteAttrs.store = createStore(); siteAttrs.siteSettings = siteSettings; - return Site.resetCurrent(Site.create(siteAttrs)); + PreloadStore.store("site", siteAttrs); + Site.resetCurrent(); } export function applyPretender(name, server, helper) { @@ -158,6 +163,7 @@ function testCleanup(container, app) { resetOneboxCache(); resetCustomPostMessageCallbacks(); resetUserSearchCache(); + resetHighestReadCache(); resetCardClickListenerSelector(); resetComposerCustomizations(); resetQuickSearchRandomTips(); @@ -177,6 +183,7 @@ function testCleanup(container, app) { if (!LEGACY_ENV) { clearPresenceCallbacks(); } + restoreBaseUri(); } export function discourseModule(name, options) { @@ -400,6 +407,11 @@ export function fixture(selector) { } QUnit.assert.not = function (actual, message) { + deprecated("assert.not() is deprecated. Use assert.notOk() instead.", { + since: "2.9.0.beta1", + dropFrom: "2.10.0.beta1", + }); + this.pushResult({ result: !actual, actual, @@ -432,16 +444,6 @@ QUnit.assert.containsInstance = function (collection, klass, message) { }); }; -export function waitFor(assert, callback, timeout) { - timeout = timeout || 500; - - const done = assert.async(); - later(() => { - callback(); - done(); - }, timeout); -} - export async function selectDate(selector, date) { return new Promise((resolve) => { const elem = document.querySelector(selector); @@ -561,3 +563,11 @@ export function createFile(name, type = "image/png", blobData = null) { }); return file; } + +export async function paste(element, text, otherClipboardData = {}) { + let e = new Event("paste", { cancelable: true }); + e.clipboardData = deepMerge({ getData: () => text }, otherClipboardData); + element.dispatchEvent(e); + await settled(); + return e; +} diff --git a/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js b/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js index 1bdea3ddca..0e2cc1eaad 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js @@ -12,50 +12,36 @@ discourseModule("Integration | Component | ace-editor", function (hooks) { setupRenderingTest(hooks); componentTest("css editor", { - skip: true, template: hbs`{{ace-editor mode="css"}}`, test(assert) { - assert.expect(1); assert.ok(exists(".ace_editor"), "it renders the ace editor"); }, }); componentTest("html editor", { - skip: true, template: hbs`{{ace-editor mode="html" content="wat"}}`, test(assert) { - assert.expect(1); assert.ok(exists(".ace_editor"), "it renders the ace editor"); }, }); componentTest("sql editor", { - skip: true, template: hbs`{{ace-editor mode="sql" content="SELECT * FROM users"}}`, test(assert) { - assert.expect(1); assert.ok(exists(".ace_editor"), "it renders the ace editor"); }, }); componentTest("disabled editor", { - skip: true, template: hbs` {{ace-editor mode="sql" content="SELECT * FROM users" disabled=true}} `, test(assert) { - const $ace = queryAll(".ace_editor"); - assert.expect(3); - assert.ok($ace.length, "it renders the ace editor"); - assert.strictEqual( - $ace.parent().data().editor.getReadOnly(), - true, - "it sets ACE to read-only mode" - ); - assert.strictEqual( - $ace.parent().attr("data-disabled"), - "true", - "ACE wrapper has `data-disabled` attribute set to true" + assert.ok(exists(".ace_editor"), "it renders the ace editor"); + assert.equal( + queryAll(".ace-wrapper[data-disabled]").length, + 1, + "it has a data-disabled attr" ); }, }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js b/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js index 0226bef7c4..36fb680dfa 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js @@ -1,16 +1,8 @@ import componentTest, { setupRenderingTest, } from "discourse/tests/helpers/component-test"; -import { - discourseModule, - exists, - fakeTime, - query, - queryAll, -} from "discourse/tests/helpers/qunit-helpers"; -import I18n from "I18n"; +import { discourseModule, query } from "discourse/tests/helpers/qunit-helpers"; import hbs from "htmlbars-inline-precompile"; -import { click } from "@ember/test-helpers"; discourseModule("Integration | Component | bookmark", function (hooks) { setupRenderingTest(hooks); @@ -34,143 +26,6 @@ discourseModule("Integration | Component | bookmark", function (hooks) { }); }); - hooks.afterEach(function () { - if (this.clock) { - this.clock.restore(); - } - }); - - componentTest("shows correct options", { - template, - - beforeEach() { - const tuesday = "2100-06-08T08:00:00"; - this.clock = fakeTime(tuesday, this.currentUser._timezone, true); - }, - - async test(assert) { - const expected = [ - I18n.t("time_shortcut.later_today"), - I18n.t("time_shortcut.tomorrow"), - I18n.t("time_shortcut.later_this_week"), - I18n.t("time_shortcut.start_of_next_business_week"), - I18n.t("time_shortcut.next_month"), - I18n.t("time_shortcut.custom"), - I18n.t("time_shortcut.none"), - ]; - - const options = Array.from( - queryAll( - "div.control-group div.tap-tile-grid div.tap-tile-title" - ).map((_, div) => div.innerText.trim()) - ); - - assert.deepEqual(options, expected); - }, - }); - - componentTest("show later this week option if today is < Thursday", { - template, - - beforeEach() { - const monday = "2100-06-07T08:00:00"; - this.clock = fakeTime(monday, this.currentUser._timezone, true); - }, - - test(assert) { - assert.ok(exists("#tap_tile_later_this_week"), "it has later this week"); - }, - }); - - componentTest( - "does not show later this week option if today is >= Thursday", - { - template, - - beforeEach() { - const thursday = "2100-06-10T08:00:00"; - this.clock = fakeTime(thursday, this.currentUser._timezone, true); - }, - - test(assert) { - assert.notOk( - exists("#tap_tile_later_this_week"), - "it does not have later this week" - ); - }, - } - ); - - componentTest("later today does not show if later today is tomorrow", { - template, - - beforeEach() { - this.clock = fakeTime( - "2100-12-11T22:00:00", - this.currentUser._timezone, - true - ); - }, - - test(assert) { - assert.notOk( - exists("#tap_tile_later_today"), - "it does not have later today" - ); - }, - }); - - componentTest("later today shows if it is after 5pm but before 6pm", { - template, - - beforeEach() { - this.clock = fakeTime( - "2100-12-11T14:30:00", - this.currentUser._timezone, - true - ); - }, - - test(assert) { - assert.ok(exists("#tap_tile_later_today"), "it does have later today"); - }, - }); - - componentTest("later today does not show if it is after 5pm", { - template, - - beforeEach() { - this.clock = fakeTime( - "2100-12-11T17:00:00", - this.currentUser._timezone, - true - ); - }, - - test(assert) { - assert.notOk( - exists("#tap_tile_later_today"), - "it does not have later today" - ); - }, - }); - - componentTest("later today does show if it is before the end of the day", { - template, - - beforeEach() { - this.clock = fakeTime( - "2100-12-11T13:00:00", - this.currentUser._timezone, - true - ); - }, - - test(assert) { - assert.ok(exists("#tap_tile_later_today"), "it does have later today"); - }, - }); - componentTest("prefills the custom reminder type date and time", { template, @@ -189,32 +44,4 @@ discourseModule("Integration | Component | bookmark", function (hooks) { assert.strictEqual(query("#custom-time").value, "09:45"); }, }); - - componentTest("defaults to 08:00 for custom time", { - template, - - async test(assert) { - await click("#tap_tile_custom"); - assert.strictEqual(query("#custom-time").value, "08:00"); - }, - }); - - componentTest("Next Month points to the first day of the next month", { - template, - - beforeEach() { - this.clock = fakeTime( - "2100-01-01T08:00:00", - this.currentUser._timezone, - true - ); - }, - - async test(assert) { - assert.strictEqual( - query("div#tap_tile_next_month div.tap-tile-date").innerText, - "Feb 1, 8:00 am" - ); - }, - }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js b/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js index d80d186ff9..df9c4b6388 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js @@ -5,6 +5,7 @@ import componentTest, { import { discourseModule, exists, + paste, query, queryAll, } from "discourse/tests/helpers/qunit-helpers"; @@ -822,14 +823,6 @@ third line` } ); - async function paste(element, text) { - let e = new Event("paste", { cancelable: true }); - e.clipboardData = { getData: () => text }; - element.dispatchEvent(e); - await settled(); - return e; - } - componentTest("paste table", { template: hbs`{{d-editor value=value composerEvents=true}}`, beforeEach() { diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/host-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/host-list-test.js new file mode 100644 index 0000000000..dbb318176c --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/host-list-test.js @@ -0,0 +1,31 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { discourseModule, query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; + +discourseModule( + "Integration | Component | site-setting | host-list", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("displays setting value", { + template: hbs`{{site-setting setting=setting}}`, + + beforeEach() { + this.set("setting", { + setting: "blocked_onebox_domains", + value: "a.com|b.com", + type: "host_list", + }); + }, + + async test(assert) { + assert.strictEqual( + query(".formated-selection").innerText, + "a.com, b.com" + ); + }, + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js b/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js new file mode 100644 index 0000000000..443e886669 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/time-shortcut-picker-test.js @@ -0,0 +1,230 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { + discourseModule, + exists, + fakeTime, + query, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import I18n from "I18n"; +import hbs from "htmlbars-inline-precompile"; +import { click } from "@ember/test-helpers"; + +discourseModule( + "Integration | Component | time-shortcut-picker", + function (hooks) { + setupRenderingTest(hooks); + + const template = hbs`{{time-shortcut-picker _itsatrap=itsatrap}}`; + + hooks.beforeEach(function () { + const itsatrapStub = { + bind: () => {}, + unbind: () => {}, + }; + + this.set("itsatrap", itsatrapStub); + }); + + hooks.afterEach(function () { + if (this.clock) { + this.clock.restore(); + } + }); + + componentTest("shows default options", { + template, + + beforeEach() { + const tuesday = "2100-06-08T08:00:00"; + this.clock = fakeTime(tuesday, this.currentUser._timezone, true); + }, + + async test(assert) { + const expected = [ + I18n.t("time_shortcut.later_today"), + I18n.t("time_shortcut.tomorrow"), + I18n.t("time_shortcut.later_this_week"), + I18n.t("time_shortcut.this_weekend"), + I18n.t("time_shortcut.start_of_next_business_week"), + I18n.t("time_shortcut.next_month"), + I18n.t("time_shortcut.custom"), + I18n.t("time_shortcut.none"), + ]; + + const options = Array.from( + queryAll("div.tap-tile-grid div.tap-tile-title").map((_, div) => + div.innerText.trim() + ) + ); + + assert.deepEqual(options, expected); + }, + }); + + componentTest("show 'Later This Week' if today is < Thursday", { + template, + + beforeEach() { + const monday = "2100-06-07T08:00:00"; + this.clock = fakeTime(monday, this.currentUser._timezone, true); + }, + + test(assert) { + assert.ok( + exists("#tap_tile_later_this_week"), + "it has later this week" + ); + }, + }); + + componentTest("does not show 'Later This Week' if today is >= Thursday", { + template, + + beforeEach() { + const thursday = "2100-06-10T08:00:00"; + this.clock = fakeTime(thursday, this.currentUser._timezone, true); + }, + + test(assert) { + assert.notOk( + exists("#tap_tile_later_this_week"), + "it does not have later this week" + ); + }, + }); + + componentTest("does not show 'Later Today' if 'Later Today' is tomorrow", { + template, + + beforeEach() { + this.clock = fakeTime( + "2100-12-11T22:00:00", // + 3 hours is tomorrow + this.currentUser._timezone, + true + ); + }, + + test(assert) { + assert.notOk( + exists("#tap_tile_later_today"), + "it does not have later today" + ); + }, + }); + + componentTest("shows 'Later Today' if it is before 5pm", { + template, + + beforeEach() { + this.clock = fakeTime( + "2100-12-11T16:50:00", + this.currentUser._timezone, + true + ); + }, + + test(assert) { + assert.ok(exists("#tap_tile_later_today"), "it does have later today"); + }, + }); + + componentTest("does not show 'Later Today' if it is after 5pm", { + template, + + beforeEach() { + this.clock = fakeTime( + "2100-12-11T17:00:00", + this.currentUser._timezone, + true + ); + }, + + test(assert) { + assert.notOk( + exists("#tap_tile_later_today"), + "it does not have later today" + ); + }, + }); + + componentTest("defaults to 08:00 for custom time", { + template, + + async test(assert) { + await click("#tap_tile_custom"); + assert.strictEqual(query("#custom-time").value, "08:00"); + }, + }); + + componentTest("shows 'Next Monday' instead of 'Monday' on Sundays", { + template, + + beforeEach() { + const sunday = "2100-01-24T08:00:00"; + this.clock = fakeTime(sunday, this.currentUser._timezone, true); + }, + + async test(assert) { + assert.equal( + query("#tap_tile_start_of_next_business_week .tap-tile-title") + .innerText, + "Next Monday" + ); + + assert.equal( + query("div#tap_tile_start_of_next_business_week div.tap-tile-date") + .innerText, + "Feb 1, 8:00 am" + ); + }, + }); + + componentTest("shows 'Next Monday' instead of 'Monday' on Mondays", { + template, + + beforeEach() { + const monday = "2100-01-25T08:00:00"; + this.clock = fakeTime(monday, this.currentUser._timezone, true); + }, + + async test(assert) { + assert.equal( + query("#tap_tile_start_of_next_business_week .tap-tile-title") + .innerText, + "Next Monday" + ); + + assert.equal( + query("div#tap_tile_start_of_next_business_week div.tap-tile-date") + .innerText, + "Feb 1, 8:00 am" + ); + }, + }); + + componentTest( + "the 'Next Month' option points to the first day of the next month", + { + template, + + beforeEach() { + this.clock = fakeTime( + "2100-01-01T08:00:00", + this.currentUser._timezone, + true + ); + }, + + async test(assert) { + assert.strictEqual( + query("div#tap_tile_next_month div.tap-tile-date").innerText, + "Feb 1, 8:00 am" + ); + }, + } + ); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js index 46a8d1e207..6ad51806b6 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/value-list-test.js @@ -16,8 +16,6 @@ discourseModule("Integration | Component | value-list", function (hooks) { componentTest("adding a value", { template: hbs`{{value-list values=values}}`, - skip: true, - beforeEach() { this.set("values", "vinkas\nosama"); }, @@ -132,8 +130,6 @@ discourseModule("Integration | Component | value-list", function (hooks) { this.set("values", "vinkas|osama"); }, - skip: true, - async test(assert) { await selectKit().expand(); await selectKit().fillInFilter("eviltrout"); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js index 515dcd8561..9582b3a590 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/home-logo-test.js @@ -24,7 +24,6 @@ discourseModule( componentTest("basics", { template: hbs`{{mount-widget widget="home-logo" args=args}}`, - skip: true, beforeEach() { this.siteSettings.site_logo_url = bigLogo; this.siteSettings.site_logo_small_url = smallLogo; diff --git a/app/assets/javascripts/discourse/tests/setup-tests.js b/app/assets/javascripts/discourse/tests/setup-tests.js index 05702fe849..766b0bab4a 100644 --- a/app/assets/javascripts/discourse/tests/setup-tests.js +++ b/app/assets/javascripts/discourse/tests/setup-tests.js @@ -25,6 +25,7 @@ import QUnit from "qunit"; import { ScrollingDOMMethods } from "discourse/mixins/scrolling"; import Session from "discourse/models/session"; import User from "discourse/models/user"; +import Site from "discourse/models/site"; import bootbox from "bootbox"; import { buildResolver } from "discourse-common/resolver"; import { createHelperContext } from "discourse-common/lib/helpers"; @@ -269,11 +270,11 @@ function setupTestsCommon(application, container, config) { const cdn = setupData ? setupData.cdn : null; const baseUri = setupData ? setupData.baseUri : ""; - setupURL(cdn, "http://localhost:3000", baseUri); + setupURL(cdn, "http://localhost:3000", baseUri, { snapshot: true }); if (setupData && setupData.s3BaseUrl) { - setupS3CDN(setupData.s3BaseUrl, setupData.s3Cdn); + setupS3CDN(setupData.s3BaseUrl, setupData.s3Cdn, { snapshot: true }); } else { - setupS3CDN(null, null); + setupS3CDN(null, null, { snapshot: true }); } server = pretender; @@ -319,15 +320,28 @@ function setupTestsCommon(application, container, config) { session.highlightJsPath = setupData.highlightJsPath; } User.resetCurrent(); - let site = resetSite(settings); + createHelperContext({ - siteSettings: settings, + get siteSettings() { + if (isLegacyEmber() && container.isDestroyed) { + return settings; + } else { + return container.lookup("site-settings:main"); + } + }, capabilities: {}, - site, + get site() { + if (isLegacyEmber() && container.isDestroyed) { + return Site.current(); + } else { + return container.lookup("site:main") || Site.current(); + } + }, registry: app.__registry__, }); PreloadStore.reset(); + resetSite(settings); sinon.stub(ScrollingDOMMethods, "screenNotFull"); sinon.stub(ScrollingDOMMethods, "bindOnScroll"); @@ -342,8 +356,20 @@ function setupTestsCommon(application, container, config) { resetPretender(); clearPresenceState(); - // Destroy any modals - $(".modal-backdrop").remove(); + // Clean up the DOM. Some tests might leave extra classes or elements behind. + Array.from(document.getElementsByClassName("modal-backdrop")).forEach((e) => + e.remove() + ); + document.body.removeAttribute("class"); + let html = document.documentElement; + html.removeAttribute("class"); + html.removeAttribute("style"); + let testing = document.getElementById("ember-testing"); + testing.removeAttribute("class"); + testing.removeAttribute("style"); + let testContainer = document.getElementById("ember-testing-container"); + testContainer.scrollTop = 0; + flushMap(); MessageBus.unsubscribe("*"); diff --git a/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js index e58bc312aa..58d25ada54 100644 --- a/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js +++ b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js @@ -34,6 +34,13 @@ document.addEventListener("discourse-booted", () => { setup(QUnit.assert); setupTests(config.APP); let loader = loadEmberExam(); + + if (loader.urlParams.size === 0 && !QUnit.config.seed) { + // If we're running in browser, default to random order. Otherwise, let Ember Exam + // handle randomization. + QUnit.config.seed = true; + } + loader.loadModules(); start({ setupTestContainer: false, diff --git a/app/assets/javascripts/discourse/tests/unit/controllers/admin-user-badges-test.js b/app/assets/javascripts/discourse/tests/unit/controllers/admin-user-badges-test.js index 529cc34252..439079f0f5 100644 --- a/app/assets/javascripts/discourse/tests/unit/controllers/admin-user-badges-test.js +++ b/app/assets/javascripts/discourse/tests/unit/controllers/admin-user-badges-test.js @@ -51,7 +51,10 @@ discourseModule("Unit | Controller | admin-user-badges", function () { return badge.name; }); - assert.not(badgeNames.includes(badgeDisabled), "excludes disabled badges"); + assert.notOk( + badgeNames.includes(badgeDisabled), + "excludes disabled badges" + ); assert.deepEqual(badgeNames, sortedNames, "sorts badges by name"); }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/controllers/topic-test.js b/app/assets/javascripts/discourse/tests/unit/controllers/topic-test.js index bd8badbf58..5f770e2d5e 100644 --- a/app/assets/javascripts/discourse/tests/unit/controllers/topic-test.js +++ b/app/assets/javascripts/discourse/tests/unit/controllers/topic-test.js @@ -33,12 +33,15 @@ discourseModule("Unit | Controller | topic", function (hooks) { test("editTopic", function (assert) { const model = Topic.create(); const controller = this.getController("topic", { model }); - assert.not(controller.get("editingTopic"), "we are not editing by default"); + assert.notOk( + controller.get("editingTopic"), + "we are not editing by default" + ); controller.set("model.details.can_edit", false); controller.send("editTopic"); - assert.not( + assert.notOk( controller.get("editingTopic"), "calling editTopic doesn't enable editing unless the user can edit" ); @@ -58,7 +61,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { controller.send("cancelEditingTopic"); - assert.not( + assert.notOk( controller.get("editingTopic"), "cancelling edit mode reverts the property value" ); @@ -84,7 +87,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { model.set("views", 10000); controller.send("deleteTopic"); - assert.not(destroyed, "don't destroy popular topic"); + assert.notOk(destroyed, "don't destroy popular topic"); assert.ok(modalDisplayed, "display confirmation modal for popular topic"); model.set("views", 3); @@ -96,7 +99,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { const model = Topic.create(); const controller = this.getController("topic", { model }); - assert.not( + assert.notOk( controller.get("multiSelect"), "multi selection mode is disabled by default" ); @@ -123,7 +126,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { controller.send("toggleMultiSelect"); await settled(); - assert.not( + assert.notOk( controller.get("multiSelect"), "calling 'toggleMultiSelect' twice disables multi selection mode" ); @@ -145,7 +148,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { 2, "selectedPosts only contains already loaded posts" ); - assert.not( + assert.notOk( controller.get("selectedPosts").some((p) => p === undefined), "selectedPosts only contains valid post objects" ); @@ -157,7 +160,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { controller.set("selectedPostIds", [1, 2]); - assert.not( + assert.notOk( controller.get("selectedAllPosts"), "not all posts are selected" ); @@ -240,11 +243,14 @@ discourseModule("Unit | Controller | topic", function (hooks) { const model = Topic.create({ posts_count: 3 }); const controller = this.getController("topic", { model, site }); - assert.not(controller.get("showSelectedPostsAtBottom"), "false on desktop"); + assert.notOk( + controller.get("showSelectedPostsAtBottom"), + "false on desktop" + ); site.set("mobileView", true); - assert.not( + assert.notOk( controller.get("showSelectedPostsAtBottom"), "requires at least 3 posts on mobile" ); @@ -277,14 +283,14 @@ discourseModule("Unit | Controller | topic", function (hooks) { }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not( + assert.notOk( controller.get("canDeleteSelected"), "false when no posts are selected" ); selectedPostIds.pushObject(1); - assert.not( + assert.notOk( controller.get("canDeleteSelected"), "false when can't delete one of the selected posts" ); @@ -298,7 +304,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { selectedPostIds.pushObject(1); - assert.not( + assert.notOk( controller.get("canDeleteSelected"), "false when all posts are selected and user is staff" ); @@ -324,14 +330,14 @@ discourseModule("Unit | Controller | topic", function (hooks) { const controller = this.getController("topic", { model }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not( + assert.notOk( controller.get("canMergeTopic"), "can't merge topic when no posts are selected" ); selectedPostIds.pushObject(1); - assert.not( + assert.notOk( controller.get("canMergeTopic"), "can't merge topic when can't move posts" ); @@ -377,14 +383,14 @@ discourseModule("Unit | Controller | topic", function (hooks) { }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not( + assert.notOk( controller.get("canChangeOwner"), "false when no posts are selected" ); selectedPostIds.pushObject(1); - assert.not(controller.get("canChangeOwner"), "false when not admin"); + assert.notOk(controller.get("canChangeOwner"), "false when not admin"); currentUser.set("admin", true); @@ -395,7 +401,7 @@ discourseModule("Unit | Controller | topic", function (hooks) { selectedPostIds.pushObject(2); - assert.not( + assert.notOk( controller.get("canChangeOwner"), "false when admin but more than 1 user" ); @@ -416,28 +422,28 @@ discourseModule("Unit | Controller | topic", function (hooks) { }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not( + assert.notOk( controller.get("canMergePosts"), "false when no posts are selected" ); selectedPostIds.pushObject(1); - assert.not( + assert.notOk( controller.get("canMergePosts"), "false when only one post is selected" ); selectedPostIds.pushObject(2); - assert.not( + assert.notOk( controller.get("canMergePosts"), "false when selected posts are from different users" ); selectedPostIds.replace(1, 1, [3]); - assert.not( + assert.notOk( controller.get("canMergePosts"), "false when selected posts can't be deleted" ); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/allow-lister-test.js b/app/assets/javascripts/discourse/tests/unit/lib/allow-lister-test.js index 0a759c16bc..257af29f14 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/allow-lister-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/allow-lister-test.js @@ -21,6 +21,7 @@ module("Unit | Utility | allowLister", function () { "custom.foo", "custom.baz", "custom[data-*]", + "custom[data-custom-*=foo]", "custom[rel=nofollow]", ]); @@ -38,11 +39,12 @@ module("Unit | Utility | allowLister", function () { custom: { class: ["foo", "baz"], "data-*": ["*"], + "data-custom-*": ["foo"], rel: ["nofollow", "test"], }, }, }, - "Expecting a correct white list" + "Expecting a correct allow list" ); allowLister.disable("test"); @@ -53,7 +55,7 @@ module("Unit | Utility | allowLister", function () { tagList: {}, attrList: {}, }, - "Expecting an empty white list" + "Expecting an empty allow list" ); }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/click-track-edit-history-test.js b/app/assets/javascripts/discourse/tests/unit/lib/click-track-edit-history-test.js deleted file mode 100644 index ae13699e88..0000000000 --- a/app/assets/javascripts/discourse/tests/unit/lib/click-track-edit-history-test.js +++ /dev/null @@ -1,107 +0,0 @@ -import { fixture, logIn } from "discourse/tests/helpers/qunit-helpers"; -import { module, skip } from "qunit"; -import ClickTrack from "discourse/lib/click-track"; -import DiscourseURL from "discourse/lib/url"; -import User from "discourse/models/user"; -import pretender from "discourse/tests/helpers/create-pretender"; -import sinon from "sinon"; - -const track = ClickTrack.trackClick; - -function generateClickEventOn(selector) { - return $.Event("click", { currentTarget: fixture(selector) }); -} - -module("Unit | Utility | click-track-edit-history", function (hooks) { - hooks.beforeEach(function () { - logIn(); - - let win = { focus: function () {} }; - sinon.stub(window, "open").returns(win); - sinon.stub(win, "focus"); - - sinon.stub(DiscourseURL, "routeTo"); - sinon.stub(DiscourseURL, "redirectTo"); - - sessionStorage.clear(); - - fixture().innerHTML = `
-
- `; - }); - - skip("tracks internal URLs", async function (assert) { - assert.expect(2); - sinon.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - - const done = assert.async(); - pretender.post("/clicks/track", (request) => { - assert.strictEqual( - request.requestBody, - "url=http%3A%2F%2Fdiscuss.domain.com&post_id=42&topic_id=1337" - ); - done(); - }); - - assert.notOk(track(generateClickEventOn("#same-site"))); - }); - - skip("tracks external URLs", async function (assert) { - assert.expect(2); - - const done = assert.async(); - pretender.post("/clicks/track", (request) => { - assert.strictEqual( - request.requestBody, - "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" - ); - done(); - }); - - assert.notOk(track(generateClickEventOn("a"))); - }); - - skip("tracks external URLs when opening in another window", async function (assert) { - assert.expect(3); - User.currentProp("external_links_in_new_tab", true); - - const done = assert.async(); - pretender.post("/clicks/track", (request) => { - assert.strictEqual( - request.requestBody, - "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" - ); - done(); - }); - - assert.notOk(track(generateClickEventOn("a"))); - assert.ok(window.open.calledWith("http://www.google.com", "_blank")); - }); -}); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/click-track-profile-page-test.js b/app/assets/javascripts/discourse/tests/unit/lib/click-track-profile-page-test.js deleted file mode 100644 index 8373b51710..0000000000 --- a/app/assets/javascripts/discourse/tests/unit/lib/click-track-profile-page-test.js +++ /dev/null @@ -1,98 +0,0 @@ -import { fixture, logIn } from "discourse/tests/helpers/qunit-helpers"; -import { module, skip } from "qunit"; -import ClickTrack from "discourse/lib/click-track"; -import DiscourseURL from "discourse/lib/url"; -import pretender from "discourse/tests/helpers/create-pretender"; -import sinon from "sinon"; - -const track = ClickTrack.trackClick; - -function generateClickEventOn(selector) { - return $.Event("click", { currentTarget: fixture(selector) }); -} - -module("Unit | Utility | click-track-profile-page", function (hooks) { - hooks.beforeEach(function () { - logIn(); - - let win = { focus: function () {} }; - sinon.stub(window, "open").returns(win); - sinon.stub(win, "focus"); - - sinon.stub(DiscourseURL, "routeTo"); - sinon.stub(DiscourseURL, "redirectTo"); - - sessionStorage.clear(); - - fixture().innerHTML = `

- google.com - google.com -

- google.com - forum - log.txt - #hashtag -

-

- google.com - google.com -

- google.com - forum - log.txt - #hashtag -

`; - }); - - skip("tracks internal URLs", async function (assert) { - assert.expect(2); - sinon.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - - const done = assert.async(); - pretender.post("/clicks/track", (request) => { - assert.strictEqual( - request.requestBody, - "url=http%3A%2F%2Fdiscuss.domain.com" - ); - done(); - }); - - assert.notOk(track(generateClickEventOn("#same-site"))); - }); - - skip("tracks external URLs", async function (assert) { - assert.expect(2); - - const done = assert.async(); - pretender.post("/clicks/track", (request) => { - assert.strictEqual( - request.requestBody, - "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" - ); - done(); - }); - - assert.notOk(track(generateClickEventOn("a"))); - }); - - skip("tracks external URLs in other posts", async function (assert) { - assert.expect(2); - - const done = assert.async(); - pretender.post("/clicks/track", (request) => { - assert.strictEqual( - request.requestBody, - "url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331" - ); - done(); - }); - - assert.notOk(track(generateClickEventOn(".second a"))); - }); -}); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js b/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js index 77ce2d61d1..8be1a2a50a 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/emoji-test.js @@ -158,4 +158,11 @@ discourseModule("Unit | Utility | emoji", function () { assert.deepEqual(matches, ["bowing_man"]); }); + + test("search does partial-match on emoji aliases", function (assert) { + const matches = emojiSearch("instru"); + + assert.ok(matches.includes("woman_teacher")); + assert.ok(matches.includes("violin")); + }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/load-script-test.js b/app/assets/javascripts/discourse/tests/unit/lib/load-script-test.js index f977a03ad9..fe48fbc858 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/load-script-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/load-script-test.js @@ -1,23 +1,8 @@ -import { cacheBuster, loadScript } from "discourse/lib/load-script"; -import { module, skip, test } from "qunit"; +import { cacheBuster } from "discourse/lib/load-script"; +import { module, test } from "qunit"; import { PUBLIC_JS_VERSIONS as jsVersions } from "discourse/lib/public-js-versions"; module("Unit | Utility | load-script", function () { - skip("load with a script tag, and callbacks are only executed after script is loaded", async function (assert) { - assert.ok( - typeof window.ace === "undefined", - "ensures ace is not previously loaded" - ); - - const src = "/javascripts/ace/ace.js"; - - await loadScript(src); - assert.ok( - typeof window.ace !== "undefined", - "callbacks should only be executed after the script has fully loaded" - ); - }); - test("works when a value is not present", function (assert) { assert.strictEqual( cacheBuster("/javascripts/my-script.js"), diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js index 2f80646e34..827b01f5de 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js @@ -3,7 +3,7 @@ import { applyCachedInlineOnebox, deleteCachedInlineOnebox, } from "pretty-text/inline-oneboxer"; -import QUnit, { module, skip, test } from "qunit"; +import QUnit, { module, test } from "qunit"; import Post from "discourse/models/post"; import { buildQuote } from "discourse/lib/quote"; import { deepMerge } from "discourse-common/lib/object"; @@ -54,20 +54,6 @@ QUnit.assert.cookedPara = function (input, expected, message) { }; module("Unit | Utility | pretty-text", function () { - skip("Pending Engine fixes and spec fixes", function (assert) { - assert.cooked( - "Derpy: http://derp.com?_test_=1", - '

Derpy: http://derp.com?_test_=1

', - "works with underscores in urls" - ); - - assert.cooked( - "**a*_b**", - "

a*_b

", - "allows for characters within bold" - ); - }); - test("buildOptions", function (assert) { assert.ok( buildOptions({ siteSettings: { enable_emoji: true } }).discourse.features @@ -907,8 +893,8 @@ eviltrout

assert.cooked( "```eviltrout\nhello\n```", - '
hello\n
', - "it doesn't not allowlist all classes" + '
hello\n
', + "it converts to custom block unknown code names" ); assert.cooked( @@ -1520,7 +1506,7 @@ var bar = 'bar'; assert.cookedOptions( ":grin: @sam", { featuresOverride: ["emoji"] }, - `

:grin: @sam

`, + `

:grin: @sam

`, "cooks emojis when only the emoji markdown engine is enabled" ); @@ -1535,15 +1521,15 @@ var bar = 'bar'; test("emoji", function (assert) { assert.cooked( ":smile:", - `

:smile:

` + `

:smile:

` ); assert.cooked( ":(", - `

:frowning:

` + `

:frowning:

` ); assert.cooked( "8-)", - `

:sunglasses:

` + `

:sunglasses:

` ); }); @@ -1557,7 +1543,7 @@ var bar = 'bar'; assert.cookedOptions( "test:smile:test", { siteSettings: { enable_inline_emoji_translation: true } }, - `

test:smile:test

` + `

test:smile:test

` ); }); @@ -1565,7 +1551,7 @@ var bar = 'bar'; assert.cookedOptions( ":smile:", { siteSettings: { emoji_set: "twitter" } }, - `

:smile:

` + `

:smile:

` ); }); @@ -1578,7 +1564,7 @@ var bar = 'bar'; external_emoji_url: "https://emoji.hosting.service", }, }, - `

:smile:

` + `

:smile:

` ); }); @@ -1588,7 +1574,7 @@ var bar = 'bar'; assert.cookedOptions( ":foo:", {}, - `

:foo:

` + `

:foo:

` ); registerEmoji("bar", "/images/avatar.png", "baz"); @@ -1596,7 +1582,7 @@ var bar = 'bar'; assert.cookedOptions( ":bar:", {}, - `

:bar:

` + `

:bar:

` ); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js b/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js index 5dbca0b4f4..a1efa2b8bc 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js @@ -1,6 +1,7 @@ import PrettyText, { buildOptions } from "pretty-text/pretty-text"; import { module, test } from "qunit"; -import { hrefAllowed } from "pretty-text/sanitizer"; +import { hrefAllowed, sanitize } from "pretty-text/sanitizer"; +import AllowLister from "pretty-text/allow-lister"; module("Unit | Utility | sanitizer", function () { test("sanitize", function (assert) { @@ -250,4 +251,67 @@ module("Unit | Utility | sanitizer", function () { "escape single quotes" ); }); + + test("correctly sanitizes complex data attributes rules", function (assert) { + const allowLister = new AllowLister(); + + allowLister.allowListFeature("test", [ + "pre[data-*]", + "code[data-custom-*=foo]", + "div[data-cat-*]", + ]); + allowLister.enable("test"); + + assert.strictEqual(sanitize("", allowLister), ""); + assert.strictEqual(sanitize("", allowLister), ""); + assert.strictEqual(sanitize("", allowLister), ""); + assert.strictEqual(sanitize("", allowLister), ""); + assert.strictEqual(sanitize("", allowLister), ""); + assert.strictEqual(sanitize("", allowLister), ""); + + assert.strictEqual( + sanitize("
", allowLister),
+      '
'
+    );
+
+    assert.strictEqual(
+      sanitize("
", allowLister),
+      '
'
+    );
+
+    assert.strictEqual(
+      sanitize("", allowLister),
+      ""
+    );
+
+    assert.strictEqual(
+      sanitize("", allowLister),
+      ""
+    );
+
+    assert.strictEqual(
+      sanitize("", allowLister),
+      ""
+    );
+
+    assert.strictEqual(
+      sanitize("", allowLister),
+      ''
+    );
+
+    assert.strictEqual(
+      sanitize("", allowLister),
+      ""
+    );
+
+    assert.strictEqual(
+      sanitize("
", allowLister), + '
' + ); + + assert.strictEqual( + sanitize("
", allowLister), + '
' + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js b/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js index 2d429fb563..09dcecae46 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js @@ -4,7 +4,6 @@ import buildTimeframes from "discourse/lib/timeframes-builder"; const DEFAULT_OPTIONS = { includeWeekend: null, - includeMidFuture: true, includeFarFuture: null, includeDateTime: null, canScheduleNow: false, @@ -109,7 +108,7 @@ module("Unit | Lib | timeframes-builder", function (hooks) { this.clock = fakeTime("2100-04-19 18:00:00", timezone, true); // Monday evening const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); - assert.not(timeframes.includes("later_today")); + assert.notOk(timeframes.includes("later_today")); assert.ok(timeframes.includes("later_this_week")); }); @@ -118,7 +117,7 @@ module("Unit | Lib | timeframes-builder", function (hooks) { this.clock = fakeTime("2100-04-22 18:00:00", timezone, true); // Tuesday evening const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); - assert.not(timeframes.includes("later_this_week")); + assert.notOk(timeframes.includes("later_this_week")); }); test("doesn't output 'Later This Week' on Sundays", function (assert) { @@ -139,7 +138,7 @@ module("Unit | Lib | timeframes-builder", function (hooks) { this.clock = fakeTime("2100-04-25 18:00:00", timezone, true); // Sunday evening const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); - assert.not(timeframes.includes("later_this_week")); + assert.notOk(timeframes.includes("later_this_week")); }); test("doesn't output 'Next Month' on the last day of the month", function (assert) { @@ -147,6 +146,6 @@ module("Unit | Lib | timeframes-builder", function (hooks) { this.clock = fakeTime("2100-04-30 18:00:00", timezone, true); // The last day of April const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); - assert.not(timeframes.includes("next_month")); + assert.notOk(timeframes.includes("next_month")); }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js b/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js index 68d3f4de72..e2ca35c97a 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js @@ -17,15 +17,15 @@ import { test } from "qunit"; discourseModule("Unit | Utility | uploads", function () { test("validateUploadedFiles", function (assert) { - assert.not( + assert.notOk( validateUploadedFiles(null, { siteSettings: this.siteSettings }), "no files are invalid" ); - assert.not( + assert.notOk( validateUploadedFiles(undefined, { siteSettings: this.siteSettings }), "undefined files are invalid" ); - assert.not( + assert.notOk( validateUploadedFiles([], { siteSettings: this.siteSettings }), "empty array of files is invalid" ); @@ -34,7 +34,7 @@ discourseModule("Unit | Utility | uploads", function () { test("uploading one file", function (assert) { sinon.stub(bootbox, "alert"); - assert.not( + assert.notOk( validateUploadedFiles([1, 2], { siteSettings: this.siteSettings }) ); assert.ok(bootbox.alert.calledWith(I18n.t("post.errors.too_many_uploads"))); @@ -44,7 +44,7 @@ discourseModule("Unit | Utility | uploads", function () { this.siteSettings.newuser_max_embedded_media = 0; sinon.stub(bootbox, "alert"); - assert.not( + assert.notOk( validateUploadedFiles([{ name: "image.png" }], { user: User.create(), siteSettings: this.siteSettings, @@ -88,7 +88,7 @@ discourseModule("Unit | Utility | uploads", function () { this.siteSettings.newuser_max_attachments = 0; sinon.stub(bootbox, "alert"); - assert.not( + assert.notOk( validateUploadedFiles([{ name: "roman.txt" }], { user: User.create(), siteSettings: this.siteSettings, @@ -103,7 +103,7 @@ discourseModule("Unit | Utility | uploads", function () { test("ensures an authorized upload", function (assert) { sinon.stub(bootbox, "alert"); - assert.not( + assert.notOk( validateUploadedFiles([{ name: "unauthorized.html" }], { siteSettings: this.siteSettings, }) @@ -124,7 +124,7 @@ discourseModule("Unit | Utility | uploads", function () { const files = [{ name: "backup.tar.gz" }]; sinon.stub(bootbox, "alert"); - assert.not( + assert.notOk( validateUploadedFiles(files, { skipValidation: false, siteSettings: this.siteSettings, @@ -144,7 +144,7 @@ discourseModule("Unit | Utility | uploads", function () { sinon.stub(bootbox, "alert"); let user = User.create({ moderator: true }); - assert.not( + assert.notOk( validateUploadedFiles(files, { user, siteSettings: this.siteSettings }) ); assert.ok( @@ -193,7 +193,7 @@ discourseModule("Unit | Utility | uploads", function () { }) ); - assert.not(bootbox.alert.calledOnce); + assert.notOk(bootbox.alert.calledOnce); }); test("isImage", function (assert) { @@ -205,9 +205,9 @@ discourseModule("Unit | Utility | uploads", function () { image + " is recognized as an image" ); }); - assert.not(isImage("file.txt")); - assert.not(isImage("http://foo.bar/path/to/file.txt")); - assert.not(isImage("")); + assert.notOk(isImage("file.txt")); + assert.notOk(isImage("http://foo.bar/path/to/file.txt")); + assert.notOk(isImage("")); }); test("allowsImages", function (assert) { @@ -235,7 +235,7 @@ discourseModule("Unit | Utility | uploads", function () { test("allowsAttachments", function (assert) { this.siteSettings.authorized_extensions = "jpg|jpeg|gif"; - assert.not( + assert.notOk( allowsAttachments(false, this.siteSettings), "no attachments allowed by default" ); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/url-test.js b/app/assets/javascripts/discourse/tests/unit/lib/url-test.js index 61ee568ec0..e70e65b42c 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/url-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/url-test.js @@ -13,7 +13,7 @@ module("Unit | Utility | url", function () { test("isInternal with a HTTP url", function (assert) { sinon.stub(DiscourseURL, "origin").returns("http://eviltrout.com"); - assert.not(DiscourseURL.isInternal(null), "a blank URL is not internal"); + assert.notOk(DiscourseURL.isInternal(null), "a blank URL is not internal"); assert.ok(DiscourseURL.isInternal("/test"), "relative URLs are internal"); assert.ok( DiscourseURL.isInternal("//eviltrout.com"), @@ -27,11 +27,11 @@ module("Unit | Utility | url", function () { DiscourseURL.isInternal("https://eviltrout.com/moustache"), "a url on a HTTPS of the same host is internal" ); - assert.not( + assert.notOk( DiscourseURL.isInternal("//twitter.com.com"), "a different host is not internal (protocol-less)" ); - assert.not( + assert.notOk( DiscourseURL.isInternal("http://twitter.com"), "a different host is not internal" ); @@ -47,11 +47,11 @@ module("Unit | Utility | url", function () { test("isInternal on subfolder install", function (assert) { sinon.stub(DiscourseURL, "origin").returns("http://eviltrout.com/forum"); - assert.not( + assert.notOk( DiscourseURL.isInternal("http://eviltrout.com"), "the host root is not internal" ); - assert.not( + assert.notOk( DiscourseURL.isInternal("http://eviltrout.com/tophat"), "a url on the same host but on a different folder is not internal" ); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js index edbc2ddab2..ddca5c5578 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js @@ -15,7 +15,7 @@ import { slugify, toAsciiPrintable, } from "discourse/lib/utilities"; -import { skip, test } from "qunit"; +import { test } from "qunit"; import Handlebars from "handlebars"; import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; @@ -282,21 +282,4 @@ discourseModule("Unit | Utilities", function () { } }); }); - - skip("inCodeBlock - runs fast", function (assert) { - const phrase = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - const text = `${phrase}\n\n\`\`\`\n${phrase}\n\`\`\`\n\n${phrase}\n\n\`${phrase}\n${phrase}\n\n${phrase}\n\n[code]\n${phrase}\n[/code]\n\n${phrase}\n\n ${phrase}\n\n\`${phrase}\`\n\n${phrase}`; - - let time = Number.MAX_VALUE; - for (let i = 0; i < 10; ++i) { - const start = performance.now(); - inCodeBlock(text, text.length); - const end = performance.now(); - time = Math.min(time, end - start); - } - - // This runs in 'keyUp' event handler so it should run as fast as - // possible. It should take less than 1ms for the test text. - assert.ok(time < 10); - }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/mixins/grant-badge-controller-test.js b/app/assets/javascripts/discourse/tests/unit/mixins/grant-badge-controller-test.js index 8b939bd9de..cb56f1812e 100644 --- a/app/assets/javascripts/discourse/tests/unit/mixins/grant-badge-controller-test.js +++ b/app/assets/javascripts/discourse/tests/unit/mixins/grant-badge-controller-test.js @@ -60,11 +60,11 @@ module("Unit | Mixin | grant-badge-controller", function (hooks) { .get("grantableBadges") .map((badge) => badge.name); - assert.not( + assert.notOk( badgeNames.includes(this.badgeDisabled), "excludes disabled badges" ); - assert.not( + assert.notOk( badgeNames.includes(this.badgeAutomatic), "excludes automatic badges" ); @@ -73,7 +73,7 @@ module("Unit | Mixin | grant-badge-controller", function (hooks) { test("selectedBadgeGrantable", function (assert) { this.subject.set("selectedBadgeId", this.badgeDisabled.id); - assert.not(this.subject.get("selectedBadgeGrantable")); + assert.notOk(this.subject.get("selectedBadgeGrantable")); this.subject.set("selectedBadgeId", this.badgeFirst.id); assert.ok(this.subject.get("selectedBadgeGrantable")); diff --git a/app/assets/javascripts/discourse/tests/unit/models/category-test.js b/app/assets/javascripts/discourse/tests/unit/models/category-test.js index 18c2f7cee5..27c4193c80 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/category-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/category-test.js @@ -276,9 +276,17 @@ module("Unit | Model | category", function () { id: 2, name: "middle term", slug: "another-different-slug", + }), + subcategory = store.createRecord("category", { + id: 3, + name: "middle term", + slug: "another-different-slug2", + parent_category_id: 2, }); - sinon.stub(Category, "listByActivity").returns([category1, category2]); + sinon + .stub(Category, "listByActivity") + .returns([category1, category2, subcategory]); assert.deepEqual( Category.search("term", { limit: 0 }), @@ -292,24 +300,30 @@ module("Unit | Model | category", function () { ); assert.deepEqual( Category.search("term"), - [category1, category2], + [category1, category2, subcategory], "orders by activity" ); category2.set("name", "TeRm start"); assert.deepEqual( Category.search("tErM"), - [category2, category1], + [category2, category1, subcategory], "ignores case of category name and search term" ); category2.set("name", "term start"); assert.deepEqual( Category.search("term"), - [category2, category1], + [category2, category1, subcategory], "orders matching begin with and then contains" ); + assert.deepEqual( + Category.search("term", { parentCategoryId: 2 }), + [subcategory], + "search only subcategories belonging to specific parent category" + ); + sinon.restore(); const child_category1 = store.createRecord("category", { diff --git a/app/assets/javascripts/discourse/tests/unit/models/report-test.js b/app/assets/javascripts/discourse/tests/unit/models/report-test.js index 3cb76c9a0d..c07323f142 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/report-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/report-test.js @@ -109,12 +109,19 @@ module("Unit | Model | report", function () { }); test("thirtyDaysCountTitle", function (assert) { - const report = reportWithData([5, 5, 5, 5]); + let report = reportWithData([5, 5, 5, 5]); report.set("prev30Days", 10); - const title = report.get("thirtyDaysCountTitle"); + let title = report.get("thirtyDaysCountTitle"); assert.ok(title.indexOf("+50%") !== -1); assert.ok(title.match(/Was 10/)); + + report = reportWithData([5, 5, 5, 5]); + report.set("prev_period", 20); + title = report.get("thirtyDaysCountTitle"); + + assert.ok(title.indexOf("-25%") !== -1); + assert.ok(title.match(/Was 20/)); }); test("sevenDaysTrend", function (assert) { diff --git a/app/assets/javascripts/discourse/tests/unit/models/topic-test.js b/app/assets/javascripts/discourse/tests/unit/models/topic-test.js index 84e9a3f96c..5b5446eb3c 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/topic-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/topic-test.js @@ -20,7 +20,7 @@ discourseModule("Unit | Model | topic", function () { last_read_post_number: 1, }); - assert.not( + assert.notOk( topic.get("visited"), "not visited unless we've read all the posts" ); diff --git a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js index 8b02e16689..380e326abe 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js @@ -519,6 +519,28 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) { ); }); + test("correct tag and category filters for different lists", function (assert) { + trackingState.trackIncoming("unread"); + assert.strictEqual(trackingState.filterCategory, undefined); + assert.strictEqual(trackingState.filterTag, undefined); + assert.strictEqual(trackingState.filter, "unread"); + + trackingState.trackIncoming("tag/test/l/latest"); + assert.strictEqual(trackingState.filterCategory, undefined); + assert.strictEqual(trackingState.filterTag, "test"); + assert.strictEqual(trackingState.filter, "latest"); + + trackingState.trackIncoming("c/cat/subcat/6/l/latest"); + assert.strictEqual(trackingState.filterCategory.id, 6); + assert.strictEqual(trackingState.filterTag, undefined); + assert.strictEqual(trackingState.filter, "latest"); + + trackingState.trackIncoming("tags/c/cat/subcat/6/test/l/latest"); + assert.strictEqual(trackingState.filterCategory.id, 6); + assert.strictEqual(trackingState.filterTag, "test"); + assert.strictEqual(trackingState.filter, "latest"); + }); + test("adds incoming in the categories latest topics list", function (assert) { trackingState.trackIncoming("categories"); const unreadCategoriesLatestTopicsPayload = { diff --git a/app/assets/javascripts/discourse/tests/unit/models/user-test.js b/app/assets/javascripts/discourse/tests/unit/models/user-test.js index fe80430aa2..751ba85733 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/user-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/user-test.js @@ -1,8 +1,8 @@ import * as ajaxlib from "discourse/lib/ajax"; import { module, test } from "qunit"; +import sinon from "sinon"; import Group from "discourse/models/group"; import User from "discourse/models/user"; -import sinon from "sinon"; module("Unit | Model | user", function () { test("staff", function (assert) { @@ -108,7 +108,7 @@ module("Unit | Model | user", function () { undefined, "if the user has no timezone and the user is not the current user, do NOT guess with moment" ); - assert.not( + assert.notOk( spy.calledWith("/u/howardhamlin.json", { type: "PUT", dataType: "json", diff --git a/app/assets/javascripts/discourse/tests/unit/services/screen-track-test.js b/app/assets/javascripts/discourse/tests/unit/services/screen-track-test.js index 7eeceb4b26..8a73f75066 100644 --- a/app/assets/javascripts/discourse/tests/unit/services/screen-track-test.js +++ b/app/assets/javascripts/discourse/tests/unit/services/screen-track-test.js @@ -7,15 +7,22 @@ discourseModule("Unit | Service | screen-track", function () { tracker.consolidateTimings({ 1: 10, 2: 5 }, 10, 1); tracker.consolidateTimings({ 1: 5, 3: 1 }, 3, 1); - const consolidated = tracker.consolidateTimings({ 1: 5, 3: 1 }, 3, 2); + const consolidated = tracker.consolidateTimings({ 1: 5, 3: 1, 4: 5 }, 3, 2); assert.deepEqual( consolidated, [ { timings: { 1: 15, 2: 5, 3: 1 }, topicTime: 13, topicId: 1 }, - { timings: { 1: 5, 3: 1 }, topicTime: 3, topicId: 2 }, + { timings: { 1: 5, 3: 1, 4: 5 }, topicTime: 3, topicId: 2 }, ], "expecting consolidated timings to match correctly" ); + + tracker.sendNextConsolidatedTiming(); + assert.equal( + tracker.highestReadFromCache(2), + 4, + "caches highest read post number for second topic" + ); }); }); diff --git a/app/assets/javascripts/pretty-text/addon/emoji.js b/app/assets/javascripts/pretty-text/addon/emoji.js index 9d79a6ce61..16d4b31aea 100644 --- a/app/assets/javascripts/pretty-text/addon/emoji.js +++ b/app/assets/javascripts/pretty-text/addon/emoji.js @@ -222,9 +222,11 @@ export function emojiSearch(term, options) { } } - if (searchAliases[term]) { - for (const emoji of searchAliases[term]) { - addResult(emoji); + for (const [key, value] of Object.entries(searchAliases)) { + if (key.startsWith(term)) { + for (const emoji of value) { + addResult(emoji); + } } } diff --git a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js index 5d9ca251bd..558f66cdf0 100644 --- a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js +++ b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js @@ -1,4 +1,5 @@ import AllowLister from "pretty-text/allow-lister"; +import { cloneJSON } from "discourse-common/lib/object"; import deprecated from "discourse-common/lib/deprecated"; import guid from "pretty-text/guid"; import { sanitize } from "pretty-text/sanitizer"; @@ -18,11 +19,18 @@ function deprecate(feature, name) { }; } +// We have some custom extensions and extension points for markdown-it, including +// the helper (passed in via setup) that has registerOptions, registerPlugin etc. +// as well as our postProcessText rule to replace text with a regex. +// +// Take a look at https://meta.discourse.org/t/developers-guide-to-markdown-extensions/66023 +// for more detailed information. function createHelper( featureName, opts, optionCallbacks, pluginCallbacks, + customMarkdownCookFnCallbacks, getOptions, allowListed ) { @@ -58,6 +66,10 @@ function createHelper( pluginCallbacks.push([featureName, callback]); }; + helper.buildCookFunction = (callback) => { + customMarkdownCookFnCallbacks.push([featureName, callback]); + }; + return helper; } @@ -101,10 +113,12 @@ function setupBlockBBCode(md) { md.block.bbcode = { ruler: new Ruler() }; } +// inline bbcode ruler for parsing of spoiler tags, discourse-chart etc function setupInlineBBCode(md) { md.inline.bbcode = { ruler: new Ruler() }; } +// rule for text replacement via regex, used for @mentions, category hashtags, etc. function setupTextPostProcessRuler(md) { const TextPostProcessRuler = requirejs( "pretty-text/engines/discourse-markdown/text-post-process" @@ -112,11 +126,14 @@ function setupTextPostProcessRuler(md) { md.core.textPostProcess = { ruler: new TextPostProcessRuler() }; } +// hoists html_raw tokens out of the render flow and replaces them +// with a GUID. this GUID is then replaced with the final raw HTML +// content in unhoistForCooked function renderHoisted(tokens, idx, options) { const content = tokens[idx].content; if (content && content.length > 0) { let id = guid(); - options.discourse.hoisted[id] = tokens[idx].content; + options.discourse.hoisted[id] = content; return id; } else { return ""; @@ -129,25 +146,12 @@ function setupUrlDecoding(md) { md.utils.lib.mdurl.decode.defaultChars = ";/?:@&=+$,# "; } +// html_raw tokens, funnily enough, render raw HTML via renderHoisted and +// unhoistForCooked function setupHoister(md) { md.renderer.rules.html_raw = renderHoisted; } -export function extractDataAttribute(str) { - let sep = str.indexOf("="); - if (sep === -1) { - return null; - } - - const key = `data-${str.substr(0, sep)}`.toLowerCase(); - if (!/^[A-Za-z]+[\w\-\:\.]*$/.test(key)) { - return null; - } - - const value = str.substr(sep + 1); - return [key, value]; -} - // videoHTML and audioHTML follow the same HTML syntax // as oneboxer.rb when dealing with these formats function videoHTML(token) { @@ -287,6 +291,152 @@ function setupAttachments(md) { md.renderer.rules.link_open = renderAttachment; } +function buildCustomMarkdownCookFunction(engineOpts, defaultEngineOpts) { + // everything except the engine for opts can just point to the other + // opts references, they do not change and we don't need to worry about + // mutating them. note that this may need to be updated when additional + // opts are added to the pipeline + const newOpts = {}; + newOpts.allowListed = defaultEngineOpts.allowListed; + newOpts.pluginCallbacks = defaultEngineOpts.pluginCallbacks; + newOpts.sanitizer = defaultEngineOpts.sanitizer; + newOpts.discourse = {}; + const featureConfig = cloneJSON(defaultEngineOpts.discourse.features); + + // everything from the discourse part of defaultEngineOpts can be cloned except + // the features, because these can be a limited subset and we don't want to + // change the original object reference + for (const [key, value] of Object.entries(defaultEngineOpts.discourse)) { + if (key !== "features") { + newOpts.discourse[key] = value; + } + } + + if (engineOpts.featuresOverride !== undefined) { + overrideMarkdownFeatures(featureConfig, engineOpts.featuresOverride); + } + newOpts.discourse.features = featureConfig; + + const markdownitOpts = { + discourse: newOpts.discourse, + html: defaultEngineOpts.engine.options.html, + breaks: defaultEngineOpts.engine.options.breaks, + xhtmlOut: defaultEngineOpts.engine.options.xhtmlOut, + linkify: defaultEngineOpts.engine.options.linkify, + typographer: defaultEngineOpts.engine.options.typographer, + }; + newOpts.engine = createMarkdownItEngineWithOpts( + markdownitOpts, + engineOpts.markdownItRules + ); + + // we have to do this to make sure plugin callbacks, allow list, and helper + // functions are all set up correctly for the new engine + setupMarkdownEngine(newOpts, featureConfig); + + // we don't need the whole engine as a consumer, just a cook function + // will do + return function customRenderFn(contentToRender) { + return newOpts.discourse + .sanitizer(newOpts.engine.render(contentToRender)) + .trim(); + }; +} + +function createMarkdownItEngineWithOpts(markdownitOpts, ruleOverrides) { + if (ruleOverrides !== undefined) { + // Preset for "zero", https://github.com/markdown-it/markdown-it/blob/master/lib/presets/zero.js + return window.markdownit("zero", markdownitOpts).enable(ruleOverrides); + } + return window.markdownit(markdownitOpts); +} + +function overrideMarkdownFeatures(features, featureOverrides) { + if (featureOverrides !== undefined) { + Object.keys(features).forEach((feature) => { + features[feature] = featureOverrides.includes(feature); + }); + } +} + +function setupMarkdownEngine(opts, featureConfig) { + const quotation_marks = + opts.discourse.limitedSiteSettings.markdownTypographerQuotationMarks; + if (quotation_marks) { + opts.engine.options.quotes = quotation_marks.split("|"); + } + + opts.engine.linkify.tlds( + (opts.discourse.limitedSiteSettings.markdownLinkifyTlds || "").split("|") + ); + + setupUrlDecoding(opts.engine); + setupHoister(opts.engine); + setupImageAndPlayableMediaRenderer(opts.engine); + setupAttachments(opts.engine); + setupBlockBBCode(opts.engine); + setupInlineBBCode(opts.engine); + setupTextPostProcessRuler(opts.engine); + + opts.pluginCallbacks.forEach(([feature, callback]) => { + if (featureConfig[feature]) { + opts.engine.use(callback); + } + }); + + // top level markdown it notifier + opts.markdownIt = true; + opts.setup = true; + + if (!opts.discourse.sanitizer || !opts.sanitizer) { + const allowLister = new AllowLister(opts.discourse); + + opts.allowListed.forEach(([feature, info]) => { + allowLister.allowListFeature(feature, info); + }); + + opts.sanitizer = opts.discourse.sanitizer = !!opts.discourse.sanitize + ? (a) => sanitize(a, allowLister) + : (a) => a; + } +} + +function unhoistForCooked(hoisted, cooked) { + const keys = Object.keys(hoisted); + if (keys.length) { + let found = true; + + const unhoist = function (key) { + cooked = cooked.replace(new RegExp(key, "g"), function () { + found = true; + return hoisted[key]; + }); + }; + + while (found) { + found = false; + keys.forEach(unhoist); + } + } + + return cooked; +} + +export function extractDataAttribute(str) { + let sep = str.indexOf("="); + if (sep === -1) { + return null; + } + + const key = `data-${str.substr(0, sep)}`.toLowerCase(); + if (!/^[A-Za-z]+[\w\-\:\.]*$/.test(key)) { + return null; + } + + const value = str.substr(sep + 1); + return [key, value]; +} + let Helpers; export function setup(opts, siteSettings, state) { @@ -302,6 +452,7 @@ export function setup(opts, siteSettings, state) { let optionCallbacks = []; let pluginCallbacks = []; + let customMarkdownCookFnCallbacks = []; // ideally I would like to change the top level API a bit, but in the mean time this will do let getOptions = { @@ -312,6 +463,9 @@ export function setup(opts, siteSettings, state) { let features = []; let allowListed = []; + // all of the modules under discourse-markdown or markdown-it + // directories are considered additional markdown "features" which + // may define their own rules Object.keys(require._eak_seen).forEach((entry) => { if (check.test(entry)) { const module = requirejs(entry); @@ -325,13 +479,14 @@ export function setup(opts, siteSettings, state) { features .sort((a, b) => a.priority - b.priority) - .forEach((f) => { - f.setup( + .forEach((markdownFeature) => { + markdownFeature.setup( createHelper( - f.id, + markdownFeature.id, opts, optionCallbacks, pluginCallbacks, + customMarkdownCookFnCallbacks, getOptions, allowListed ) @@ -353,10 +508,8 @@ export function setup(opts, siteSettings, state) { } }); - if (opts.featuresOverride) { - Object.keys(opts.features).forEach((feature) => { - opts.features[feature] = opts.featuresOverride.includes(feature); - }); + if (opts.featuresOverride !== undefined) { + overrideMarkdownFeatures(opts.features, opts.featuresOverride); } let copy = {}; @@ -375,92 +528,50 @@ export function setup(opts, siteSettings, state) { opts.discourse.limitedSiteSettings = { secureMedia: siteSettings.secure_media, enableDiffhtmlPreview: siteSettings.enable_diffhtml_preview, + traditionalMarkdownLinebreaks: siteSettings.traditional_markdown_linebreaks, + enableMarkdownLinkify: siteSettings.enable_markdown_linkify, + enableMarkdownTypographer: siteSettings.enable_markdown_typographer, + markdownTypographerQuotationMarks: + siteSettings.markdown_typographer_quotation_marks, + markdownLinkifyTlds: siteSettings.markdown_linkify_tlds, }; const markdownitOpts = { discourse: opts.discourse, html: true, - breaks: !siteSettings.traditional_markdown_linebreaks, + breaks: !opts.discourse.limitedSiteSettings.traditionalMarkdownLinebreaks, xhtmlOut: false, - linkify: siteSettings.enable_markdown_linkify, - typographer: siteSettings.enable_markdown_typographer, + linkify: opts.discourse.limitedSiteSettings.enableMarkdownLinkify, + typographer: opts.discourse.limitedSiteSettings.enableMarkdownTypographer, }; - if (opts.discourse.markdownItRules !== undefined) { - opts.engine = window - .markdownit("zero", markdownitOpts) // Preset for "zero", https://github.com/markdown-it/markdown-it/blob/master/lib/presets/zero.js - .enable(opts.discourse.markdownItRules); - } else { - opts.engine = window.markdownit(markdownitOpts); - } - - const quotation_marks = siteSettings.markdown_typographer_quotation_marks; - if (quotation_marks) { - opts.engine.options.quotes = quotation_marks.split("|"); - } - - opts.engine.linkify.tlds( - (siteSettings.markdown_linkify_tlds || "").split("|") + opts.engine = createMarkdownItEngineWithOpts( + markdownitOpts, + opts.discourse.markdownItRules ); - setupUrlDecoding(opts.engine); - setupHoister(opts.engine); - setupImageAndPlayableMediaRenderer(opts.engine); - setupAttachments(opts.engine); - setupBlockBBCode(opts.engine); - setupInlineBBCode(opts.engine); - setupTextPostProcessRuler(opts.engine); + opts.pluginCallbacks = pluginCallbacks; + opts.allowListed = allowListed; - pluginCallbacks.forEach(([feature, callback]) => { - if (opts.discourse.features[feature]) { - opts.engine.use(callback); - } + setupMarkdownEngine(opts, opts.discourse.features); + + customMarkdownCookFnCallbacks.forEach(([, callback]) => { + callback(opts, (engineOpts, afterBuild) => + afterBuild(buildCustomMarkdownCookFunction(engineOpts, opts)) + ); }); - - // top level markdown it notifier - opts.markdownIt = true; - opts.setup = true; - - if (!opts.discourse.sanitizer || !opts.sanitizer) { - const allowLister = new AllowLister(opts.discourse); - - allowListed.forEach(([feature, info]) => { - allowLister.allowListFeature(feature, info); - }); - - opts.sanitizer = opts.discourse.sanitizer = !!opts.discourse.sanitize - ? (a) => sanitize(a, allowLister) - : (a) => a; - } } export function cook(raw, opts) { // we still have to hoist html_raw nodes so they bypass the allowlister // this is the case for oneboxes let hoisted = {}; - opts.discourse.hoisted = hoisted; const rendered = opts.engine.render(raw); let cooked = opts.discourse.sanitizer(rendered).trim(); - - const keys = Object.keys(hoisted); - if (keys.length) { - let found = true; - - const unhoist = function (key) { - cooked = cooked.replace(new RegExp(key, "g"), function () { - found = true; - return hoisted[key]; - }); - }; - - while (found) { - found = false; - keys.forEach(unhoist); - } - } - + cooked = unhoistForCooked(hoisted, cooked); delete opts.discourse.hoisted; + return cooked; } diff --git a/app/assets/javascripts/pretty-text/addon/pretty-text.js b/app/assets/javascripts/pretty-text/addon/pretty-text.js index 6654f9cc24..80138a7ab3 100644 --- a/app/assets/javascripts/pretty-text/addon/pretty-text.js +++ b/app/assets/javascripts/pretty-text/addon/pretty-text.js @@ -40,6 +40,7 @@ export function buildOptions(state) { watchedWordsLink, featuresOverride, markdownItRules, + additionalOptions, } = state; let features = {}; @@ -80,6 +81,7 @@ export function buildOptions(state) { watchedWordsLink, featuresOverride, markdownItRules, + additionalOptions, }; // note, this will mutate options due to the way the API is designed diff --git a/app/assets/javascripts/pretty-text/addon/sanitizer.js b/app/assets/javascripts/pretty-text/addon/sanitizer.js index 1b61c6c243..19017184a9 100644 --- a/app/assets/javascripts/pretty-text/addon/sanitizer.js +++ b/app/assets/javascripts/pretty-text/addon/sanitizer.js @@ -41,6 +41,18 @@ export function hrefAllowed(href, extraHrefMatchers) { } } +function testDataAttribute(forTag, name, value) { + return Object.keys(forTag).find((k) => { + const nameWithMatcher = `^${k.replace(/\*$/, "\\w+?")}`; + const validValues = forTag[k]; + + return ( + new RegExp(nameWithMatcher).test(name) && + (validValues.includes("*") ? true : validValues.includes(value)) + ); + }); +} + export function sanitize(text, allowLister) { if (!text) { return ""; @@ -72,12 +84,13 @@ export function sanitize(text, allowLister) { const forTag = allowList.attrList[tag]; if (forTag) { const forAttr = forTag[name]; + if ( (forAttr && (forAttr.indexOf("*") !== -1 || forAttr.indexOf(value) !== -1)) || (name.indexOf("data-html-") === -1 && name.indexOf("data-") === 0 && - forTag["data-*"]) || + (forTag["data-*"] || testDataAttribute(forTag, name, value))) || (tag === "a" && name === "href" && hrefAllowed(value, extraHrefMatchers)) || diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js index 1fdab1bf29..d73516677f 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js @@ -1,35 +1,78 @@ // 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 extractTokenInfo(info, md) { + if (!info) { + return; + } + + info = info.trim(); + + const matches = info.match(/(^\s*\S*)\s*(.*)/i); + if (!matches) { + return; + } + + // ensure the token has only valid chars + // c++, strucuted-text and p91, are all valid + if (!/^[\w+-]*$/i.test(matches[1])) { + return; + } + + const ASCII_REGEX = /[^\x00-\x7F]/; + const tag = md.utils.unescapeAll(matches[1].replace(ASCII_REGEX, "")); + const extractedData = { tag, attributes: {} }; + + if (matches[2]?.length) { + md.utils + .unescapeAll(matches[2].replace(ASCII_REGEX, "")) + .split(",") + .forEach((potentialPair) => { + const [key, value] = potentialPair.trim().split(/\s+/g)[0].split("="); + + // invalid pairs would get caught here and not used, eg `foo=` + if (key && value) { + extractedData.attributes[key] = value; + } + }); + } + + return extractedData; +} + 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); + const token = tokens[idx]; + const escapedContent = md.utils.escapeHtml(token.content); + const tokenInfo = extractTokenInfo(token.info, md); + const tag = tokenInfo?.tag || md.options.discourse.defaultCodeLang; + const attributes = tokenInfo?.attributes || {}; - if (info) { - // strip off any additional languages - info = info.trim().split(/\s+/g)[0]; + let className; + + const acceptableCodeClasses = + md.options.discourse.acceptableCodeClasses || []; + + if (TEXT_CODE_CLASSES.indexOf(tag) > -1) { + className = "lang-nohighlight"; + } else if (acceptableCodeClasses.indexOf(tag) > -1) { + className = `lang-${tag}`; + } else { + className = "lang-nohighlight"; + attributes["wrap"] = tag; } - const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses; - if ( - acceptableCodeClasses && - info && - acceptableCodeClasses.indexOf(info) !== -1 - ) { - langName = info; - } + const dataAttributes = Object.keys(attributes) + .map((key) => { + const value = md.utils.escapeHtml(attributes[key]); + key = md.utils.escapeHtml(key); + return `data-code-${key}="${value}"`; + }) + .join(" "); - className = - TEXT_CODE_CLASSES.indexOf(info) !== -1 - ? "lang-nohighlight" - : "lang-" + langName; - - return `
${escapedContent}
\n`; + return `${escapedContent}\n`; } export function setup(helper) { @@ -41,6 +84,8 @@ export function setup(helper) { .concat(["auto", "nohighlight"]); }); + helper.allowList(["pre[data-code-*]"]); + helper.allowList({ custom(tag, name, value) { if (tag === "code" && name === "class") { diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js index 0806a97b33..6a68b19d9f 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js @@ -118,6 +118,9 @@ function getEmojiTokenByName(name, state) { ["title", info.title], ["class", info.classes], ["alt", info.title], + ["loading", "lazy"], + ["width", "20"], + ["height", "20"], ]; return token; @@ -345,5 +348,8 @@ export function setup(helper) { "img[class=emoji emoji-custom]", "img[class=emoji emoji-custom only-emoji]", "img[class=emoji only-emoji]", + "img[loading=lazy]", + "img[width=20]", + "img[height=20]", ]); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js index f43fceec0d..3a12846ee2 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js @@ -127,6 +127,7 @@ const rule = { emojiCDNUrl: options.emojiCDNUrl, enableEmojiShortcuts: options.enableEmojiShortcuts, inlineEmoji: options.inlineEmoji, + lazy: true, }); } @@ -170,7 +171,7 @@ export function setup(helper) { md.block.bbcode.ruler.push("quotes", rule); }); - helper.allowList(["img[class=avatar]"]); + helper.allowList(["img[class=avatar]", "img[loading=lazy]"]); helper.allowList({ custom(tag, name, value) { if (tag === "aside" && name === "class") { diff --git a/app/assets/javascripts/select-kit/addon/components/category-drop.js b/app/assets/javascripts/select-kit/addon/components/category-drop.js index 5b19218fd6..925df37ed4 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-drop.js +++ b/app/assets/javascripts/select-kit/addon/components/category-drop.js @@ -131,7 +131,10 @@ export default ComboBoxComponent.extend({ search(filter) { if (filter) { - let results = Category.search(filter); + let opts = { + parentCategoryId: this.options.parentCategory?.id, + }; + let results = Category.search(filter, opts); results = this._filterUncategorized(results).sort((a, b) => { if (a.parent_category_id && !b.parent_category_id) { return 1; diff --git a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js index 7263a27ca0..9806006ad7 100644 --- a/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js +++ b/app/assets/javascripts/select-kit/addon/components/future-date-input-selector.js @@ -29,7 +29,6 @@ export default ComboBoxComponent.extend(DatetimeMixin, { now, day: now.day(), includeWeekend: this.includeWeekend, - includeMidFuture: this.includeMidFuture || true, includeFarFuture: this.includeFarFuture, includeDateTime: this.includeDateTime, canScheduleNow: this.includeNow || false, diff --git a/app/assets/javascripts/select-kit/addon/components/timezone-input.js b/app/assets/javascripts/select-kit/addon/components/timezone-input.js index 4590c703a7..28a0c25e6d 100644 --- a/app/assets/javascripts/select-kit/addon/components/timezone-input.js +++ b/app/assets/javascripts/select-kit/addon/components/timezone-input.js @@ -1,25 +1,29 @@ import ComboBoxComponent from "select-kit/components/combo-box"; -import { computed } from "@ember/object"; export default ComboBoxComponent.extend({ pluginApiIdentifiers: ["timezone-input"], classNames: ["timezone-input"], - nameProperty: null, - valueProperty: null, selectKitOptions: { filterable: true, allowAny: false, }, - content: computed(function () { - if ( - moment.locale() !== "en" && - typeof moment.tz.localizedNames === "function" - ) { - return moment.tz.localizedNames().mapBy("value"); - } else { - return moment.tz.names(); - } - }), + get nameProperty() { + return this.isLocalized ? "name" : null; + }, + + get valueProperty() { + return this.isLocalized ? "value" : null; + }, + + get content() { + return this.isLocalized ? moment.tz.localizedNames() : moment.tz.names(); + }, + + get isLocalized() { + return ( + moment.locale() !== "en" && typeof moment.tz.localizedNames === "function" + ); + }, }); diff --git a/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-filter.hbs b/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-filter.hbs index 9dc5578a85..d213fed5a8 100644 --- a/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-filter.hbs +++ b/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-filter.hbs @@ -4,7 +4,7 @@ tabindex=0 class="filter-input" placeholder=placeholder - autocomplete="discourse" + autocomplete="off" autocorrect="off" autocapitalize="off" name="filter-input-search" @@ -14,6 +14,7 @@ paste=(action "onPaste") keyDown=(action "onKeydown") keyUp=(action "onKeyup") + type="search" }} {{#if selectKit.options.filterIcon}} diff --git a/app/assets/javascripts/vendor-common.js b/app/assets/javascripts/vendor-common.js index 9ce43717d4..c4ea04921b 100644 --- a/app/assets/javascripts/vendor-common.js +++ b/app/assets/javascripts/vendor-common.js @@ -3,13 +3,11 @@ //= require template_include.js //= require message-bus -//= require jquery.ui.widget.js //= require Markdown.Converter.js //= require bootbox.js //= require popper.js //= require bootstrap-modal.js //= require caret_position -//= require jquery.sortable.js //= require lodash.js //= require itsatrap.js //= require rsvp.js diff --git a/app/assets/javascripts/wizard-vendor.js b/app/assets/javascripts/wizard-vendor.js index 0a72163b2c..fae10ff315 100644 --- a/app/assets/javascripts/wizard-vendor.js +++ b/app/assets/javascripts/wizard-vendor.js @@ -1,6 +1,5 @@ //= require ember_jquery //= require template_include.js -//= require jquery.ui.widget.js //= require uppy.js //= require bootstrap-modal.js //= require bootbox.js diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index f004e44354..12489ab12e 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -10,9 +10,9 @@ $mobile-breakpoint: 700px; display: inline-flex; position: relative; width: 100%; - height: auto; overflow: hidden; height: 100%; + @include breakpoint(tablet) { width: calc(100% + 10px); margin-left: -10px; @@ -856,6 +856,19 @@ table#user-badges { @include value-btn; margin-right: 0.25em; } + + .shift-up-value-btn, + .shift-down-value-btn { + display: none; + margin-inline: 0.25em; + } + + &:hover { + .shift-up-value-btn, + .shift-down-value-btn { + display: block; + } + } } .values { margin-bottom: 0.5em; diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index dfeff7a4a7..f09583c1b3 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -176,12 +176,10 @@ .themes-list-header { width: 100%; - border-bottom: 1px solid var(--primary-low); - border-top: 1px solid var(--primary-low); - border-right: 1px solid var(--primary-low); display: flex; .tab { + border: none; padding: 1em; width: 50%; box-sizing: border-box; @@ -190,16 +188,12 @@ &.active { font-weight: 700; } - - &:last-child { - border-left: 1px solid var(--primary-low); - } } } .themes-list-container { overflow-y: auto; - box-sizing: content-box; + box-sizing: border-box; max-height: 60vh; @media screen and (max-height: 1000px) { max-height: 50vh; @@ -209,6 +203,7 @@ } border-right: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low); + border-left: 1px solid var(--primary-low); width: 100%; .themes-list-item:last-child { @@ -218,7 +213,6 @@ color: var(--primary); border-bottom: 1px solid var(--primary-low); display: flex; - border-left: 1px solid var(--primary-low); &.inactive-indicator { border-right: 0; @@ -557,10 +551,9 @@ td.actions { width: 200px; } - .hex-input { + .color-picker input { width: 80px; margin-bottom: 0; - margin-right: 6px; } th.overriden { text-align: right; diff --git a/app/assets/stylesheets/common/admin/settings.scss b/app/assets/stylesheets/common/admin/settings.scss index 6fb4134d8a..c27a0606a3 100644 --- a/app/assets/stylesheets/common/admin/settings.scss +++ b/app/assets/stylesheets/common/admin/settings.scss @@ -107,15 +107,6 @@ font-size: $font-0; font-weight: normal; } - - &.color { - .color-picker { - display: flex; - .picker + .sp-replacer { - border-left: none; - } - } - } } .setting.overridden { h3 { diff --git a/app/assets/stylesheets/common/base/colorpicker.scss b/app/assets/stylesheets/common/base/colorpicker.scss index 4ebb76ae78..47f8329019 100644 --- a/app/assets/stylesheets/common/base/colorpicker.scss +++ b/app/assets/stylesheets/common/base/colorpicker.scss @@ -14,14 +14,6 @@ .colorpicker-wrapper { display: flex; align-items: flex-start; - .add-on { - @include form-item-sizing; - background-color: var(--primary-low); - border-color: var(--primary-medium); - border-right-color: transparent; - padding-left: 0.5em; - padding-right: 0.5em; - } } .colors-container { diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 8a07695208..5a8024c739 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -237,11 +237,6 @@ max-width: 40%; margin: 0 0 8px 8px; - @media screen and (max-width: 955px) { - flex: 1 0 100%; - margin-left: 0; - } - .category-chooser { display: flex; flex: 1 0 auto; @@ -286,12 +281,6 @@ min-width: 0; // allows flex to shrink flex-wrap: wrap; max-width: calc(50% - 4px); - - @media screen and (max-width: 920px) { - margin-right: 0; - margin-bottom: 6px; - max-width: calc(50% - 3px); - } } } @@ -322,10 +311,6 @@ margin: 0 0 8px 8px; z-index: z("composer", "dropdown"); - @media screen and (max-width: 920px) { - max-width: calc(50% - 3px); - } - .select-kit-header { color: var(--primary-high); } diff --git a/app/assets/stylesheets/common/base/crawler_layout.scss b/app/assets/stylesheets/common/base/crawler_layout.scss index ead8a0089e..fcf1e157cd 100644 --- a/app/assets/stylesheets/common/base/crawler_layout.scss +++ b/app/assets/stylesheets/common/base/crawler_layout.scss @@ -143,3 +143,8 @@ body.crawler { padding: 0.5em 1em 0.5em 0; } } + +.poll-info { + // crawler vote count always shows 0 + display: none; +} diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 6d79f84b53..9b253d36b8 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -230,6 +230,14 @@ input { } } +input[type="search"] { + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; + } +} + // Fixes Safari height inconsistency ::-webkit-datetime-edit { display: inline; diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index 7ca7384c57..5f1087ecd9 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -252,7 +252,7 @@ table.group-category-permissions { .group-imap-email-settings { .groups-form { display: grid; - grid-template-columns: 1fr 3fr; + grid-template-columns: 1fr 1fr 1fr; margin-bottom: 0; &.groups-form-imap { diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 7302275b13..07cffbaf11 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -14,8 +14,10 @@ backface-visibility: hidden; /** do magic for scrolling performance **/ > .wrap { - width: calc(100% - 20px); // accommodates for 10px vertical padding + box-sizing: border-box; + width: 100%; height: 100%; + .contents { display: flex; align-items: center; diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index e54dbc40b3..ec7911f438 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -1,5 +1,5 @@ @mixin search-page-spacing { - padding: 2rem 10%; + padding: 1rem 10%; @include breakpoint(medium) { padding: 1rem; @@ -63,7 +63,6 @@ .search-advanced { position: relative; - .search-actions, .search-title, .search-bar { @@ -76,19 +75,17 @@ .search-info { display: flex; - @include search-page-spacing(); flex-wrap: wrap; + padding: 1em 10%; + background-color: var(--secondary); border-bottom: 1px solid var(--primary-low); - padding-top: 2em; - margin-bottom: 2em; - flex-direction: row; - align-items: flex-start; - justify-content: flex-start; + @include breakpoint(medium) { + padding: 0.5em 0; + } &.bulk-select-visible { @include sticky; top: 60px; - background-color: var(--secondary); z-index: 10; } @@ -97,7 +94,10 @@ } button { - margin: 0 0.5em 0.5em 0; + margin: 0 0.5em 0 0; + @include breakpoint(medium) { + margin: 0 0.5em 0.5em 0; + } } #bulk-select { @@ -110,6 +110,13 @@ margin-right: 0.5em; } } + + .sort-by.inline-form label { + margin-bottom: 0; + } + #search-sort-by { + margin-bottom: 0; + } } } @@ -229,7 +236,7 @@ .fps-result { display: flex; - padding: 0 0.5em; + padding: 0 0.5em 0 0; margin-bottom: 2em; max-width: 100%; word-break: break-word; @@ -258,10 +265,10 @@ align-items: baseline; .bulk-select { position: absolute; - left: 0px; - top: 0px; - padding: 0.5em; - background: var(--tertiary-very-low); + left: 0.5em; + top: 0.75em; + padding: 0.25em 0.5em; + background-color: var(--tertiary-low); input[type="checkbox"] { margin: 0; } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 2d4783b1a4..4ca310e6c4 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -144,17 +144,17 @@ $quote-share-maxwidth: 150px; word-wrap: break-word; } - del, - ins, mark { text-decoration: none; } ins { background-color: var(--success-low); + text-decoration: underline; } del { background-color: var(--danger-low); + text-decoration: line-through; } mark { background-color: var(--highlight); diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index 3329133022..0e58e6cb64 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -3,6 +3,7 @@ @import "bookmark-list"; @import "bookmark-modal"; @import "buttons"; +@import "color-input"; @import "conditional-loading-section"; @import "convert-to-public-topic-modal"; @import "date-input"; diff --git a/app/assets/stylesheets/common/components/color-input.scss b/app/assets/stylesheets/common/components/color-input.scss new file mode 100644 index 0000000000..60462b499e --- /dev/null +++ b/app/assets/stylesheets/common/components/color-input.scss @@ -0,0 +1,31 @@ +.color-picker { + display: flex; + * { + height: 35px; + } + .add-on { + @include form-item-sizing; + background-color: var(--primary-low); + border-color: var(--primary-medium); + border-right-color: transparent; + padding-left: 0.5em; + padding-right: 0.5em; + } + .hex-input { + margin-right: 0; + } + .picker { + padding: 0; + border-left: none; + cursor: pointer; + margin-bottom: 0; + } + // Reset webkit/blink default style + input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + } + input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 0; // Reset webkit specific style + } +} diff --git a/app/assets/stylesheets/common/foundation/base.scss b/app/assets/stylesheets/common/foundation/base.scss index e1a47dbd71..972e2f7ac7 100644 --- a/app/assets/stylesheets/common/foundation/base.scss +++ b/app/assets/stylesheets/common/foundation/base.scss @@ -167,13 +167,15 @@ input[type="submit"] { display: flex; } - > input[type="text"] { + > input[type="text"], + > input[type="search"] { display: inline-flex; flex: 1; } > .select-kit, > input[type="text"], + > input[type="search"], > label, > .btn, > .d-date-input { diff --git a/app/assets/stylesheets/common/software-update-prompt.scss b/app/assets/stylesheets/common/software-update-prompt.scss index c253eaad48..812b7e2687 100644 --- a/app/assets/stylesheets/common/software-update-prompt.scss +++ b/app/assets/stylesheets/common/software-update-prompt.scss @@ -22,6 +22,12 @@ .update-prompt-message { cursor: pointer; padding: 0.75em 0; + + .d-icon { + margin-right: 0.33em; + font-size: 0.9em; + } + span { text-decoration: underline; } @@ -35,17 +41,15 @@ span { cursor: pointer; - display: inline; height: 100%; display: flex; align-items: center; padding-left: 20px; - } - } - .d-icon { - margin-right: 0.33em; - font-size: 0.9em; + &:hover { + color: var(--tertiary-hover); + } + } } &.require-software-refresh { diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index cb64cbf2cd..f9530d81b1 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -85,8 +85,10 @@ margin-bottom: 6px; } - .category-input { + .category-input, + .with-tags .category-input { margin-bottom: 6px; + max-width: calc(50% - 3px); } .submit-panel { @@ -189,6 +191,7 @@ .mini-tag-chooser { margin: 0 0 6px 6px; + max-width: calc(50% - 3px); } .selected-name { diff --git a/app/assets/stylesheets/mobile/login.scss b/app/assets/stylesheets/mobile/login.scss index 60d121b9ee..0ea664e63f 100644 --- a/app/assets/stylesheets/mobile/login.scss +++ b/app/assets/stylesheets/mobile/login.scss @@ -190,6 +190,7 @@ width: calc(100% - 2em); position: sticky; position: -webkit-sticky; + z-index: z("header"); top: 0; background-color: var(--secondary); } diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index 579231ee99..bf154a0b70 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -991,6 +991,7 @@ body.wizard { color: var(--secondary-or-primary); z-index: 13; left: 1.5em; + line-height: 1.4em; } .screen { diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 3b9224f1b3..806bf4636d 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -157,7 +157,7 @@ class Admin::EmailController < Admin::AdminController retry_count = 0 begin - Jobs.enqueue(:process_email, mail: email_raw, retry_on_rate_limit: true, source: :handle_mail) + Jobs.enqueue(:process_email, mail: email_raw, retry_on_rate_limit: true, source: "handle_mail") rescue JSON::GeneratorError, Encoding::UndefinedConversionError => e if retry_count == 0 email_raw = email_raw.force_encoding('iso-8859-1').encode("UTF-8") diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b29ceda5fb..d719db0ab8 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -127,7 +127,7 @@ class Admin::UsersController < Admin::AdminController if message.present? Jobs.enqueue( :critical_user_email, - type: :account_suspended, + type: "account_suspended", user_id: @user.id, user_history_id: user_history.id ) @@ -368,7 +368,7 @@ class Admin::UsersController < Admin::AdminController if silencer.silence Jobs.enqueue( :critical_user_email, - type: :account_silenced, + type: "account_silenced", user_id: @user.id, user_history_id: silencer.user_history.id ) @@ -412,7 +412,7 @@ class Admin::UsersController < Admin::AdminController Jobs.enqueue( :critical_user_email, - type: :account_second_factor_disabled, + type: "account_second_factor_disabled", user_id: @user.id ) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 2ca448e323..153790cfbb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -251,7 +251,7 @@ class GroupsController < ApplicationController Discourse.deprecate(":desc is deprecated please use :asc instead", output_in_test: true, drop_from: '2.9.0') dir = (params[:desc] && params[:desc].present?) ? 'DESC' : 'ASC' end - order = "" + order = "NOT group_users.owner" if params[:requesters] guardian.ensure_can_edit!(group) @@ -305,21 +305,15 @@ class GroupsController < ApplicationController end end - users = users.joins(:user_option).select('users.*, user_options.timezone, group_users.created_at as added_at') - - members = users - .order('NOT group_users.owner') + users = users + .includes(:primary_group) + .joins(:user_option) + .select('users.*, user_options.timezone, group_users.created_at as added_at') .order(order) .order(username_lower: dir) - .limit(limit) - .offset(offset) - .includes(:primary_group) - owners = users - .order(order) - .order(username_lower: dir) - .where('group_users.owner') - .includes(:primary_group) + members = users.limit(limit).offset(offset) + owners = users.where('group_users.owner') render json: { members: serialize_data(members, GroupUserSerializer), @@ -713,6 +707,7 @@ class GroupsController < ApplicationController :imap_updated_at, :email_username, :email_password, + :email_from_alias, :primary_group, :visibility_level, :members_visibility_level, diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 2c75d65911..5404f9a670 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -22,31 +22,36 @@ class InvitesController < ApplicationController invite = Invite.find_by(invite_key: params[:id]) if invite.present? && invite.redeemable? if current_user - added_to_group = false + InvitedUser.transaction do + invited_user = InvitedUser.find_or_initialize_by(user: current_user, invite: invite) + if invited_user.new_record? + invited_user.save! + Invite.increment_counter(:redemption_count, invite.id) + invite.invited_by.notifications.create!( + notification_type: Notification.types[:invitee_accepted], + data: { display_username: current_user.username }.to_json + ) + end + end if invite.groups.present? invite_by_guardian = Guardian.new(invite.invited_by) new_group_ids = invite.groups.pluck(:id) - current_user.group_users.pluck(:group_id) new_group_ids.each do |id| if group = Group.find_by(id: id) - if invite_by_guardian.can_edit_group?(group) - group.add(current_user) - added_to_group = true - end + group.add(current_user) if invite_by_guardian.can_edit_group?(group) end end end - create_topic_invite_notifications(invite, current_user) - if topic = invite.topics.first - new_guardian = Guardian.new(current_user) - return redirect_to(topic.url) if new_guardian.can_see?(topic) - elsif added_to_group - return redirect_to(path("/")) + if current_user.guardian.can_see?(topic) + create_topic_invite_notifications(invite, current_user) + return redirect_to(topic.url) + end end - return ensure_not_logged_in + return redirect_to(path("/")) end email = Email.obfuscate(invite.email) @@ -361,6 +366,12 @@ class InvitesController < ApplicationController def resend_all_invites guardian.ensure_can_resend_all_invites!(current_user) + begin + RateLimiter.new(current_user, "bulk-reinvite-per-day", 1, 1.day, apply_limit_to_staff: true).performed! + rescue RateLimiter::LimitExceeded + return render_json_error(I18n.t("rate_limiter.slow_down")) + end + Invite.pending(current_user) .where('invites.email IS NOT NULL') .find_each { |invite| invite.resend_invite } diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index 47213031d9..eff9af7ac5 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -65,49 +65,21 @@ class MetadataController < ApplicationController name: I18n.t('js.topic.create_long'), short_name: I18n.t('js.topic.create'), url: "#{Discourse.base_path}/new-topic", - icons: [ - { - src: "#{icon_url_base}/plus.svg", - sizes: "131x150", - type: "image/svg" - } - ] }, { name: I18n.t('js.user.messages.inbox'), short_name: I18n.t('js.user.messages.inbox'), url: "#{Discourse.base_path}/my/messages", - icons: [ - { - src: "#{icon_url_base}/envelope.svg", - sizes: "150x150", - type: "image/svg" - } - ] }, { name: I18n.t('js.user.bookmarks'), short_name: I18n.t('js.user.bookmarks'), url: "#{Discourse.base_path}/my/activity/bookmarks", - icons: [ - { - src: "#{icon_url_base}/bookmark.svg", - sizes: "113x150", - type: "image/svg" - } - ] }, { name: I18n.t('js.filters.top.title'), short_name: I18n.t('js.filters.top.title'), url: "#{Discourse.base_path}/top", - icons: [ - { - src: "#{icon_url_base}/signal.svg", - sizes: "188x150", - type: "image/svg" - } - ] } ] } diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index b3014a111f..da7ecf92fd 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -182,6 +182,7 @@ class PostsController < ApplicationController def create @manager_params = create_params @manager_params[:first_post_checks] = !is_api? + @manager_params[:advance_draft] = !is_api? manager = NewPostManager.new(current_user, @manager_params) @@ -546,6 +547,7 @@ class PostsController < ApplicationController def wiki post = find_post_from_params + params.require(:wiki) guardian.ensure_can_wiki!(post) post.revise(current_user, wiki: params[:wiki]) @@ -555,8 +557,10 @@ class PostsController < ApplicationController def post_type guardian.ensure_can_change_post_type! - post = find_post_from_params + params.require(:post_type) + raise Discourse::InvalidParameters.new(:post_type) if Post.types[params[:post_type].to_i].blank? + post.revise(current_user, post_type: params[:post_type].to_i) render body: nil @@ -757,6 +761,8 @@ class PostsController < ApplicationController # We allow `created_at` via the API permitted << :created_at + # We allow `external_id` via the API + permitted << :external_id end result = params.permit(*permitted).tap do |allowed| diff --git a/app/controllers/qunit_controller.rb b/app/controllers/qunit_controller.rb index 003b343024..2abd0744e4 100644 --- a/app/controllers/qunit_controller.rb +++ b/app/controllers/qunit_controller.rb @@ -23,7 +23,7 @@ class QunitController < ApplicationController @is_proxied = is_ember_cli_proxy? @legacy_ember = if Rails.env.production? - ENV['EMBER_CLI_PROD_ASSETS'] != "1" + ENV['EMBER_CLI_PROD_ASSETS'] == "0" else !@is_proxied end diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index d24e3e9e03..62264baa60 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -444,7 +444,7 @@ class SessionController < ApplicationController if user RateLimiter.new(nil, "forgot-password-login-day-#{user.username}", 6, 1.day).performed! email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) - Jobs.enqueue(:critical_user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token) + Jobs.enqueue(:critical_user_email, type: "forgot_password", user_id: user.id, email_token: email_token.token) else RateLimiter.new(nil, "forgot-password-login-hour-#{normalized_login_param}", 5, 1.hour).performed! end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index baeef8c388..abb249c2e8 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -43,6 +43,13 @@ class TopicsController < ApplicationController render json: { slug: topic.slug, topic_id: topic.id, url: topic.url } end + def show_by_external_id + topic = Topic.find_by(external_id: params[:external_id]) + raise Discourse::NotFound unless topic + guardian.ensure_can_see!(topic) + redirect_to_correct_topic(topic, params[:post_number]) + end + def show if request.referer flash["referer"] ||= request.referer[0..255] @@ -839,7 +846,7 @@ class TopicsController < ApplicationController destination_topic = move_posts_to_destination(topic) render_topic_changes(destination_topic) - rescue ActiveRecord::RecordInvalid => ex + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => ex render_json_error(ex) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 25355a4260..68cf75bd87 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -737,7 +737,7 @@ class UsersController < ApplicationController session["user_created_message"] = activation.success_message if existing_user = User.find_by_email(user.primary_email&.email) - Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id) + Jobs.enqueue(:critical_user_email, type: "account_exists", user_id: existing_user.id) end render json: { @@ -932,7 +932,7 @@ class UsersController < ApplicationController if user = User.with_email(params[:email]).admins.human_users.first email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) - Jobs.enqueue(:critical_user_email, type: :admin_login, user_id: user.id, email_token: email_token.token) + Jobs.enqueue(:critical_user_email, type: "admin_login", user_id: user.id, email_token: email_token.token) @message = I18n.t("admin_login.success") else @message = I18n.t("admin_login.errors.unknown_email_address") @@ -967,7 +967,7 @@ class UsersController < ApplicationController email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) Jobs.enqueue(:critical_user_email, - type: :email_login, + type: "email_login", user_id: user.id, email_token: email_token.token ) @@ -1535,7 +1535,7 @@ class UsersController < ApplicationController Jobs.enqueue( :critical_user_email, - type: :account_second_factor_disabled, + type: "account_second_factor_disabled", user_id: current_user.id ) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index fdcd1dd55e..8d82e2d2df 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -293,7 +293,23 @@ module Jobs opts[:current_site_id] ||= RailsMultisite::ConnectionManagement.current_db end - # If we are able to queue a job, do it + delay = opts.delete(:delay_for) + queue = opts.delete(:queue) + + # Only string keys are allowed in JSON. We call `.with_indifferent_access` + # in Jobs::Base#perform, so this is invisible to developers + opts = opts.deep_stringify_keys + + # Simulate the args being dumped/parsed through JSON + parsed_opts = JSON.parse(JSON.dump(opts)) + if opts != parsed_opts + Discourse.deprecate(<<~MSG.squish, since: "2.9", drop_from: "3.0") + #{klass.name} was enqueued with argument values which do not cleanly serialize to/from JSON. + This means that the job will be run with slightly different values than the ones supplied to `enqueue`. + Argument values should be strings, booleans, numbers, or nil (or arrays/hashes of those value types). + MSG + end + opts = parsed_opts if ::Jobs.run_later? hash = { @@ -301,23 +317,21 @@ module Jobs 'args' => [opts] } - if delay = opts.delete(:delay_for) + if delay if delay.to_f > 0 hash['at'] = Time.now.to_f + delay.to_f end end - if queue = opts.delete(:queue) + if queue hash['queue'] = queue end DB.after_commit { klass.client_push(hash) } else # Otherwise execute the job right away - opts.delete(:delay_for) - opts.delete(:queue) + opts["sync_exec"] = true - opts[:sync_exec] = true if Rails.env == "development" Scheduler::Defer.later("job") do klass.new.perform(opts) diff --git a/app/jobs/regular/suspicious_login.rb b/app/jobs/regular/suspicious_login.rb index 2c08776c5d..a45b820de0 100644 --- a/app/jobs/regular/suspicious_login.rb +++ b/app/jobs/regular/suspicious_login.rb @@ -13,7 +13,7 @@ module Jobs client_ip: args[:client_ip]) ::Jobs.enqueue(:critical_user_email, - type: :suspicious_login, + type: "suspicious_login", user_id: args[:user_id], client_ip: args[:client_ip], user_agent: args[:user_agent]) diff --git a/app/jobs/scheduled/activation_reminder_emails.rb b/app/jobs/scheduled/activation_reminder_emails.rb index c3203a273d..5bfbf9e9f3 100644 --- a/app/jobs/scheduled/activation_reminder_emails.rb +++ b/app/jobs/scheduled/activation_reminder_emails.rb @@ -16,7 +16,7 @@ module Jobs email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup]) ::Jobs.enqueue( :user_email, - type: :activation_reminder, + type: "activation_reminder", user_id: user.id, email_token: email_token.token ) diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index 4496f2548b..e76a60d97e 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -10,7 +10,7 @@ module Jobs users = target_user_ids users.each do |user_id| - ::Jobs.enqueue(:user_email, type: :digest, user_id: user_id) + ::Jobs.enqueue(:user_email, type: "digest", user_id: user_id) end end diff --git a/app/mailers/group_smtp_mailer.rb b/app/mailers/group_smtp_mailer.rb index ce12e4b0cc..25bb887b0d 100644 --- a/app/mailers/group_smtp_mailer.rb +++ b/app/mailers/group_smtp_mailer.rb @@ -48,7 +48,7 @@ class GroupSmtpMailer < ActionMailer::Base add_re_to_subject: true, locale: SiteSetting.default_locale, delivery_method_options: delivery_options, - from: from_group.email_username, + from: from_group.smtp_from_address, from_alias: I18n.t('email_from_without_site', user_name: group_name), html_override: html_override(post), cc: cc_addresses diff --git a/app/models/email_token.rb b/app/models/email_token.rb index 1178352523..1db13a01d5 100644 --- a/app/models/email_token.rb +++ b/app/models/email_token.rb @@ -95,7 +95,7 @@ class EmailToken < ActiveRecord::Base def self.enqueue_signup_email(email_token, to_address: nil) Jobs.enqueue( :critical_user_email, - type: :signup, + type: "signup", user_id: email_token.user_id, email_token: email_token.token, to_address: to_address diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index 212348bc3d..6dd77cbb2b 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -14,13 +14,13 @@ class EmbeddableHost < ActiveRecord::Base self.ignored_columns = ["path_whitelist"] def self.record_for_url(uri) - if uri.is_a?(String) uri = begin URI(UrlHelper.escape_uri(uri)) - rescue URI::Error + rescue URI::Error, Addressable::URI::InvalidURIError end end + return false unless uri.present? host = uri.host diff --git a/app/models/emoji.rb b/app/models/emoji.rb index 8e7d8fd6be..36bdea93be 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -240,9 +240,9 @@ class Emoji if code && Emoji.custom?(code) emoji = Emoji[code] - "\"#{code}\"" + "\"#{code}\"" elsif code && Emoji.exists?(code) - "\"#{code}\"" + "\"#{code}\"" else name end diff --git a/app/models/group.rb b/app/models/group.rb index a53d7e3ce3..3c64ab49d1 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -110,7 +110,8 @@ class Group < ActiveRecord::Base "imap_port", "imap_ssl", "email_username", - "email_password" + "email_password", + "email_from_alias" ] ALIAS_LEVELS = { @@ -290,6 +291,10 @@ class Group < ActiveRecord::Base end end + def smtp_from_address + self.email_from_alias.present? ? self.email_from_alias : self.email_username + end + def downcase_incoming_email self.incoming_email = (incoming_email || "").strip.downcase.presence end @@ -708,7 +713,9 @@ class Group < ActiveRecord::Base def self.find_by_email(email) self.where( - "email_username = :email OR string_to_array(incoming_email, '|') @> ARRAY[:email]", + "email_username = :email OR + string_to_array(incoming_email, '|') @> ARRAY[:email] OR + email_from_alias = :email", email: Email.downcase(email) ).first end @@ -1128,6 +1135,7 @@ end # imap_enabled :boolean default(FALSE) # imap_updated_at :datetime # imap_updated_by_id :integer +# email_from_alias :string # # Indexes # diff --git a/app/models/post.rb b/app/models/post.rb index 04df7e932f..2acb46814a 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -546,12 +546,17 @@ class Post < ActiveRecord::Base self.hidden_at = Time.zone.now self.hidden_reason_id = reason self.skip_unique_check = true - save! - Topic.where( - "id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", - topic_id: topic_id - ).update_all(visible: false) + Post.transaction do + save! + + Topic.where( + "id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", + topic_id: topic_id + ).update_all(visible: false) + + UserStatCountUpdater.decrement!(self) + end # inform user if user.present? @@ -574,16 +579,20 @@ class Post < ActiveRecord::Base 5.seconds, :send_system_message, user_id: user.id, - message_type: message, + message_type: message.to_s, message_options: options ) end end def unhide! - self.update(hidden: false) - self.topic.update(visible: true) if is_first_post? - save(validate: false) + Post.transaction do + self.update!(hidden: false) + self.topic.update(visible: true) if is_first_post? + UserStatCountUpdater.increment!(self) + save(validate: false) + end + publish_change_to_clients!(:acted) end diff --git a/app/models/post_action.rb b/app/models/post_action.rb index a26998aa7c..5bb7b39eb5 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -239,8 +239,7 @@ class PostAction < ActiveRecord::Base end if column == "like_count" - topic_count = Post.where(topic_id: topic_id).sum(column) - Topic.where(id: topic_id).update_all ["#{column} = ?", topic_count] + Topic.find_by(id: topic_id)&.update_action_counts end end diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index e24b9827d8..945d547377 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -165,7 +165,7 @@ class PostMover guardian: Guardian.new(user), skip_jobs: true ) - new_post = @post_creator.create + new_post = @post_creator.create! move_email_logs(post, new_post) diff --git a/app/models/reviewable_flagged_post.rb b/app/models/reviewable_flagged_post.rb index 3795f05267..68e04c19c8 100644 --- a/app/models/reviewable_flagged_post.rb +++ b/app/models/reviewable_flagged_post.rb @@ -321,7 +321,7 @@ private Jobs.enqueue( :send_system_message, user_id: post.user_id, - message_type: :flags_disagreed, + message_type: "flags_disagreed", message_options: { flagged_post_raw_content: post.raw, url: post.url diff --git a/app/models/reviewable_user.rb b/app/models/reviewable_user.rb index 93bc96924e..02cbfe4bf0 100644 --- a/app/models/reviewable_user.rb +++ b/app/models/reviewable_user.rb @@ -31,7 +31,7 @@ class ReviewableUser < Reviewable if args[:send_email] != false && SiteSetting.must_approve_users? Jobs.enqueue( :critical_user_email, - type: :signup_after_approval, + type: "signup_after_approval", user_id: target.id ) end diff --git a/app/models/site.rb b/app/models/site.rb index d8ba6cf083..4442a12e27 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -7,6 +7,20 @@ class Site cattr_accessor :preloaded_category_custom_fields self.preloaded_category_custom_fields = Set.new + ## + # Sometimes plugins need to have additional data or options available + # when rendering custom markdown features/rules that are not available + # on the default opts.discourse object. These additional options should + # be namespaced to the plugin adding them. + # + # ``` + # Site.markdown_additional_options["chat"] = { limited_pretty_text_markdown_rules: [] } + # ``` + # + # These are passed down to markdown rules on opts.discourse.additionalOptions. + cattr_accessor :markdown_additional_options + self.markdown_additional_options = {} + def self.add_categories_callbacks(&block) categories_callbacks << block end diff --git a/app/models/theme.rb b/app/models/theme.rb index c7b3def305..5fcaf5f436 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -152,7 +152,8 @@ class Theme < ActiveRecord::Base SvgSprite.expire_cache end - BASE_COMPILER_VERSION = 51 + BASE_COMPILER_VERSION = 53 + def self.compiler_version get_set_cache "compiler_version" do dependencies = [ diff --git a/app/models/topic.rb b/app/models/topic.rb index 1a6948c6cc..d3dc304700 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -11,6 +11,8 @@ class Topic < ActiveRecord::Base include LimitedEdit extend Forwardable + EXTERNAL_ID_MAX_LENGTH = 50 + self.ignored_columns = [ "avg_time", # TODO(2021-01-04): remove "image_url" # TODO(2021-06-01): remove @@ -195,6 +197,8 @@ class Topic < ActiveRecord::Base end end + validates :external_id, allow_nil: true, uniqueness: { case_sensitive: false }, length: { maximum: EXTERNAL_ID_MAX_LENGTH }, format: { with: /\A[\w-]+\z/ } + before_validation do self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[:title].empty? self.featured_link = self.featured_link.strip.presence if self.featured_link @@ -326,6 +330,7 @@ class Topic < ActiveRecord::Base attr_accessor :ignore_category_auto_close attr_accessor :skip_callbacks + attr_accessor :advance_draft before_create do initialize_default_values @@ -334,7 +339,7 @@ class Topic < ActiveRecord::Base after_create do unless skip_callbacks changed_to_category(category) - advance_draft_sequence + advance_draft_sequence if advance_draft end end @@ -1902,6 +1907,7 @@ end # image_upload_id :bigint # slow_mode_seconds :integer default(0), not null # bannered_until :datetime +# external_id :string # # Indexes # @@ -1911,6 +1917,7 @@ end # index_topics_on_bannered_until (bannered_until) WHERE (bannered_until IS NOT NULL) # index_topics_on_bumped_at_public (bumped_at) WHERE ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) # index_topics_on_created_at_and_visible (created_at,visible) WHERE ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) +# index_topics_on_external_id (external_id) UNIQUE WHERE (external_id IS NOT NULL) # index_topics_on_id_and_deleted_at (id,deleted_at) # index_topics_on_id_filtered_banner (id) UNIQUE WHERE (((archetype)::text = 'banner'::text) AND (deleted_at IS NULL)) # index_topics_on_image_upload_id (image_upload_id) diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index 925c98c64b..e9988ab89d 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -40,7 +40,7 @@ class TopicConverter def convert_to_private_message Topic.transaction do - @topic.update_category_topic_count_by(-1) + @topic.update_category_topic_count_by(-1) if @topic.visible PostRevisor.new(@topic.first_post, @topic).revise!( @user, @@ -63,21 +63,50 @@ class TopicConverter private def posters - @posters ||= @topic.posts.distinct.pluck(:user_id).to_a + @posters ||= @topic.posts.where("post_number > 1").distinct.pluck(:user_id) + end + + def increment_users_post_count + update_users_post_count(:increment) + end + + def decrement_users_post_count + update_users_post_count(:decrement) + end + + def update_users_post_count(action) + operation = action == :increment ? "+" : "-" + + # NOTE that DirectoryItem.refresh will overwrite this by counting UserAction records. + # + # Changes user_stats (post_count) by the number of posts in the topic. + # First post, hidden posts and non-regular posts are ignored. + DB.exec(<<~SQL) + UPDATE user_stats + SET post_count = post_count #{operation} X.count + FROM ( + SELECT + us.user_id, + COUNT(*) AS count + FROM user_stats us + INNER JOIN posts ON posts.topic_id = #{@topic.id.to_i} AND posts.user_id = us.user_id + WHERE posts.post_number > 1 + AND NOT posts.hidden + AND posts.post_type = #{Post.types[:regular].to_i} + GROUP BY us.user_id + ) X + WHERE X.user_id = user_stats.user_id + SQL end def update_user_stats - # update posts count. NOTE that DirectoryItem.refresh will overwrite this by counting UserAction records. - # update topics count - UserStat.where(user_id: posters).update_all('post_count = post_count + 1') - UserStat.where(user_id: @topic.user_id).update_all('topic_count = topic_count + 1') + increment_users_post_count + UserStatCountUpdater.increment!(@topic.first_post) end def add_allowed_users - # update posts count. NOTE that DirectoryItem.refresh will overwrite this by counting UserAction records. - # update topics count - UserStat.where(user_id: posters).update_all('post_count = post_count - 1') - UserStat.where(user_id: @topic.user_id).update_all('topic_count = topic_count - 1') + decrement_users_post_count + UserStatCountUpdater.decrement!(@topic.first_post) existing_allowed_users = @topic.topic_allowed_users.pluck(:user_id) users_to_allow = posters << @user.id diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 75003f4345..4c01632a11 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -106,7 +106,7 @@ class UserOption < ActiveRecord::Base Discourse.redis.expire(key, delay) # delay the update - Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.user_id, redirected_at: Time.zone.now) + Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.user_id, redirected_at: Time.zone.now.to_s) end def should_be_redirected_to_top diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb index 0b51aed01b..f3b520d19a 100644 --- a/app/serializers/group_show_serializer.rb +++ b/app/serializers/group_show_serializer.rb @@ -32,6 +32,7 @@ class GroupShowSerializer < BasicGroupSerializer :imap_updated_by, :email_username, :email_password, + :email_from_alias, :imap_last_error, :imap_old_emails, :imap_new_emails, diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index c65628748f..7b16d6f1a6 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -32,7 +32,8 @@ class SiteSerializer < ApplicationSerializer :custom_emoji_translation, :watched_words_replace, :watched_words_link, - :categories + :categories, + :markdown_additional_options ) has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer @@ -203,6 +204,10 @@ class SiteSerializer < ApplicationSerializer object.categories.map { |c| c.to_h } end + def markdown_additional_options + Site.markdown_additional_options + end + private def ordered_flags(flags) diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 0d717fa2b5..268fbc7872 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -41,7 +41,8 @@ class TopicViewSerializer < ApplicationSerializer :pinned_at, :pinned_until, :image_url, - :slow_mode_seconds + :slow_mode_seconds, + :external_id ) attributes( @@ -104,6 +105,10 @@ class TopicViewSerializer < ApplicationSerializer is_warning end + def include_external_id? + external_id + end + def draft object.draft end diff --git a/app/services/notification_emailer.rb b/app/services/notification_emailer.rb index a1e7f12efa..05e7a8b435 100644 --- a/app/services/notification_emailer.rb +++ b/app/services/notification_emailer.rb @@ -59,11 +59,11 @@ class NotificationEmailer notification_type = Notification.types[notification.notification_type] hash = { - type: type, + type: type.to_s, user_id: notification.user_id, notification_id: notification.id, notification_data_hash: notification.data_hash, - notification_type: notification_type, + notification_type: notification_type.to_s, } hash[:post_id] = post_id if post_id > 0 && notification_type != :post_approved diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index e774ad4df6..36a846fdf2 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -120,11 +120,11 @@ class SearchIndexer a_weight: topic_title, b_weight: category_name, c_weight: topic_tags, - # Length of a tsvector must be less than 1_048_576 bytes. - # The difference between the max ouptut limit and imposed input limit - # accounts for the fact that sometimes the output tsvector may be - # slighlty longer than the input. - d_weight: scrub_html_for_search(cooked)[0..1_000_000] + # The tsvector resulted from parsing a string can be double the size of + # the original string. Since there is no way to estimate the length of + # the expected tsvector, we limit the input to ~50% of the maximum + # length of a tsvector (1_048_576 bytes). + d_weight: scrub_html_for_search(cooked)[0..600_000] ) do |params| params["private_message"] = private_message end diff --git a/app/services/topic_status_updater.rb b/app/services/topic_status_updater.rb index fe0f912ec0..d5e1d95d44 100644 --- a/app/services/topic_status_updater.rb +++ b/app/services/topic_status_updater.rb @@ -46,8 +46,9 @@ TopicStatusUpdater = Struct.new(:topic, :user) do UserProfile.remove_featured_topic_from_all_profiles(topic) end - if status.visible? + if status.visible? && result topic.update_category_topic_count_by(status.enabled? ? 1 : -1) + UserStatCountUpdater.public_send(status.enabled? ? :increment! : :decrement!, topic.first_post) end if @topic_timer diff --git a/app/services/user_stat_count_updater.rb b/app/services/user_stat_count_updater.rb new file mode 100644 index 0000000000..53995d3f32 --- /dev/null +++ b/app/services/user_stat_count_updater.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class UserStatCountUpdater + class << self + def increment!(post, user_stat: nil) + update!(post, user_stat: user_stat) + end + + def decrement!(post, user_stat: nil) + update!(post, user_stat: user_stat, action: :decrement!) + end + + private + + def update!(post, user_stat: nil, action: :increment!) + return if !post&.topic + return if action == :increment! && post.topic.private_message? + stat = user_stat || post.user&.user_stat + + return if stat.blank? + + column = + if post.is_first_post? + :topic_count + elsif post.post_type == Post.types[:regular] + :post_count + end + + return if column.blank? + + # There are lingering bugs in the code base that does not properly increase the count when the status of the post + # changes. Since we have Job::DirectoryRefreshOlder which runs daily to reconcile the count, there is no need + # to trigger an error. + if action == :decrement! && stat.public_send(column) < 1 + if SiteSetting.verbose_user_stat_count_logging + Rails.logger.warn("Attempted to insert negative count into UserStat##{column} for post with id '#{post.id}'") + end + + return + end + + stat.public_send(action, column) + end + end +end diff --git a/config/application.rb b/config/application.rb index 1c4a0bb51b..072b86f6d1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -180,7 +180,7 @@ module Discourse discourse/tests/test_starter.js } - if ENV['EMBER_CLI_PROD_ASSETS'] != "1" + if ENV['EMBER_CLI_PROD_ASSETS'] == "0" config.assets.precompile += %w{ discourse/tests/test-support-rails.js discourse/tests/test-helpers-rails.js diff --git a/config/initializers/100-sidekiq.rb b/config/initializers/100-sidekiq.rb index a14a080307..4f7a77b69a 100644 --- a/config/initializers/100-sidekiq.rb +++ b/config/initializers/100-sidekiq.rb @@ -81,7 +81,16 @@ if Sidekiq.server? end end -Sidekiq.logger.level = Logger::WARN +# Sidekiq#logger= applies patches to whichever logger we pass it. +# Therefore something like Sidekiq.logger = Rails.logger will break +# all logging in the application. +# +# Instead, this patch adds a dedicated logger instance and patches +# the #add method to forward messages to Rails.logger. +Sidekiq.logger = Logger.new(nil) +Sidekiq.logger.define_singleton_method(:add) do |severity, message = nil, progname = nil, &blk| + Rails.logger.add(severity, message, progname, &blk) +end class SidekiqLogsterReporter < Sidekiq::ExceptionHandler::Logger def call(ex, context = {}) @@ -111,8 +120,7 @@ class SidekiqLogsterReporter < Sidekiq::ExceptionHandler::Logger end end -unless Rails.env.development? - Sidekiq.error_handlers.clear -end - +Sidekiq.error_handlers.clear Sidekiq.error_handlers << SidekiqLogsterReporter.new + +Sidekiq.strict_args! diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 0c9966d58d..1b18005b49 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -3597,7 +3597,6 @@ ar: default_list_filter: "تصفية القائمة الافتراضية:" allow_badges_label: "السماح بمنح الشارات في هذه الفئة" edit_permissions: "تعديل الأذونات" - reviewable_by_group: "بالإضافة إلى فريق العمل، يمكن أيضًا مراجعة المحتوى في هذه الفئة بواسطة:" review_group_name: "اسم المجموعة" require_topic_approval: "طلب موافقة المشرف على جميع الموضوعات الجديدة" require_reply_approval: "طلب موافقة المشرف على جميع الردود الجديدة" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index c36d656876..bb4b8c35a1 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -2951,7 +2951,6 @@ da: default_list_filter: "Standard Listefilter:" allow_badges_label: "Tillad at emblemer bliver tildelt i denne kategori" edit_permissions: "Redigér tilladelser" - reviewable_by_group: "Ud over personalet kan indholdet i denne kategori også gennemgås af:" review_group_name: "gruppe navn" require_topic_approval: "Kræv moderator godkendelse af alle nye emner" require_reply_approval: "Kræv moderator godkendelse af alle nye svar" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 87ee105f0d..7dc8579350 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -2590,7 +2590,6 @@ el: default_list_filter: "Προεπιλεγμένο φίλτρο λίστας:" allow_badges_label: "Να επιτρέπεται η απονομή παράσημων σε αυτή την κατηγορία" edit_permissions: "Επεξεργασία Δικαιωμάτων" - reviewable_by_group: "Εκτός από το προσωπικό, το περιεχόμενο αυτής της κατηγορίας μπορεί επίσης να αναθεωρηθεί από:" review_group_name: "όνομα ομάδας" require_topic_approval: "Απαιτήστε έγκριση συντονιστή για όλα τα νέα θέματα" require_reply_approval: "Απαιτήστε έγκριση συντονιστή για όλες τις νέες απαντήσεις" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2547c435d2..80b7b8fda1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -754,6 +754,8 @@ en: title: "Settings" allow_unknown_sender_topic_replies: "Allow unknown sender topic replies." allow_unknown_sender_topic_replies_hint: "Allows unknown senders to reply to group topics. If this is not enabled, replies from email addresses not already invited to the topic will create a new topic." + from_alias: "From Alias" + from_alias_hint: "Alias to use as the from address when sending group SMTP emails. Note this may not be supported by all mail providers, please consult your mail provider's documentation." mailboxes: synchronized: "Synchronized Mailbox" none_found: "No mailboxes were found in this email account." @@ -3359,7 +3361,7 @@ en: default_list_filter: "Default List Filter:" allow_badges_label: "Allow badges to be awarded in this category" edit_permissions: "Edit Permissions" - reviewable_by_group: "In addition to staff, content in this category can be also be reviewed by:" + reviewable_by_group: "In addition to staff, content in this category can also be reviewed by:" review_group_name: "group name" require_topic_approval: "Require moderator approval of all new topics" require_reply_approval: "Require moderator approval of all new replies" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index aa74ab52cc..cdbf2b7774 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -173,6 +173,7 @@ es: themes: default_description: "Por defecto" broken_theme_alert: "Tu sitio puede que no funcione porque el tema / componente %{theme} tiene errores. Desactívalo en %{path}." + broken_decorator_alert: "Puede que las publicaciones no se muestren correctamente porque uno de los decoradores de contenido de publicaciones de tu sitio está causando errores. Revisa la consola de desarrollo del navegador para más información." s3: regions: ap_northeast_1: "Asia-Pacífico (Tokio)" @@ -3071,7 +3072,7 @@ es: default_list_filter: "Filtro de lista por defecto:" allow_badges_label: "Permitir que se concedan insignias en esta categoría" edit_permissions: "Editar permisos" - reviewable_by_group: "Además del personal, el contenido de esta categoría también puede ser revisado por:" + reviewable_by_group: "Además del personal, los siguientes grupos también pueden revisar el contenido de esta categoría:" review_group_name: "nombre del grupo" require_topic_approval: "Requiere aprobación del moderador para todos los temas nuevos" require_reply_approval: "Requiere aprobación del moderador para todas las respuestas nuevas" diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index f6715004b1..504aa48878 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -2161,7 +2161,6 @@ et: default_list_filter: "Vaikimisi teemade filter:" allow_badges_label: "Luba selles foorumis autasustamist märgistega" edit_permissions: "Muuda kasutusõigusi" - reviewable_by_group: "Lisaks meeskonnaliikmetle saavad selle kategooria sisu üle vaadata ka:" review_group_name: "grupi nimi" require_topic_approval: "Nõua moderaatori heakskiitu kõikidele uutele teemadele" require_reply_approval: "Nõua moderaatori heakskiitu kõikidele uutele vastustele" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index cefdbe617b..a69e93b5a2 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -685,6 +685,7 @@ fa_IR: title: "تنظیمات" allow_unknown_sender_topic_replies: "پاسخ‌های موضوع فرستنده ناشناس را مجاز کنید." allow_unknown_sender_topic_replies_hint: "به فرستندگان ناشناس اجازه می‌دهد تا به موضوعات گروه پاسخ دهند. اگر این مورد فعال نباشد، پاسخ‌هایی از آدرس‌های ایمیلی که قبلاً به موضوع دعوت نشده‌اند، موضوع جدیدی ایجاد می‌کنند." + from_alias: "از نام مستعار" mailboxes: synchronized: "صندوق پستی همگام شده" none_found: "هیچ صندوق پستی در این حساب ایمیل یافت نشد." diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 2023530ff0..2754c9d528 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -2956,7 +2956,6 @@ fi: default_list_filter: "Oletusluettelosuodatin:" allow_badges_label: "Salli kunniamerkkien myöntäminen tällä alueella" edit_permissions: "Muokkaa oikeuksia" - reviewable_by_group: "Henkilökunnan lisäksi tämän alueen sisältöä voi käsitellä myös:" review_group_name: "ryhmän nimi" require_topic_approval: "Edellytä valvojan hyväksyntää kaikille uusille ketjuille" require_reply_approval: "Edellytä valvojan hyväksyntää kaikille uusille vastauksille" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 7fb29a47ab..7e27bb35c9 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -2953,7 +2953,6 @@ fr: default_list_filter: "Filtre de liste par défaut :" allow_badges_label: "Autoriser les badges à être accordés dans cette catégorie" edit_permissions: "Modifier les permissions" - reviewable_by_group: "En plus des responsables, le contenu de cette catégorie peut également être examiné par :" review_group_name: "nom du groupe" require_topic_approval: "Nécessiter l'approbation pour chaque nouveau sujet" require_reply_approval: "Nécessiter l'approbation pour chaque nouvelle réponse" diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 36640a9f85..99ff2f9390 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -2859,7 +2859,6 @@ gl: default_list_filter: "Filtro de listaxe predeterminado:" allow_badges_label: "Permitir adxudicar insignias nesta categoría" edit_permissions: "Editar permisos" - reviewable_by_group: "Ademais do equipo, o contido desta categoría tamén pode ser revisado por:" review_group_name: "nome do grupo" require_topic_approval: "Require a aprobación de todos os temas novos polo moderador" require_reply_approval: "Require a aprobación de todas as respostas novas polo moderador" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 3ccccbbd4d..44e3dc477b 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -414,7 +414,7 @@ he: uploaded: "הועלה!" pasting: "מדביק..." enable: "לאפשר" - disable: "לנטרל" + disable: "השבתה" continue: "המשך" undo: "לבטל פעולה" revert: "להחזיר" @@ -780,6 +780,8 @@ he: title: "הגדרות" allow_unknown_sender_topic_replies: "לאפשר תגובות לנושאים משולחים בלתי־ידועים." allow_unknown_sender_topic_replies_hint: "מאפשר לשולחים בלתי־ידועים להגיב לנושאים קבוצתיים. אם האפשרות מבוטלת, תגובות מכתובות דוא״ל שטרם הוזמנו לנושא תיצורנה נושא חדש." + from_alias: "כינוי מאת" + from_alias_hint: "כינוי בו יש להשתמש בתור כתוב מאת בעת שליחת הודעות SMTP קבוצתיות בדוא״ל. נא לשים לב שאולי חלק מספקי הדוא״ל לא תומכים בזה, נא להיעזר בתיעוד של ספק הדוא״ל שלך." mailboxes: synchronized: "תיבת דוא״ל מסונכרנת" none_found: "לא נמצאו תיבות דוא״ל בחשבון הדוא״ל הזה." @@ -919,7 +921,7 @@ he: description: "תקבלו התראה על כל פוסט חדש במסגרת כל הודעה, וסך התשובות יוצג." watching_first_post: title: "צפייה בפוסט הראשון" - description: "תקבל התראה עבור הודעות חדשות בקבוצה זו אבל לא לתגובות עליהן." + description: "תישלח אליך התראה על הודעות חדשות בקבוצה זו אבל לא לתגובות עליהן." tracking: title: "במעקב" description: "תישלח אליך התראה אם מישהו מזכיר את @שמך או עונה לך, ותופיע ספירה של תגובות חדשות." @@ -2067,6 +2069,11 @@ he: two: "על ידי אזכור %{group}, אתם עומדים ליידע %{count} אנשים – אתם בטוחים?" many: "על ידי אזכור %{group}, אתם עומדים ליידע %{count} אנשים – אתם בטוחים?" other: "על ידי אזכור %{group}, אתם עומדים ליידע %{count} אנשים – אתם בטוחים?" + cannot_see_mention: + category: "אזכרת את ‎@%{username} אבל לא תישלח התראה עקב העדר גישה לקטגוריה הזו. עליך להוסיף את מי שאוזכר לקבוצה שיש לה גישה לקטגוריה הזאת." + private: "אזכרת את ‎@%{username} אבל לא תישלח התראה עקב העדר גישה לצפייה בהודעה האישית הזאת. עליך להזמין את מי שאוזכר להודעה האישית הזאת." + muted_topic: "אזכרת את ‎@%{username} אבל לא תישלח התראה עקב השתקת הנושא הזה." + not_allowed: "אזכרת את ‎@%{username} אבל לא תישלח התראה כיוון שמי שאוזכר לא הוזמן לנושא הזה." here_mention: one: "אזכור של ‎@%{here} יודיע למשתמש %{count}, להמשיך?" two: "אזכור של ‎@%{here} יודיע ל־%{count} משתמשים, להמשיך?" @@ -2332,16 +2339,16 @@ he: clear_search: "פינוי החיפוש" sort_or_bulk_actions: "מיון או בחירת תוצאות במרוכז" result_count: - one: "תוצאה אחת עבור %{term}" - two: "%{count}%{plus} תוצאות עבור %{term}" - many: "%{count}%{plus} תוצאות עבור%{term}" + one: "תוצאה %{count} עבור %{term}" + two: "%{count}%{plus} תוצאות עבור %{term}" + many: "%{count}%{plus} תוצאות עבור %{term}" other: "%{count}%{plus} תוצאות עבור %{term}" title: "חיפוש" full_page_title: "חיפוש" no_results: "אין תוצאות." no_more_results: "לא נמצאו עוד תוצאות." post_format: "#%{post_number} מאת %{username}" - results_page: "חפש תוצאות עבור '%{term}'" + results_page: "תוצאות לחיפוש אחר ‚%{term}’" more_results: "יש עוד תוצאות. אנא צמצם את קריטריוני החיפוש." cant_find: "לא מצליחים למצוא את מה שחיפשתם?" start_new_topic: "אולי תפתחו נושא חדש?" @@ -2535,7 +2542,7 @@ he: title: "העברה לדואר נכנס" help: "החזרת הודעה לדואר נכנס" edit_message: - help: "ערוך פוסט ראשון של ההודעה" + help: "עריכת פוסט ראשון של ההודעה" title: "עריכה" defer: help: "סימון כלא נקראו" @@ -2776,7 +2783,7 @@ he: remove_banner: "הסרת נושא באנר" reply: title: "תגובה" - help: "התחל לערוך תגובה לנושא זה" + help: "להתחיל לכתוב תגובה לנושא זה" share: title: "שיתוף" extended_title: "שתף קישור" @@ -2812,7 +2819,7 @@ he: pin: "גרמו לנושא זה להופיע בראש קטגוריה %{categoryLink} עד" unpin: "הסרת נושא זה מראש הקטגוריה %{categoryLink}." unpin_until: "גרמו לנושא זה להופיע בראש הקטגוריה %{categoryLink} או המתינו עד %{until}." - pin_note: "משתמשים יכולים לבטל עצמאית את נעיצת הנושא עבורם בלבד." + pin_note: "משתמשים יכולים לנתק את הנושא עצמאית עבור עצמם." pin_validation: "דרוש תאריך על מנת לנעוץ את הנושא." not_pinned: "אין נושאים שננעצו בקטגוריה %{categoryLink}." already_pinned: @@ -3082,7 +3089,7 @@ he: undo_like: "בטל 'אהוב'" edit: "עירכו פוסט זה" edit_action: "עריכה" - edit_anonymous: "מצטערים, אך עליכם להיות מחוברים בכדי לערוך פוסט זה." + edit_anonymous: "עליך להיכנס כדי לערוך פוסט זה, עמך הסליחה." flag: "דגלו פוסט זה באופן פרטי לתשומת לב או שלחו התראה פרטית עליו" delete: "מחק פוסט זה" undelete: "שחזר פוסט זה" @@ -3297,7 +3304,7 @@ he: inherited: 'הרשאה זו התקבלה בירושה מתוך „כולם”' special_warning: "אזהרה: קטגוריה זו הגיעה מראש והגדרות האבטחה שלה אינן ניתנות לשינוי. אם אתם מעוניינים להשתמש בקטגוריה זו, מחקו אותה במקום להשתמש בה מחדש." uncategorized_security_warning: "קטגוריה זו היא מיוחדת. היא מיועדת להחזקת מגוון של נושאים שאין להם קטגוריה, לא יכולות להיות לקבוצה זו הגדרות אבטחה." - uncategorized_general_warning: 'קטגוריה זו היא מיוחדת. היא משמשת כקטגוריית בררת המחדל לנושאים חדשים שלא נבחרה עבורם קטגוריה. אם ברצונך למנוע את ההתנהגות הזאת ולאלץ בחירת קטגוריה, נא לנטרל את ההגדרה הזאת כאן. אם מעניין אותך לשנות את השם או את התיאור, עליך לגשת אל התאמה אישית / תוכן טקסט.' + uncategorized_general_warning: 'קטגוריה זו היא מיוחדת. היא משמשת כקטגוריית בררת המחדל לנושאים חדשים שלא נבחרה עבורם קטגוריה. כדי למנוע את ההתנהגות הזאת ולאלץ בחירת קטגוריה, נא להשבית את ההגדרה הזאת כאן. אם מעניין אותך לשנות את השם או את התיאור, עליך לגשת אל התאמה אישית / תוכן טקסט.' pending_permission_change_alert: "לא הוספת %{group} לקטגוריה הזאת, יש ללחוץ על הכפתור הזה כדי להוסיף אותן." images: "תמונות" email_in: "כתובת דואר נכנס מותאמת אישית:" @@ -5158,7 +5165,7 @@ he: send_activation_email_failed: "הייתה בעיה בשליחת הודעת האישור. %{error}" activate: "הפעלת חשבון" activate_failed: "הייתה בעיה בהפעלת המשתמש." - deactivate_account: "נטרל חשבון" + deactivate_account: "השבתת חשבון" deactivate_failed: "הייתה בעיה בהשבתת חשבון המשתמש." unsilence_failed: "אירעה תקלה בעת ביטול השתקת המשתמש." silence_failed: "אירעה תקלה בעת השתקת המשתמש." @@ -5168,7 +5175,7 @@ he: reset_bounce_score: label: "איפוס" title: "איפוס ניקוד-החזר" - visit_profile: "בקרו בדף ההעדפות של משתמש/ת זה/זו כדי לערוך את הפרופיל שלהם" + visit_profile: "ביקור בדף ההעדפות של אותו המשתמש מאפשר את עריכת הפרופיל" deactivate_explanation: "חשבון משתמש מושבת נדרש לאמת דוא״ל מחדש." suspended_explanation: "משתמש מושעה לא יכול להיכנס." silence_explanation: "משתמש מושתק לא יכול לפרסם או לפתוח נושאים." @@ -5271,9 +5278,9 @@ he: multiselect: "מגוון בחירות" site_text: description: "ניתן להתאים כל טקסט בפורום שלך. נא להתחיל בחיפוש שלהלן:" - search: "חפשו טקסט שברצונכם לערוך" + search: "חיפוש הטקסט שמיועד לעריכה" title: "טקסט" - edit: "ערוך" + edit: "עריכה" revert: "ביטול שינויים" revert_confirm: "לבטל את השינויים שלך?" go_back: "חזרה לחיפוש" @@ -5457,7 +5464,7 @@ he: host: "שרתים מורשים" class_name: "שם מחלקה" allowed_paths: "רשימת נתיבים מותרים" - edit: "ערוך" + edit: "עריכה" category: "פרסם לקטגוריה" add_host: "הוספת שרת" settings: "הגדרות הטמעה" diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index b3740c40c3..95e7156e77 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -689,6 +689,7 @@ hu: title: "Beállítások" allow_unknown_sender_topic_replies: "Engedélyezze az ismeretlen feladóktól érkező témaválaszokat." allow_unknown_sender_topic_replies_hint: "Lehetővé teszi az ismeretlen feladók számára, hogy válaszoljanak a csoport témáira. Ha ez nincs engedélyezve, akkor az e-mail szálban még nem szereplő címekről érkező válaszok új témát hoznak létre." + from_alias: "Álnévből" mailboxes: synchronized: "Szinkronizált postafiók" none_found: "Ebben az e-mail fiókban nem találhatók postaládák." @@ -2834,6 +2835,7 @@ hu: default_view: "Alapértelmezett témakörök" allow_badges_label: "Kitűzök elnyerésének engedélyezése ebben a kategóriában" edit_permissions: "Jogok szerkesztése" + reviewable_by_group: "Az ebbe a kategóriába tartozó tartalmakat a stábon kívül a következők is jóváhagyhatják:" review_group_name: "csoport neve" this_year: "ez az év" position: "Kategóriák oldalon elfoglalt pozíció:" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 071cad8bfe..f6f82f9a01 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -173,6 +173,7 @@ it: themes: default_description: "Predefinito" broken_theme_alert: "Il tuo sito potrebbe non funzionare perché il tema / componente %{theme} contiene degli errori. Disabilitalo qui %{path}." + broken_decorator_alert: "I messaggi potrebbero non essere visualizzati correttamente perché uno dei decoratori di contenuti sul tuo sito ha generato un errore. Per maggiori informazioni, controlla gli strumenti di sviluppo del browser." s3: regions: ap_northeast_1: "Asia Pacifico (Tokyo)" @@ -689,6 +690,7 @@ it: title: "Impostazioni" allow_unknown_sender_topic_replies: "Consenti a mittenti sconosciuti di rispondere agli argomenti." allow_unknown_sender_topic_replies_hint: "Consenti a mittenti sconosciuti di rispondere agli argomenti del gruppo. Se l'opzione è disabilitata, le risposte provenienti da indirizzi e-mail non invitati all'argomento causeranno la creazione di un nuovo argomento in cui saranno incluse." + from_alias: "Da Alias" mailboxes: synchronized: "Maibox sincronizzata" none_found: "Nessuna mailbox trovata in questo account e-mail." @@ -1482,13 +1484,16 @@ it: show_advanced: "Mostra Opzioni Avanzate" hide_advanced: "Nascondi opzioni avanzate" restrict: "Limita a" + restrict_email: "Limita all'e-mail" restrict_domain: "Limita al dominio" email_or_domain_placeholder: "nome@esempio.com o esempio.com" max_redemptions_allowed: "Limite max di utilizzi" add_to_groups: "Aggiungi ai gruppi" + invite_to_topic: "Arriva all'argomento" expires_at: "Scadenza:" custom_message: "Messaggio personale facoltativo" send_invite_email: "Salva e invia Email" + send_invite_email_instructions: "Limita l'invito all'e-mail per inviare un'e-mail di invito" save_invite: "Salva invito" invite_saved: "Invito salvato." bulk_invite: @@ -1685,10 +1690,12 @@ it: disclaimer: "Registrandoti accetti Informativa sulla privacy e termini del servizio." title: "Crea il tuo account" failed: "Qualcosa non ha funzionato. Forse questa email è già registrata, prova a usare il link di recupero password" + associate: "Hai già un account? Accedi per collegare il tuo account %{provider}." forgot_password: title: "Reimposta Password" action: "Ho dimenticato la password" invite: "Inserisci il nome utente o l'indirizzo email. Ti manderemo un'email per reimpostare la password." + invite_no_username: "Inserisci il tuo indirizzo di posta elettronica e ti spediremo un'e-mail per reimpostare la password." reset: "Reimposta Password" complete_username: "Se un account corrisponde al nome utente %{username}, a breve dovresti ricevere un'email con le istruzioni per ripristinare la tua password." complete_email: "Se un account corrisponde a %{email}, a breve dovresti ricevere un'email contenente le istruzioni per ripristinare la password." @@ -1762,21 +1769,27 @@ it: google_oauth2: name: "Google" title: "con Google" + sr_title: "Accedi con Google" twitter: name: "Twitter" title: "con Twitter" + sr_title: "Accedi con Twitter" instagram: name: "Instagram" title: "con Instagram" + sr_title: "Accedi con Instagram" facebook: name: "Facebook" title: "con Facebook" + sr_title: "Accedi con Facebook" github: name: "GitHub" title: "con GitHub" + sr_title: "Accedi con GitHub" discord: name: "Discord" title: "con Discord" + sr_title: "Accedi con Discord" second_factor_toggle: totp: "Utilizzare un'app di autenticazione" backup_code: "Utilizza un codice di backup" @@ -3026,7 +3039,7 @@ it: default_list_filter: "Filtro predefinito della lista:" allow_badges_label: "Permetti l'assegnazione di distintivi in questa categoria" edit_permissions: "Modifica Permessi" - reviewable_by_group: "Oltre allo staff, i contenuti di questa categoria possono essere esaminati da:" + reviewable_by_group: "Oltre che dal personale, i contenuti di questa categoria possono essere revisionati anche da:" review_group_name: "nome gruppo" require_topic_approval: "Richiedi l'approvazione di un moderatore per tutti i nuovi argomenti" require_reply_approval: "Richiedi l'approvazione di un moderatore per tutte le nuove risposte" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index e326491dad..015a753c73 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -2886,7 +2886,6 @@ ja: default_list_filter: "デフォルトのリストのフィルタ:" allow_badges_label: "このカテゴリでバッジの付与を許可する" edit_permissions: "権限を編集" - reviewable_by_group: "このカテゴリのコンテンツはスタッフのほか、次のユーザーもレビューできる:" review_group_name: "グループ名" require_topic_approval: "すべての新しいトピックにモデレーターの承認を必要とする" require_reply_approval: "すべての新しい返信にモデレーターの承認を必要とする" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index cc281b0f28..f0aa11e425 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -2918,7 +2918,6 @@ ko: default_list_filter: "목록 기본 필터:" allow_badges_label: "배지가 이 카테고리에서 주어질 수 있도록 허용" edit_permissions: "권한 수정" - reviewable_by_group: "이 카테고리의 콘텐츠는 관리자 외에도 다음과 같은 방법으로 검토할 수 있습니다." review_group_name: "그룹명" require_topic_approval: "모든 새 글에 대한 관리자 승인 필요" require_reply_approval: "모든 새 댓글의 관리자 승인 필요" diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index b1781dad16..90ba3bde66 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -2898,7 +2898,6 @@ lt: default_list_filter: "Numatytasis sąrašo filtras:" allow_badges_label: "Leisti trofėjų apdovanojimus šioje kategorijoje" edit_permissions: "Redaguoti leidimus" - reviewable_by_group: "Be darbuotojų, šios kategorijos turinį taip pat gali peržiūrėti:" review_group_name: "grupės pavadinimas" require_topic_approval: "Reikalauti, kad moderatorius patvirtintų visas naujas temas" require_reply_approval: "Reikalauti, kad moderatorius patvirtintų visus naujus atsakymus" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index b746a61132..8d2099c08d 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -2910,7 +2910,6 @@ nb_NO: default_list_filter: "Standard listefilter:" allow_badges_label: "Tillat merker å bli tildelt i denne kategorien" edit_permissions: "Rediger tillatelser" - reviewable_by_group: "I tillegg til de ansatte, kan innholdet i denne kategorien også gjennomgås ved å:" review_group_name: "gruppenavn" require_topic_approval: "Krev godkjennelse fra moderator for alle nye emner" require_reply_approval: "Krev godkjennelse fra moderator for alle nye svar" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 9dc5da0738..42ecdae32a 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -2970,7 +2970,6 @@ nl: default_list_filter: "Standaard lijstfilter:" allow_badges_label: "Badges laten toekennen in deze categorie" edit_permissions: "Toestemmingen bewerken" - reviewable_by_group: "Naast stafleden kan inhoud in deze categorie ook worden beoordeeld door:" review_group_name: "groepsnaam" require_topic_approval: "Goedkeuring van moderator voor alle nieuwe topics vereisen" require_reply_approval: "Goedkeuring van moderator voor alle nieuwe antwoorden vereisen" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index b3e6699ab6..ef555640ce 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -219,6 +219,7 @@ pl_PL: themes: default_description: "Domyślny" broken_theme_alert: "Twoja strona może nie działać, bo motyw / komponent %{theme} zawiera błędy. Wyłącz go w %{path}." + broken_decorator_alert: "Posty mogą nie wyświetlać się poprawnie, ponieważ jeden z dekoratorów treści postów w Twojej witrynie zgłosił błąd. Sprawdź narzędzia programistyczne przeglądarki, aby uzyskać więcej informacji." s3: regions: ap_northeast_1: "Azja i Pacyfik (Tokio)" @@ -1250,6 +1251,8 @@ pl_PL: warnings: "Oficjalne ostrzeżenia" read_more_in_group: "Chcesz przeczytać więcej? Przeglądaj inne wiadomości w %{groupLink}." read_more: "Chcesz przeczytać więcej? Przeglądaj inne wiadomości w wiadomościach osobistych." + read_more_group_pm_MF: "Masz do zobaczenia { UNREAD, plural, =0 {} one { # nieprzeczytaną wiadomość } few { # nieprzeczytane wiadomości } many { # nieprzeczytanych wiadomości } other { # nieprzeczytanych wiadomości } } { NEW, plural, =0 {} one { {BOTH, select, true{i } false {} other{}} # nową wiadomość } few {} many {} other { {BOTH, select, true{i } false {are } other{}} # nowych wiadomości} }lub przeglądaj inne wiadomości w {groupLink}" + read_more_personal_pm_MF: "Masz do zobaczenia { UNREAD, plural, =0 {} one { # nieprzeczytaną wiadomość } few { # nieprzeczytane wiadomości } many { # nieprzeczytanych wiadomości } other { # nieprzeczytanych } } { NEW, plural, =0 {} one { {BOTH, select, true{i } false {} other{}} # nową wiadomość} few {} many {} other { {BOTH, select, true{and } false {are } other{}} # nowych wiadomości} } lub przeglądaj inne wiadomości osobiste" preferences_nav: account: "Konto" security: "Bezpieczeństwo" @@ -1503,9 +1506,11 @@ pl_PL: every_month: "każdego miesiąca" every_six_months: "co 6 miesięcy" email_level: + title: "Wyślij do mnie e-maila, gdy jestem cytowany, odpowiadam, pojawia się moja @nazwa_użytkownika lub gdy pojawia się nowa aktywność w moich obserwowanych kategoriach, tagach lub tematach" always: "zawsze" only_when_away: "tylko kiedy jest daleko" never: "nigdy" + email_messages_level: "Wyślij do mnie e-mail, gdy otrzymam wiadomość osobistą" include_tl0_in_digests: "Dołącz treści od nowych użytkowników w e-mailach podsumowujących." email_in_reply_to: "Zawieraj w e-mailach fragment oryginalnych postów z odpowiedzi" other_settings: "Inne" @@ -1604,6 +1609,7 @@ pl_PL: expires_at: "Wygasa po" custom_message: "Opcjonalna wiadomość osobista" send_invite_email: "Zapisz i wyślij e-mail" + send_invite_email_instructions: "Ogranicz zaproszenie do emaila, aby wysłać email z zaproszeniem" save_invite: "Zapisz zaproszenie" invite_saved: "Zaproszenie zapisane." bulk_invite: @@ -2061,6 +2067,16 @@ pl_PL: few: "Wspominając %{group}, powiadomisz %{count} osoby – czy jesteś pewien?" many: "Wspominając %{group}, powiadomisz %{count} osób – czy jesteś pewien?" other: "Wspominając %{group}, powiadomisz %{count} osób –czy jesteś pewien?" + cannot_see_mention: + category: "Wspomniano o @%{username}, lecz nie zostannie on powiadomiony, ponieważ nie ma dostępu do tej kategorii. Będziesz musiał dodać go do grupy, która ma dostęp do tej kategorii." + private: "Wspomniano o @%{username}, lecz nie zostannie on powiadomiony, ponieważ nie może zobaczyć tej wiadomości osobistej. Będziesz musiał zaprosić go do tej wiadomości osobistej." + muted_topic: "Wspomniałeś o @%{username}, lecz nie zostanie on powiadomiony, ponieważ wyciszył ten temat." + not_allowed: "Wspomniałeś o @%{username}, lecz nie zostanie on powiadomiony, ponieważ nie został zaproszony do tego tematu." + here_mention: + one: "Wspominając @%{here}, masz zamiar powiadomić %{count} użytkownika – czy jesteś pewien?" + few: "Wspominając @%{here}, masz zamiar powiadomić %{count} użytkowników – czy jesteś pewien?" + many: "Wspominając @%{here}, masz zamiar powiadomić %{count} użytkowników – czy jesteś pewien?" + other: "Wspominając @%{here}, masz zamiar powiadomić %{count} użytkowników – czy jesteś pewien?" 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?" reference_topic_title: "Odpowiedz: %{title}" error: @@ -2253,6 +2269,11 @@ pl_PL: reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - zakończone" dismiss_confirmation: + body: + one: "Jesteś pewny? Masz %{count} ważne powiadomienie." + few: "Jesteś pewny? Masz %{count} ważne powiadomienia." + many: "Jesteś pewny? Masz %{count} ważnych powiadomień." + other: "Jesteś pewny? Masz %{count} ważnych powiadomień." dismiss: "Odrzuć" cancel: "Anuluj" group_message_summary: @@ -2342,6 +2363,8 @@ pl_PL: in_topics_posts: "we wszystkich tematach i postach" enter_hint: "lub naciśnij Enter" in_posts_by: "w postach %{username}" + browser_tip: "%{modifier} + f" + browser_tip_description: "ponownie, aby użyć natywnego wyszukiwania w przeglądarce" recent: "Ostatnie wyszukiwania" clear_recent: "Wyczyść ostatnie wyszukiwania" type: @@ -2781,6 +2804,11 @@ pl_PL: help: "udostępnij odnośnik do tego tematu" instructions: "Udostępnij link do tego tematu:" copied: "Link do tematu skopiowany." + restricted_groups: + one: "Widoczne tylko dla członków grupy: %{groupNames}" + few: "Widoczne tylko dla członków grup: %{groupNames}" + many: "Widoczne tylko dla członków grup: %{groupNames}" + other: "Widoczne tylko dla członków grup: %{groupNames}" notify_users: title: "Powiadom" instructions: "Powiadom następujących użytkowników o tym temacie:" @@ -3311,7 +3339,7 @@ pl_PL: default_list_filter: "Domyślny filtr listy:" allow_badges_label: "Włącz przyznawanie odznak na podstawie aktywności w tej kategorii" edit_permissions: "Edytuj uprawnienia" - reviewable_by_group: "Oprócz personelu, treści w tej kategorii mogą być również przeglądane przez:" + reviewable_by_group: "Oprócz personelu, treści w tej kategorii mogą również przeglądać:" review_group_name: "nazwa grupy" require_topic_approval: "Wymagaj zatwierdzenia moderatora dla wszystkich nowych tematów" require_reply_approval: "Wymagaj zatwierdzenia moderatora dla wszystkich nowych odpowiedzi" @@ -3698,6 +3726,7 @@ pl_PL: title: "Menu wyszukiwania" prev_next: "%{shortcut} Przesuń zaznaczenie w górę i w dół" insert_url: "%{shortcut} Wstaw zaznaczenie do otwartego edytora" + full_page_search: "%{shortcut} Uruchamia wyszukiwanie na całej stronie" badges: earned_n_times: one: "Otrzymano tę odznakę %{count} raz" @@ -3761,12 +3790,18 @@ pl_PL: changed: "zmienione tagi:" tags: "Tagi" choose_for_topic: "tagi opcjonalne" + choose_for_topic_required: + one: "wybierz co najmniej %{count} tag..." + few: "wybierz co najmniej %{count} tagi..." + many: "wybierz co najmniej %{count} tagów..." + other: "wybierz co najmniej %{count} tagów..." info: "Informacje" default_info: "Ten tag nie jest ograniczony do żadnych kategorii i nie ma synonimów." staff_info: "Aby dodać ograniczenia, umieść ten znacznik w grupie znaczników." category_restricted: "Ten tag jest ograniczony do kategorii, do których nie masz uprawnień dostępu." synonyms: "Synonimy" synonyms_description: "W przypadku użycia następujących tagów zostaną one zastąpione przez %{base_tag_name}." + save: "Zapisz nazwę i opis tagu" tag_groups_info: one: 'Ten tag należy do grupy "%{tag_groups}”.' few: "Ten tag należy do następujących grup: %{tag_groups}." @@ -4134,6 +4169,7 @@ pl_PL: no_description: (brak opisu) all_api_keys: Wszystkie klucze API user_mode: Poziom użytkownika + scope_mode: Zakres impersonate_all_users: Podszywanie się pod dowolnego użytkownika single_user: "Pojedynczy użytkownik" user_placeholder: Wprowadź nazwę użytkownika @@ -4149,7 +4185,9 @@ pl_PL: Korzystanie z interfejsu API o określonym zakresie umożliwia bardziej szczegółowe dostosowywanie uprawnień. Możesz określić, które parametry będą dozwolone. Użyj przecinków, aby oddzielić wiele wartości. title: Zakresy + granular: Ograniczony read_only: Tylko do odczytu + global: Globalny global_description: Klucz API nie ma ograniczeń i wszystkie punkty końcowe są dostępne. resource: Zasób action: Akcja @@ -4167,6 +4205,8 @@ pl_PL: wordpress: Niezbędne do działania wtyczki wp-discourse na WordPress. posts: edit: Edytuj dowolny post lub konkretny. + uploads: + create: Prześlij nowy plik lub zainicjuj pojedyncze lub wieloczęściowe bezpośrednie wysyłanie do zewnętrznego magazynu. users: bookmarks: Lista zakładek użytkownika. Zwraca przypomnienia o zakładkach podczas korzystania z formatu ICS. sync_sso: Zsynchronizuj użytkownika za pomocą DiscourseConnect. @@ -4180,9 +4220,13 @@ pl_PL: email: receive_emails: Połącz ten zakres z odbiorcą poczty w celu przetwarzania przychodzących wiadomości e-mail. badges: + create: Utwórz nową odznakę. + show: Uzyskaj informacje o odznace. update: Zaktualizuj odznakę. delete: Usuń odznakę. list_user_badges: Lista odznak użytkownika. + assign_badge_to_user: Przypisz odznakę użytkownikowi. + revoke_badge_from_user: Odbierz odznakę użytkownikowi. web_hooks: title: "Webhooki" none: "Brak webhooków" @@ -4555,6 +4599,7 @@ pl_PL: Zdecydowanie zalecamy dodawanie przedrostków do nazw zmiennych, aby uniknąć konfliktów z wtyczkami i/lub silnikiem forum. head_tag: + text: "Nagłówek" title: "HTML, który zostanie wstawiony przed tagiem head" body_tag: text: "Body" @@ -5419,6 +5464,7 @@ pl_PL: grant_existing_holders: Przyznaj dodatkowe odznaki obecnym posiadaczom odznak emoji: title: "Emoji" + help: "Dodaj nowe emotikony, które będą dostępne dla wszystkich. Przeciągnij i upuść wiele plików jednocześnie bez wprowadzania nazwy, aby utworzyć emotikony za pomocą ich nazw plików. Wybrana grupa zostanie użyta dla wszystkich plików, które są dodawane w tym samym czasie. Możesz także kliknąć przycisk „Dodaj nową emotikonę”, aby otworzyć selektor plików." add: "Dodaj nowe Emoji" choose_files: "Wybierz pliki" uploading: "Przesyłanie…" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index bf41186220..c16f88cd46 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -3074,7 +3074,6 @@ pt_BR: default_list_filter: "Filtro de lista padrão:" allow_badges_label: "Permitir a concessão de emblemas nesta categoria" edit_permissions: "Editar permissões" - reviewable_by_group: "Além da equipe, o conteúdo desta categoria também pode ser revisado por:" review_group_name: "nome do grupo" require_topic_approval: "Requer aprovação dos moderadores(as) de todos os novos tópicos" require_reply_approval: "Requer aprovação dos moderadores(as) de todas as novas respostas" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index d5d5b5724c..2016201509 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -780,6 +780,8 @@ ru: title: "Настройки" allow_unknown_sender_topic_replies: "Разрешать в теме ответы от неизвестного отправителя." allow_unknown_sender_topic_replies_hint: "Разрешать неизвестным отправителям отвечать в групповой теме. Если этот параметр не включен, ответы с адресов электронной почты пользователей, не приглашенных в тему, создадут новую тему." + from_alias: "Псевдоним отправителя" + from_alias_hint: "Псевдоним для использования в качестве адреса отправителя групповых SMTP-писем. Обратите внимание, что этот функционал может не поддерживаться всеми почтовыми провайдерами, пожалуйста, обратитесь к документации вашего почтового провайдера." mailboxes: synchronized: "Синхронизированный почтовый ящик" none_found: "В этой учётной записи электронной почты не найдено ни одного почтового ящика." @@ -3340,7 +3342,7 @@ ru: default_list_filter: "Фильтр по умолчанию:" allow_badges_label: "Разрешить вручение наград в этом разделе" edit_permissions: "Изменить права доступа" - reviewable_by_group: "Кто ещё, помимо персонала, может модерировать содержимое этого раздела:" + reviewable_by_group: "Помимо сотрудников, содержимое этого раздела также могут модерировать:" review_group_name: "Название группы" require_topic_approval: "Требовать одобрения модератором всех новых тем" require_reply_approval: "Требовать одобрения модератором всех новых ответов" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 3399fe1cce..e9115c1d26 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -3074,7 +3074,7 @@ sv: default_list_filter: "Standardlistfilter:" allow_badges_label: "Tillåt utmärkelser i den här kategorin" edit_permissions: "Redigera behörigheter" - reviewable_by_group: "Förutom av personal kan innehåll i denna kategori också granskas av:" + reviewable_by_group: "Förutom personal kan innehåll i denna kategori också granskas av:" review_group_name: "gruppnamn" require_topic_approval: "Kräv att en moderator godkänner alla nya ämnen" require_reply_approval: "Kräv att en moderator godkänner alla nya svar" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 7ab6c7a14c..dc7c064e15 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -778,7 +778,7 @@ tr_TR: title: one: "Gruplar" other: "Gruplar" - activity: "Aktivite" + activity: "Etkinlik" members: title: "Üyeler" filter_placeholder_admin: "kullanıcı adı veya e-posta" @@ -971,7 +971,7 @@ tr_TR: saturday: "Cumartesi" sunday: "Pazar" to: "kime" - activity_stream: "Hareketler" + activity_stream: "Etkinlik" read: "Okunmuş" read_help: "Son okunan konular" preferences: "Tercihler" @@ -1058,12 +1058,12 @@ tr_TR: suspended_permanently: "Bu kullanıcı beklemeye alındı." suspended_reason: "Sebep: " github_profile: "Github" - email_activity_summary: "Aktivite özeti" + email_activity_summary: "Etkinlik Özeti" mailing_list_mode: label: "Gönderi listesi modu" enabled: "Gönderi listesi modunu etkinleştir" instructions: | - Bu ayar aktivite özetini geçersiz kılar.
+ Bu ayar etkinlik özetini geçersiz kılar.
Sessize alınmış konular ve kategoriler bu e-postalarda yer almaz. individual: "Her yeni gönderi için bir e-posta gönder" individual_no_echo: "Kendi gönderilerim haricindeki her gönderi için e-posta gönder" @@ -1693,6 +1693,7 @@ tr_TR: title: "Parola Sıfırlama" action: "Parolamı unuttum" invite: "Kullanıcı adınızı veya e-posta adresinizi girin, size bir parola sıfırlama e-postası gönderelim." + invite_no_username: "E-posta adresinizi girin, size bir şifre sıfırlama e-postası göndereceğiz." reset: "Parola Sıfırlama" complete_username: "%{username} kullanıcı adı ile eşleşen bir hesap bulunması durumunda, kısa bir süre içerisinde parolanızı nasıl sıfırlayacağınızı açıklayan bir e-posta alacaksınız." complete_email: "%{email} adresi ile eşleşen bir hesap bulunması durumunda, kısa bir süre içerisinde parolanızı nasıl sıfırlayacağınızı açıklayan bir e-posta alacaksınız." @@ -2031,6 +2032,8 @@ tr_TR: desc: "Son yanıt tarihini değiştirmeden yanıtla" reload: "Yeniden Yükle" ignore: "Yoksay" + image_alt_text: + aria_label: Resim için alternatif metin notifications: tooltip: regular: @@ -2169,6 +2172,7 @@ tr_TR: enter_hint: "veya Enter tuşuna basın" in_posts_by: "@%{username} kullancısına ait gönderilerde" browser_tip: "%{modifier} + f" + browser_tip_description: "yeniden yerel tarayıcı aramasını kullanmak için" recent: "Son Aramalar" clear_recent: "Son Aramaları Temizle" type: @@ -2255,7 +2259,7 @@ tr_TR: hamburger_menu: "menü" new_item: "yeni" go_back: "geri dön" - not_logged_in_user: "güncel aktivitelerin ve tercihlerin özetinin bulunduğu kullanıcı sayfası" + not_logged_in_user: "güncel etkinliğin ve tercihlerin özetinin bulunduğu kullanıcı sayfası" current_user: "kendi kullanıcı sayfana git" view_all: "tümünü görüntüle %{tab}" topics: @@ -2318,7 +2322,7 @@ tr_TR: top: "Popüler bir konu yok." educate: new: '

Yeni konularınız burada görünecektir. Varsayılan olarak, konular yeni kabul edilir ve son 2 gün içinde oluşturulmuşlarsa göstergesi gösterilir.

Bunu değiştirmek için tercihinizi ziyaret edin.

' - unread: '

Okunmamış konularınız burada görünür.

Konular varsayılan olarak okunmamış sayılır ve aşağıdaki durumlarda okunmamış ileti sayısı 1 gösterilir:

  • Konuyu siz oluşturmuşsanız
  • Konuyu yanıtlamışsanız
  • Konuyu 4 dakikadan fazla okumuşsanız

Konuyu her konunun altında bulunan bildirim kontrol alanından Gözleniyor veya Takip Ediliyor olarak seçmişseniz.

Tercihler sayfanızdan bunu değiştirebilirsiniz.

' + unread: '

Okunmamış konularınız burada görünür.

Konular varsayılan olarak okunmamış sayılır ve aşağıdaki durumlarda okunmamış ileti sayısı 1 gösterilir:

  • Konuyu siz oluşturmuşsanız
  • Konuyu yanıtlamışsanız
  • Konuyu 4 dakikadan fazla okumuşsanız

Konuyu her konunun altında bulunan bildirim denetim alanından İzleniyor veya Takip Ediliyor olarak seçmişseniz.

Tercihler sayfanızdan bu ayarları değiştirebilirsiniz.

' bottom: latest: "Daha fazla güncel konu yok." posted: "Daha fazla gönderilmiş konu yok." @@ -2808,6 +2812,7 @@ tr_TR: edit: "Üzgünüz, gönderiniz düzenlenirken bir hata oluştu. Lütfen yeniden deneyin." upload: "Üzgünüz, dosya yüklenirken bir hata oluştu. Lütfen yeniden deneyin." file_too_large: "Üzgünüz, bu dosya çok büyük (en fazla %{max_size_kb}kb). Neden paylaşımını bir bulut sağlayıcısına yükleyip bağlantısını paylaşmıyorsun ?" + file_too_large_humanized: "Üzgünüz, bu dosya çok büyük (en fazla %{max_size} olabilir). Neden dosyanı bir bulut sağlayıcısına yükleyip bağlantısını paylaşmıyorsun?" too_many_uploads: "Üzgünüz, aynı anda yalnızca tek dosya yüklenebilir." too_many_dragged_and_dropped_files: one: "Üzgünüz, tek seferde yalnızca %{count} dosya yükleyebilirsin." @@ -3053,7 +3058,7 @@ tr_TR: default_list_filter: "Varsayılan Liste Filtresi:" allow_badges_label: "Bu kategoride rozetle ödüllendirilmesine izin ver" edit_permissions: "İzinleri Düzenle" - reviewable_by_group: "Personele ek olarak, bu kategorideki içerik şu kişiler tarafından da incelenebilir:" + reviewable_by_group: "Personele ek olarak, bu kategorideki içerik şu kişiler tarafından da gözden geçirilebilir:" review_group_name: "grup adı" require_topic_approval: "Tüm yeni konular moderatör onayını gerektirir" require_reply_approval: "Tüm yeni yanıtlar moderatör onayı gerektirir" @@ -3099,7 +3104,7 @@ tr_TR: op_likes: "Orijinal Gönderi Beğenileri" views: "Görüntüleme" posts: "Gönderiler" - activity: "Aktivite" + activity: "Etkinlik" posters: "Posterler" category: "Kategori" created: "Oluşturulan" @@ -3222,7 +3227,7 @@ tr_TR: views_long: one: "bu konu %{count} defa görüntülendi" other: "bu konu %{number} defa görüntülendi" - activity: "Aktivite" + activity: "Etkinlik" likes: "Beğeni" likes_lowercase: one: "beğeni" @@ -3611,8 +3616,8 @@ tr_TR: pick_files_button: unsupported_file_picked: "Desteklenmeyen bir dosya seçtiniz. Desteklenen dosya türleri şunlardır– %{types}." user_activity: - no_activity_title: "Henüz aktivite yok" - no_activity_others: "Aktivite yok." + no_activity_title: "Henüz etkinlik yok" + no_activity_others: "Etkinlik yok." no_replies_title: "Henüz hiçbir konuyu yanıtlamadınız." no_replies_others: "Yanıt yok." no_drafts_title: "Hiç taslak başlatmadınız" @@ -3680,7 +3685,7 @@ tr_TR: community_health: Topluluk sağlığı moderators_activity: Moderatörlerin etkinliği whats_new_in_discourse: Discourse'daki yenilikler neler? - activity_metrics: Aktivite Ölçütleri + activity_metrics: Etkinlik Ölçütleri all_reports: "Tüm raporlar" general_tab: "Genel" moderation_tab: "Moderasyon" @@ -3830,6 +3835,7 @@ tr_TR: Kapsamları kullanırken, bir API anahtarını yalnızca belirli bir endpoint grubuyla kullanılacak şekilde kısıtlayabilirsiniz. Hangi parametrelere izin verileceğini de tanımlayabilirsiniz. Birden çok değer kullanacaksanız bunları ayırmak için virgül kullanın. title: Kapsamlar read_only: Salt okunur + global_description: API anahtarında herhangi bir kısıtlama yok ve tüm uç noktalar erişilebilir durumda. resource: Kaynak action: Eylem allowed_parameters: İzin Verilen Parametreler @@ -4718,7 +4724,7 @@ tr_TR: flags_received_none: "Bu kullanıcı herhangi bir bayrak almadı." reputation: İtibar permissions: İzinler - activity: Aktivite + activity: Etkinlik like_count: Verilen / Alınan Beğeniler last_100_days: "son 100 günde" private_topics_count: Özel Konular diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index f335f14ef9..1a3639498c 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -172,6 +172,7 @@ uk: url: "Скопіювати та поширити посилання" action_codes: public_topic: "робить цю тему публічною %{when}" + open_topic: "Перетворив це на тему %{when}" private_topic: "робить цю тему особистим повідомленням %{when}" split_topic: "розділив цю тему %{when}" invited_user: "запросив %{who} %{when}" @@ -218,6 +219,7 @@ uk: themes: default_description: "Промовчання" broken_theme_alert: "Ваш сайт може не працювати, тому що оформлення/компонент %{theme} містить помилки. Вимкніть його тут: %{path}." + broken_decorator_alert: "Повідомлення можуть показуватися неправильно, тому що один з декораторів вмісту публікації на вашому сайті викликав помилку. Перевірте інструменти розробника браузера для отримання додаткової інформації." s3: regions: ap_northeast_1: "Азія (Токіо)" @@ -755,6 +757,7 @@ uk: smtp_instructions: "Коли ви ввімкнете SMTP для групи, всі вихідні повідомлення електронної пошти, надіслані з групи, буде надіслано через вказані тут параметри SMTP, замість стандартного поштового сервера, налаштованого для інших листів." imap_title: "IMAP" imap_additional_settings: "Додаткові налаштування" + imap_instructions: 'Коли ви ввімкнете IMAP для групи, електронні листи синхронізуються між папкою вхідних груп і наданим сервером IMAP і поштовою скринькою. Перш ніж увімкнути протокол IMAP, необхідно ввімкнути SMTP з дійсними та перевіреними обліковими даними. Ім’я користувача та пароль електронної пошти, які використовуються для SMTP, будуть використовуватися для IMAP. Для отримання додаткової інформації див. анонс про функції на Discourse Meta.' imap_alpha_warning: "Попередження. Це функція на альфа-тестуванні. Офіційно підтримується лише Gmail. Використовуйте на свій страх і ризик!" imap_settings_valid: "Налаштування IMAP правильні." smtp_disable_confirm: "Якщо вимкнути SMTP, усі налаштування SMTP та IMAP будуть скинуті, а відповідна функціональність буде вимкнена. Ви впевнені, що хочете продовжити?" @@ -1112,9 +1115,15 @@ uk: no_messages_body: > Потрібна особиста розмова з кимось, поза звичайним спілкуванням? Надішліть їм повідомлення, вибравши їх аватар і натиснувши кнопку повідомлення %{icon}

Якщо вам потрібна допомога, ви можете надіслати повідомлення співробітнику. no_bookmarks_title: "Ви ще нічого не додали до закладок" + no_bookmarks_body: > + Почніть закладки повідомлень за допомогою кнопки %{icon}, і вони будуть перераховані тут для швидкого доступу. Ви також можете запланувати нагадування про закладку! no_bookmarks_search: "Немає закладок за таким пошуковим запитом." no_notifications_title: "У вас ще немає сповіщень" + no_notifications_body: > + На цій панелі ви будете сповіщені про активність, яка безпосередньо стосується вас, включаючи відповіді на ваші теми та публікації, коли хтось @згадує або цитує вас, а також відповідає на теми, які ви переглядаєте. Сповіщення також буде надіслано на вашу електронну пошту, якщо ви деякий час не входили в систему.

Знайдіть %{icon} щоб визначити, про які конкретні теми, категорії та теги ви хочете отримувати сповіщення. Щоб дізнатися більше, перегляньте свої налаштування сповіщень. no_notifications_page_title: "У вас ще немає сповіщень" + no_notifications_page_body: > + Ви отримуватимете сповіщення про діяльність, яка безпосередньо стосується вас, включаючи відповіді на ваші теми та публікації, коли хтось @згадує або цитує вас, а також відповідає на теми, які ви переглядаєте. Сповіщення також буде надіслано на вашу електронну пошту, якщо ви деякий час не входили в систему.

Використовуйте іконку %{icon} щоб визначити, про які конкретні теми, категорії та теги ви хочете отримувати сповіщення. Щоб дізнатися більше, перегляньте свої параметри сповіщень. first_notification: "Ваше перше сповіщення! Оберіть його, щоб почати." dynamic_favicon: "Показувати кількість на іконці браузера" skip_new_user_tips: @@ -1244,6 +1253,7 @@ uk: read_more_in_group: "Бажаєте дізнатися більше? Перегляньте інші повідомлення в %{groupLink}." read_more: "Хочете прочитати більше? Перегляньте інші повідомлення в особистих повідомленнях." read_more_group_pm_MF: "У вас { UNREAD, plural, =0 {} one { # непрочитане } few { # непрочитаних } other { # непрочитаних } } { NEW, plural, =0 {} one { {BOTH, select, true{и } false { } other{}} # нове повідомлення} few { {BOTH, select, true{і } false { } other{}} # нових повідомлень} other { {BOTH, select, true{і } false { } other{}} # нових повідомлень} }, ви також можете продивитись інші повідомлення в групі {groupLink}" + read_more_personal_pm_MF: "У вас { UNREAD, plural, =0 {} one { є # непрочитане } few { є # непрочитані } many { є # непрочитаних } other { є # непрочитані } } { NEW, plural, =0 {} one { {BOTH, select, true{та } false {є } other{}} # нове повідомлення} few {} many {} other { {BOTH, select, true{та } false {є } other{}} # нові повідомлення} } залишайтесь, або перегляньте інші приватні повідомлень" preferences_nav: account: "Обліковий запис" security: "Безпека" @@ -1497,9 +1507,11 @@ uk: every_month: "щомісяця" every_six_months: "що шість місяців" email_level: + title: "Надсилайте мені лист, коли мене цитують, відповідають, згадують моє ім’я користувача, або коли з’являться нові дії в категоріях, тегах чи темах, за якими я стежу" always: "завжди" only_when_away: "тільки коли вдалині" never: "ніколи" + email_messages_level: "Напишіть мені, коли я надсилаю особисте повідомлення" include_tl0_in_digests: "Включити контент від нових користувачів в зведення, що відправляються по електронній пошті" email_in_reply_to: "Додати попередні відповіді до кінця електронних листів" other_settings: "Інше" @@ -2056,6 +2068,11 @@ uk: few: "Згадуючи групу %{group}, ви тим самим відправите повідомлення %{count} користувачам - ви впевнені?" many: "Згадуючи групу %{group}, ви тим самим відправите повідомлення %{count} користувачам - ви впевнені?" other: "Згадуючи групу %{group}, ви тим самим відправите повідомлення %{count} користувачам - ви впевнені?" + cannot_see_mention: + category: "Ви згадали @%{username}, але вони не одержать повідомлення, тому що у них немає доступу до цього розділу. Вам потрібно додати їх до групи, що має доступ до цього розділу." + private: "Ви згадали @%{username} але вони не отримають сповіщення, оскільки не можуть побачити це особисте повідомлення. Вам потрібно буде запросити їх до цього особистого повідомлення." + muted_topic: "Ви згадали @%{username} але вони не отримають сповіщення, оскільки вимкнули сповіщення в цій темі." + not_allowed: "Ви згадали @%{username}, але вони не отримають сповіщення, оскільки їх не запросили до цієї теми." here_mention: one: "Згадуючи @%{here}, ви збираєтеся повідомити %{count} користувача - ви впевнені?" few: "Згадуючи @%{here}, ви збираєтеся повідомити %{count} користувачів - ви впевнені?" @@ -3323,7 +3340,7 @@ uk: default_list_filter: "Стандартний фільтр списку:" allow_badges_label: "Дозволити нагороджувати значками у цій категорії" edit_permissions: "Редагувати дозволи" - reviewable_by_group: "Окрім персоналу, зміст цієї категорії також може бути переглянутий:" + reviewable_by_group: "Крім персоналу, зміст цієї категорії також може бути переглянутий:" review_group_name: "назва групи" require_topic_approval: "З них потребують схвалення модератором всіх нових тем" require_reply_approval: "З них потребують схвалення модератором всіх нових відповідей" @@ -5448,7 +5465,9 @@ uk: grant_existing_holders: Надати додаткові нагороди існуючим власникам нагород emoji: title: "Іконки" + help: "Додайте нові емодзі, які будуть доступні для всіх. Перетягніть і додайте декілька файлів відразу без введення назви для створення емодзі використовуючи їхні назви файлів. Обрана група буде використовуватися для всіх файлів, які додаються одночасно. Ви також можете натиснути \"Додати нові емодзі\", щоб відкрити вікно вибору файлів." add: "Додати нову іконку" + choose_files: "Виберіть файли" uploading: "Завантаження…" name: "Ім'я" group: "Група" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 03ff524032..7d71447083 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -2724,7 +2724,6 @@ vi: default_list_filter: "Bộ lọc danh sách mặc định:" allow_badges_label: "Cho phép thưởng huy hiệu trong chuyên mục này" edit_permissions: "Sửa quyền" - reviewable_by_group: "Ngoài nhân viên, nội dung trong danh mục này cũng có thể được kiểm duyệt bởi:" review_group_name: "Nhóm tên" require_topic_approval: "Yêu cầu người kiểm duyệt phê duyệt tất cả các chủ đề mới" require_reply_approval: "Yêu cầu người kiểm duyệt phê duyệt tất cả các câu trả lời mới" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index a20906d8b0..e8a5c2f7fb 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -2938,7 +2938,6 @@ zh_CN: default_list_filter: "默认列表筛选器:" allow_badges_label: "允许在此类别中授予徽章" edit_permissions: "编辑权限" - reviewable_by_group: "除管理人员外,此类别中的内容还可以由以下群组审核:" review_group_name: "群组名称" require_topic_approval: "所有新话题都需要版主审批" require_reply_approval: "所有新回复都需要版主审批" @@ -4098,7 +4097,7 @@ zh_CN: 强烈建议为属性名称添加前缀以避免与插件和/或核心冲突。 head_tag: - text: "头像" + text: "标签" title: "将在 tag 前插入的 HTML" body_tag: text: "正文" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 39d47656c9..2f86f071dd 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -408,8 +408,6 @@ ar: few: "عذرًا، لا يمكن للمستخدمين الجُدد وضع أكثر من %{count} روابط في المنشور." many: "عذرًا، لا يمكن للمستخدمين الجُدد وضع أكثر من %{count} رابطًا في المنشور." other: "عذرًا، لا يمكن للمستخدمين الجُدد وضع أكثر من %{count} رابط في المنشور." - contains_blocked_word: "يتضمَّن منشورك كلمة غير مسموح بها: %{word}" - contains_blocked_words: "يتضمَّن منشورك عدة كلمات غير مسموح بها: %{words}" spamming_host: "عذرًا، لا يمكنك نشر رابط إلى هذا المضيف." user_is_suspended: "غير مسموح للمستخدمين المعلَّقين بالنشر." topic_not_found: "حدث خطأ. ربما يكون هذا الموضوع قد تم إغلاقه أو حذفه بينما كنت تعرضه؟" @@ -1586,7 +1584,6 @@ ar: min_personal_message_title_length: "الحد الأدنى المسموح به لطول عنوان الرسالة بالأحرف" max_emojis_in_title: "الحد الأقصى المسموح به من الرموز التعبيرية في عنوان الموضوع" min_search_term_length: "الحد الأدنى الصالح لطول مصطلح البحث بالأحرف" - search_tokenize_chinese_japanese_korean: "فرض تقسيم جمل البحث في اللغات الصينية واليابانية والكورية إلى كلمات حتى على المواقع التي لا تدعم تلك اللغات" search_prefer_recent_posts: "سيحاول هذا الخيار البحث في فهرس أحدث المنشورات أولًا إذا كان البحث في منتداك الكبير بطيئًا" search_recent_posts_size: "عدد المنشورات الحديثة التي سيتم الإبقاء عليها في الفهرس" log_search_queries: "تسجيل استعلامات البحث التي يجريها المستخدمون" @@ -1629,7 +1626,6 @@ ar: show_pinned_excerpt_mobile: "إظهار المقتطف في الموضوعات المثبَّتة فى طريقة عرض الجوَّال." show_pinned_excerpt_desktop: "إظهار المقتطف في الموضوعات المثبَّتة فى طريقة عرض سطح المكتب" post_onebox_maxlength: "الحد الأقصى لطول منشور Discourse في لوحة المعاينة بالأحرف." - blocked_onebox_domains: "قائمة بالنطاقات التي لن يتم عرضها في لوحة المعاينة أبدًا." allowed_inline_onebox_domains: "قائمة بالنطاقات التي سيتم وضعها في لوحة المعاينة في شكلٍ مصغَّر إذا تم ربطها دون عنوان" enable_inline_onebox_on_all_domains: "تجاهل إعداد الموقع inline_onebox_domain_allowlist والسماح بلوحة المعاينة المضمَّنة على جميع النطاقات." force_custom_user_agent_hosts: "المضيفات التي سيتم استخدام وكيل المستخدم للوحة المعاينة المخصَّصة في جميع طلباتها. (هذا الإعداد مفيد بشكلٍ خاص للمضيفات الذين تقيِّد الوصول حسب وكيل المستخدم)." diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index 9116bb9b76..8dda5ef21f 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -190,7 +190,6 @@ be: no_attachments_allowed: "На жаль, новыя карыстальнікі не могуць паставіць ўкладанні ў паведамленні." no_links_allowed: "На жаль, новыя карыстальнікі не могуць змяшчаць спасылкі ў паведамленнях." links_require_trust: "На жаль, вы не можаце ўключаць спасылкі на вашыя пасты." - contains_blocked_word: "Ваша паведамленне ўтрымлівае слова якое не дазволена: %{word}" spamming_host: "На жаль, вы не можаце размясціць спасылку на гэты хост." user_is_suspended: "Заблякаваныя карыстальнікі не могуць пакідаць паведамленні." topic_not_found: "Нешта пайшло не так. Магчыма, гэтая тэма была зачыненая або выдаленая ў той час як вы глядзіце на гэта?" @@ -921,7 +920,6 @@ be: show_pinned_excerpt_mobile: "Паказаць ўрывак на ўскладалі тэмы ў мабільным рэжыме." show_pinned_excerpt_desktop: "Паказаць ўрывак на ўскладалі тэмы з улікам працоўнага стала." post_onebox_maxlength: "Максімальная даўжыня oneboxed дыскурсу паста у знаках." - blocked_onebox_domains: "Спіс даменаў, якія ніколі не будуць oneboxed." allowed_inline_onebox_domains: "Спіс даменаў, якія будуць oneboxed ў мініятурным выглядзе, калі звязаныя без назвы" max_oneboxes_per_post: "Максімальную колькасць у раздзелах Onebox пасады." logo: "Малюнак лагатыпа ў левым верхнім куце сайта. Выкарыстоўвайце шырокае прамавугольнае малюнак з вышынёй 120 і суадносінамі бакоў больш, чым 3: 1. Калі пакінуць пустым, то назва сайта будзе паказаны тэкст." diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index f7e9bedf54..e0a512846e 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -157,7 +157,6 @@ bg: too_many_links: one: "Съжаляваме, новите потребители могат да поставят само по един линк в публикация." other: "Съжаляваме, новите потребители могат да поставят само %{count} линкове в публикация." - contains_blocked_word: "Публикацията ви съдържа непозволена дума: %{word}" spamming_host: "Съжаляваме, не можете да публикувате линк на този хост." user_is_suspended: "Отстранените потребители нямат право да публикуват." topic_not_found: "Нещо не е наред. Може би тази тема е била затворена или изтрита, докато я гледате." diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index 796371066b..8994e15a35 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -262,8 +262,6 @@ ca: too_many_links: one: "Els usuaris nous només poden incloure un enllaç en una publicació." other: "Els usuaris nous només poden incloure %{count} enllaços en una publicació." - contains_blocked_word: "La vostra publicació té una paraula que no és permesa: %{word}" - contains_blocked_words: "La vostra publicació conté diverses paraules no admeses: %{words}" spamming_host: "No podeu publicar un enllaç a aquest amfitrió." user_is_suspended: "Els usuaris suspesos no tenen permís per a publicar." topic_not_found: "Alguna cosa ha fallat. Potser el tema ha estat tancat o suprimit mentre el consultàveu." @@ -1196,7 +1194,6 @@ ca: min_personal_message_title_length: "Longitud mínima permesa de títol de tema en caràcters per a un missatge" max_emojis_in_title: "Emojis màxims permesos en el títol del tema" min_search_term_length: "Longitud mínima vàlida del terme de cerca en caràcters" - search_tokenize_chinese_japanese_korean: "Força la cerca per a segmentar xinès/japonès/coreà fins i tot en llocs web no-CJK." search_prefer_recent_posts: "Si la cerca en el vostre fòrum és lenta, aquesta opció prova primer amb un índex de publicacions més recents." search_recent_posts_size: "Quantes publicacions recents es mantenen en l'índex" log_search_queries: "Registra les cerques fetes pels usuaris" @@ -1235,7 +1232,6 @@ ca: show_pinned_excerpt_mobile: "Mostra l'extracte en els temes afixats en la vista per a mòbils." show_pinned_excerpt_desktop: "Mostra l'extracte en els temes afixats en la vista d'escriptori." post_onebox_maxlength: "Longitud màxima en caràcters d'una publicació en onebox de Discourse." - blocked_onebox_domains: "Llista de dominis que no es posaran mai en onebox." allowed_inline_onebox_domains: "Llista de dominis que es mostraran en miniatura 'onebox' si s'enllacen sense títol" max_oneboxes_per_post: "Nombre màxim de onebox en una publicació" logo: "La imatge del logotip a la part superior esquerra del vostre lloc web. Utilitzeu una imatge rectangular ampla amb una alçada de 120 i una relació d'aspecte superior a 3: 1. Si es deixa en blanc, es mostrarà el text del títol del lloc web." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 16e6ce3f1a..c34f92fff5 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -323,8 +323,6 @@ da: too_many_links: one: "Beklager, nye brugere kan kun indsætte et link i et indlæg." other: "Beklager, nye brugere kan kun indsætte %{count} links i et indlæg." - contains_blocked_word: "Dit indlæg indeholder et ord, der ikke er tilladt: %{word}" - contains_blocked_words: "Dit indlæg indeholder flere ord, der ikke er tilladt: %{words}" spamming_host: "Beklager, du kan ikke indsætte et link til det pågældende domæne." user_is_suspended: "Suspenderede brugere tilllades ikke at oprette indlæg." topic_not_found: "Der er gået noget galt. Måske er emnet blevet lukket eller slettet mens du kiggede på det?" @@ -1324,7 +1322,6 @@ da: min_personal_message_title_length: "Minimumslængde af emnetitel i tegn." max_emojis_in_title: "Maksimum tilladte humørikoner i emnetitel" min_search_term_length: "Antal tegn i søgefeltet skal have et minimum" - search_tokenize_chinese_japanese_korean: "Tving søg til at \"tokenize\" Chinese/Japanese/Korean selv på sites som ikke er CJK" search_prefer_recent_posts: "Såfremt søgning på forum er langsomt, kan denne option indeksere og vise nyeste indlæg først" search_recent_posts_size: "Hvor mange nye indlæg skal der gemmes i index" log_search_queries: "Log søgeforespørgsler udført af brugere" @@ -1359,7 +1356,6 @@ da: show_pinned_excerpt_mobile: "Vis uddrag af fastgjorte emner på mobilen." show_pinned_excerpt_desktop: "Vis uddrag af fastgjorte emner i desktop visning" post_onebox_maxlength: "Max længde af Discourse indlæg i tegn" - blocked_onebox_domains: "En liste af domæner som aldrig vil blive onebox'et." allowed_inline_onebox_domains: "En liste over domæner, der vil blive onebox'et i miniatureform, hvis de linkes uden en titel" enable_inline_onebox_on_all_domains: "Ignorer webstedsindstilling for inline_onebox_domain_allowlist, og tillad indlejret onebox på alle domæner." max_oneboxes_per_post: "Maksimalt antal onebox-forekomster i et opslag." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 3650e3a3a2..ebacecbdfe 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -338,8 +338,8 @@ de: too_many_links: one: "Entschuldige, neue Benutzer können Beiträgen höchstens einen Link hinzufügen." other: "Entschuldige, neue Benutzer können Beiträgen höchstens %{count} Links hinzufügen." - contains_blocked_word: "Dein Beitrag enthält ein nicht erlaubtes Wort: %{word}" - contains_blocked_words: "Dein Beitrag enthält mehrere Wörter, die nicht erlaubt sind: %{words}" + contains_blocked_word: "Tut mir leid, du kannst das Wort '%{word}' nicht posten; es ist nicht erlaubt." + contains_blocked_words: "Sorry, das kannst du nicht posten. Nicht erlaubt: %{words}." spamming_host: "Entschuldige, leider kannst du keinen Link zu diesem Host posten." user_is_suspended: "Gesperrte Benutzer dürfen keine Beiträge schreiben." topic_not_found: "Etwas ist schiefgelaufen. Wurde das Thema eventuell geschlossen oder gelöscht, während du es angeschaut hast?" @@ -1381,6 +1381,7 @@ de: unreachable_themes: "Wir konnten die folgenden Themes nicht auf Updates prüfen:" watched_word_regexp_error: "Der reguläre Ausdruck für „%{action}“ überwachte Wörter ist ungültig. Bitte überprüfe deine Einstellungen für überwachte Wörter oder deaktiviere die Website-Einstellung „watched words regular expressions“." site_settings: + allow_bulk_invite: "Erlaube Masseneinladungen durch Hochladen einer CSV-Datei" disabled: "deaktiviert" display_local_time_in_user_card: "Zeigt die lokale Zeit basierend auf der Zeitzone eines Benutzers an, wenn die Benutzerkarte geöffnet wird." censored_words: "Wörter, die automatisch durch ■■■■ ersetzt werden" @@ -1401,7 +1402,6 @@ de: min_personal_message_title_length: "Minimal zulässige Zeichenanzahl für den Titel von Nachrichten." max_emojis_in_title: "Maximal erlaubte Emoji-Anzahl in Thementiteln" min_search_term_length: "Minimal zulässige Länge eines Suchbegriffs in Zeichen." - search_tokenize_chinese_japanese_korean: "Zwinge die Suche, Chinesisch/Japanisch/Koreanisch zu tokenisieren, auch wenn Websites keine dieser Sprachen nutzen" search_prefer_recent_posts: "Wenn das Durchsuchen deines großen Forums langsam ist, dann versucht es diese Option zuerst mit einem Index der letzten Beiträge." search_recent_posts_size: "Anzahl aktueller Beiträge im Index" log_search_queries: "Protokolliere Suchanfragen von Benutzern" @@ -1445,7 +1445,7 @@ de: show_pinned_excerpt_mobile: "Zeige einen Auszug angehefteter Themen in der mobilen Ansicht." show_pinned_excerpt_desktop: "Zeige einen Auszug angehefteter Themen in der Desktop-Ansicht." post_onebox_maxlength: "Maximale Länge eines Onebox-Discourse-Beitrags in Zeichen." - blocked_onebox_domains: "Eine Liste von Domains, die nie in eine Onebox umgewandelt werden." + blocked_onebox_domains: "Eine Liste von Domains, die niemals in einer Onebox untergebracht werden, z.B. wikipedia.org\n(Wildcard-Symbole * ? nicht unterstützt)" allowed_inline_onebox_domains: "Eine Liste von Domains, die in verkleinerter Form in eine Onebox umgewandelt werden, wenn sie ohne Titel verlinkt werden" enable_inline_onebox_on_all_domains: "Ignoriere die Website-Einstellung `inline_onebox_domain_allowlist` und erlaube Inline-Oneboxen für alle Domains" force_custom_user_agent_hosts: "Hosts, bei denen der spezifische Onebox-Useragent für alle Anfragen genutzt wird. (Besonders hilfreich bei Hosts, die Zugriffe per Useragent limitieren.)" @@ -1533,7 +1533,7 @@ de: allowed_iframes: "Eine Liste von iframe-src-Domain-Präfixen, die Discourse in Beiträgen sicher erlauben kann." allowed_crawler_user_agents: "Browserkennungen von Webcrawlern, denen der Zugriff auf die Website erlaubt sein soll. WARNUNG! DIESE EINSTELLUNG BLOCKIERT ALLE HIER NICHT GELISTETEN CRAWLER!" blocked_crawler_user_agents: "Eindeutiges Wort unter Vernachlässigung der Groß- und Kleinschreibung in der Zeichenfolge der Browserkennung, das Webcrawler identifiziert, die nicht auf die Website zugreifen dürfen. Gilt nicht, wenn die Positivliste definiert ist." - slow_down_crawler_user_agents: "Benutzeragenten von Web-Crawlern, die wie in der Einstellung \"Crawler-Rate verlangsamen\" konfiguriert, begrenzt werden sollen. Jeder Wert muss mindestens 3 Zeichen lang sein." + slow_down_crawler_user_agents: 'Benutzeragenten von Web-Crawlern, die wie in der Einstellung "Crawler-Rate verlangsamen" konfiguriert, begrenzt werden sollen. Jeder Wert muss mindestens 3 Zeichen lang sein.' slow_down_crawler_rate: "Wenn slow_down_crawler_user_agents ausgewählt ist, wird diese Anfragenbegrenzung auf alle Crawler angewendet (Anzahl der Sekunden zwischen zwei Anfragen)" content_security_policy: "Aktiviere Content-Security-Policy" content_security_policy_report_only: "Aktiviere Content-Security-Policy-Report-Only" @@ -2097,6 +2097,7 @@ de: share_quote_visibility: "Bestimme, wann die Schaltflächen zum Teilen von Zitaten angezeigt werden sollen: nie, nur für anonyme Benutzer oder für alle Benutzer. " create_revision_on_bulk_topic_moves: "Erstelle eine Überarbeitung für die ersten Beiträge, wenn Themen massenweise in eine neue Kategorie verschoben werden." allow_changing_staged_user_tracking: "Erlauben Sie, dass die Kategorie- und Tag-Benachrichtigungseinstellungen eines bereitgestellten Benutzers von einem Administratorbenutzer geändert werden." + use_email_for_username_and_name_suggestions: "Verwende den ersten Teil der E-Mail-Adressen für Vorschläge für Benutzernamen und Namen. Dies macht es für die Öffentlichkeit einfacher, die vollständigen E-Mail-Adressen der Nutzer/innen zu erraten (da ein großer Teil der Menschen gemeinsame Dienste wie \"gmail.com\" nutzt)." errors: invalid_css_color: "Ungültige Farbe. Gib einen Farbnamen oder einen Hexadezimalwert ein." invalid_email: "Ungültige E-Mail-Adresse." @@ -2116,6 +2117,7 @@ de: invalid_json: "Ungültiges JSON." invalid_reply_by_email_address: "Wert muss „%{reply_key}“ enthalten und sich von der Benachrichtigungs-E-Mail unterscheiden." invalid_alternative_reply_by_email_addresses: "Alle Werte müssen „%{reply_key}“ enthalten und sich von der Benachrichtigungs-E-Mail unterscheiden." + invalid_domain_hostname: "Darf kein * oder ? Zeichen enthalten." pop3_polling_host_is_empty: "Du musst „pop3 polling host“ definieren, um POP3-Abfragen zu aktivieren." pop3_polling_username_is_empty: "Du musst „pop3 polling username“ definieren, um POP3-Abfragen zu aktivieren." pop3_polling_password_is_empty: "Du musst „pop3 polling password“ definieren, um POP3-Abfragen zu aktivieren." diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index a0b18e7fb7..b7e621ec8f 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -922,7 +922,6 @@ el: max_topic_title_length: "Μέγιστο επιτρεπτό μήκος τίτλου νήματος σε χαρακτήρες" min_personal_message_title_length: "Ελάχιστο επιτρεπτό μήκος τίτλου για ένα μήνυμα σε χαρακτήρες" min_search_term_length: "Ελάχιστο έγκυρο μήκος όρου αναζήτησης σε χαρακτήρες" - search_tokenize_chinese_japanese_korean: "Ανάγκασε την αναζήτηση να κάνει tokenize Κινέζικα / Ιαπωνικά / κορεατικά, ακόμη και σε μη CJK ιστότοπους" search_prefer_recent_posts: "Εάν η αναζήτηση στην ιστοσελίδα σας είναι αργή, αυτή η επιλογή δημιουργεί κατάλογο των πιο πρόσφατων αναρτήσεων πρώτα" search_recent_posts_size: "Πόσες πρόσφατες αναρτήσεις να κρατηθούν στο ευρετήριο " log_search_queries: "Κατέγραψε τις αναζητήσεις που εκτελούνται από χρήστες" @@ -950,7 +949,6 @@ el: show_pinned_excerpt_mobile: "Εμφάνιση περίληψης σε καρφιτσωμένα νήματα στην περιήγηση από κινητά τηλέφωνα." show_pinned_excerpt_desktop: "Εμφάνιση περίληψης σε καρφιτσωμένα νήματα στην περιήγηση από ηλεκτρονικούς υπολογιστές." post_onebox_maxlength: "Μέγιστο μέγεθος μιας oneboxed ανάρτησης σε χαρακτήρες." - blocked_onebox_domains: "Μία λίστα τομέων που δεν θα είναι ποτέ oneboxed." allowed_inline_onebox_domains: "Μια λίστα από domains τα οποία θα γίνουν onebox σε σκίκρυνση αν αναφερθούν χωρίς τίτλο" max_oneboxes_per_post: "Ανώτατος αριθμός oneboxes σε μια ανάρτηση. " notification_email: "Η από: διεύθυνση email που χρησιμοποιείται για την αποστολή όλων των απαραιτήτων μηνυμάτων του συστήματος. Ο τομέας που καθορίζεται εδώ πρέπει να έχει SPF, DKIM και reverse PTR αρχεία ρυθμισμένα σωστά για να φθάσει το μήνυμα." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2d1bdc13b0..39c73ce03b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -366,8 +366,8 @@ 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_word: "Your post contains a word that's not allowed: %{word}" - contains_blocked_words: "Your post contains multiple words that aren't allowed: %{words}" + contains_blocked_word: "Sorry, you can't post the word '%{word}'; it's not allowed." + contains_blocked_words: "Sorry, you can't post that. Not allowed: %{words}." spamming_host: "Sorry you cannot post a link to that host." user_is_suspended: "Suspended users are not allowed to post." @@ -1465,6 +1465,7 @@ en: watched_word_regexp_error: "The regular expression for '%{action}' watched words is invalid. Please check your Watched Word settings, or disable the 'watched words regular expressions' site setting." site_settings: + allow_bulk_invite: "Allow bulk invites by uploading a CSV file" disabled: "disabled" display_local_time_in_user_card: "Display the local time based on a user's timezone when their user card is opened." censored_words: "Words that will be automatically replaced with ■■■■" @@ -1485,7 +1486,8 @@ en: min_personal_message_title_length: "Minimum allowed title length for a message in characters" max_emojis_in_title: "Maximum allowed emojis in topic title" min_search_term_length: "Minimum valid search term length in characters" - search_tokenize_chinese_japanese_korean: "Force search to tokenize Chinese/Japanese/Korean even on non CJK sites" + search_tokenize_chinese: "Force search to tokenize Chinese even on non Chinese sites" + search_tokenize_japanese: "Force search to tokenize Japanese even on non Japanese 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" @@ -1530,7 +1532,7 @@ en: show_pinned_excerpt_mobile: "Show excerpt on pinned topics in mobile view." show_pinned_excerpt_desktop: "Show excerpt on pinned topics in desktop view." post_onebox_maxlength: "Maximum length of a oneboxed Discourse post in characters." - blocked_onebox_domains: "A list of domains that will never be oneboxed." + blocked_onebox_domains: "A list of domains that will never be oneboxed e.g. wikipedia.org\n(Wildcard symbols * ? not supported)" allowed_inline_onebox_domains: "A list of domains that will be oneboxed in miniature form if linked without a title" enable_inline_onebox_on_all_domains: "Ignore inline_onebox_domain_allowlist site setting and allow inline onebox on all domains." force_custom_user_agent_hosts: "Hosts for which to use the custom onebox user agent on all requests. (Especially useful for hosts that limit access by user agent)." @@ -1628,7 +1630,7 @@ en: allowed_iframes: "A list of iframe src domain prefixes that discourse can safely allow in posts" allowed_crawler_user_agents: "User agents of web crawlers that should be allowed to access the site. WARNING! SETTING THIS WILL DISALLOW ALL CRAWLERS NOT LISTED HERE!" blocked_crawler_user_agents: "Unique case insensitive word in the user agent string identifying web crawlers that should not be allowed to access the site. Does not apply if allowlist is defined." - slow_down_crawler_user_agents: "User agents of web crawlers that should be rate limited as configured in the \"slow down crawler rate\" setting. Each value must be at least 3 characters long." + slow_down_crawler_user_agents: 'User agents of web crawlers that should be rate limited as configured in the "slow down crawler rate" setting. Each value must be at least 3 characters long.' slow_down_crawler_rate: "If slow_down_crawler_user_agents is specified this rate will apply to all the crawlers (number of seconds delay between requests)" content_security_policy: "Enable Content-Security-Policy" content_security_policy_report_only: "Enable Content-Security-Policy-Report-Only" @@ -1904,7 +1906,7 @@ en: max_mentions_per_post: "Maximum number of @name notifications anyone can use in a post." max_users_notified_per_group_mention: "Maximum number of users that may receive a notification if a group is mentioned (if threshold is met no notifications will be raised)" enable_mentions: "Allow users to mention other users." - here_mention: "Name used for @here mention. Must not be an existent username." + here_mention: "Name used for a @mention to allow privileged users to notify up to 'max_here_mentioned' people participating in the topic. Must not be an existing username." max_here_mentioned: "Maximum number of mentioned people by @here." min_trust_level_for_here_mention: "The minimum trust level allowed to mention @here." @@ -2186,7 +2188,7 @@ en: display_name_on_posts: "Show a user's full name on their posts in addition to their @username." show_time_gap_days: "If two posts are made this many days apart, display the time gap in the topic." short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value." - default_code_lang: "Default programming language syntax highlighting applied to GitHub code blocks (auto, nohighlight, ruby, python etc.)" + default_code_lang: "Default programming language syntax highlighting applied to code blocks (auto, nohighlight, ruby, python etc.)" warn_reviving_old_topic_age: "When someone starts replying to a topic where the last reply is older than this many days, a warning will be displayed. Disable by setting to 0." autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language." highlighted_languages: "Included syntax highlighting rules. (Warning: including too many languages may impact performance) see: https://highlightjs.org/static/demo for a demo" @@ -2344,6 +2346,7 @@ en: create_revision_on_bulk_topic_moves: "Create revision for first posts when topics are moved into a new category in bulk." allow_changing_staged_user_tracking: "Allow a staged user's category and tag notification preferences to be changed by an admin user." + use_email_for_username_and_name_suggestions: "Use the first part of email addresses for username and name suggestions. Note that this makes it easier for the public to guess full user email addresses (because a large proportion of people share common services like `gmail.com`)." errors: invalid_css_color: "Invalid color. Enter a color name or hex value." @@ -2364,6 +2367,7 @@ en: invalid_json: "Invalid JSON." invalid_reply_by_email_address: "Value must contain '%{reply_key}' and be different from the notification email." invalid_alternative_reply_by_email_addresses: "All values must contain '%{reply_key}' and be different from the notification email." + invalid_domain_hostname: "Must not include * or ? characters." pop3_polling_host_is_empty: "You must set a 'pop3 polling host' before enabling POP3 polling." pop3_polling_username_is_empty: "You must set a 'pop3 polling username' before enabling POP3 polling." pop3_polling_password_is_empty: "You must set a 'pop3 polling password' before enabling POP3 polling." @@ -2393,6 +2397,8 @@ en: unicode_usernames_avatars: "The internal system avatars do not support Unicode usernames." list_value_count: "The list must contain exactly %{count} values." google_oauth2_hd_groups: "You must first set 'google oauth2 hd' before enabling this setting." + search_tokenize_chinese_enabled: "You must disable 'search_tokenize_chinese' before enabling this setting." + search_tokenize_japanese_enabled: "You must disable 'search_tokenize_japanese' before enabling this setting." placeholder: discourse_connect_provider_secrets: diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index a8d99a843c..953fedade1 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -338,8 +338,6 @@ es: too_many_links: one: "Lo sentimos, los usuarios nuevos solo pueden poner un enlace en una publicación." other: "Lo sentimos, los usuarios nuevos solo pueden poner %{count} enlaces en una publicación." - contains_blocked_word: "Tu publicación contiene una palabra que no está permitida: %{word}" - contains_blocked_words: "Tu publicación contiene varias palabras que no están permitidas: %{words}" spamming_host: "Lo sentimos, no puedes publicar un enlace a esa web." user_is_suspended: "A los usuarios suspendidos no se les permite publicar." topic_not_found: "Algo salió mal. ¿Tal vez este tema se cerró o eliminó mientras estabas lo estabas mirando?" @@ -486,6 +484,7 @@ es: Puedes editar tu anterior respuesta para añadir una cita. Para ello, selecciona el texto que quieras citar y pulsa el botón citar respuesta que aparecerá. Es más fácil leer temas que tengan menos respuestas (aunque más profundas), que tener que leer muchas respuestas individuales. + dominating_topic: Has publicado más del %{percent} % de las respuestas, ¿hay alguien más de quien te gustaría saber? get_a_room: Has respondido a @%{reply_username} %{count} veces, ¿sabías que también puedes enviarle un mensaje personal directamente? too_many_replies: | ### Has llegado al límite de respuestas en este tema @@ -1337,6 +1336,7 @@ es: labels: user: Usuario qtt_like: Me gusta recibidos + description: "Los 10 usuarios que más me gusta han recibido." top_users_by_likes_received_from_inferior_trust_level: title: "Usuarios que más «me gusta» han recibido de usuarios con niveles de confianza bajos" labels: @@ -1349,7 +1349,9 @@ es: labels: user: Usuario qtt_like: Me gusta recibidos + description: "Los 10 usuarios que han recibido más me gusta de numerosas personas distintas." dashboard: + group_email_credentials_warning: 'Hay un problema con las credenciales del correo electrónico del grupo %{group_full_name}. No se enviarán mensajes desde la bandeja de entrada del grupo hasta que se solucione el problema. %{error}' rails_env_warning: "Tu servidor está funcionando en modo de %{env}." host_names_warning: "Tu archivo config/database.yml está utilizando el hostname localhost predeterminado. Actualízalo para usar el hostname de tu sitio." sidekiq_warning: 'Sidekiq no está funcionando. Muchas tareas, tal como el envío de correos electrónicos, se están ejecutando de forma desincronizada por sidekiq. Asegúrate de que por lo menos un proceso de sidekiq está funcionando. Puedes obtener más información sobre Sidekiq aquí.' @@ -1397,7 +1399,6 @@ es: min_personal_message_title_length: "Longitud mínima del título para un mensaje en número de caracteres" max_emojis_in_title: "Cantidad máxima de emojis en el título del tema" min_search_term_length: "Extensión mínima de una búsqueda válida en número de caracteres" - search_tokenize_chinese_japanese_korean: "Forzar la búsqueda a tokenizar Chino/Japonés/Coreano incluso en sitios no basados en esos idiomas" search_prefer_recent_posts: "Si la búsqueda en tu foro grande es lenta, esta opción prueba primero en un índice de publicaciones más recientes" search_recent_posts_size: "Cantidad de publicaciones recientes que se mantienen en el índice" log_search_queries: "Registro de búsquedas realizadas por los usuarios" @@ -1441,7 +1442,6 @@ es: show_pinned_excerpt_mobile: "Mostrar extracto de temas anclados en la vista móvil." show_pinned_excerpt_desktop: "Mostrar extracto de temas anclados en la vista de escritorio." post_onebox_maxlength: "Extensión máxima en caracteres de una publicación de Discourse en formato onebox." - blocked_onebox_domains: "Una lista de dominios que nunca será oneboxed." allowed_inline_onebox_domains: "Una lista de dominios que serán oneboxed en forma de miniatura si están vinculados sin título" enable_inline_onebox_on_all_domains: "Ignorar el ajuste del sitio inline_onebox_domain_allowlist y permitir inline onebox en todos los dominios." force_custom_user_agent_hosts: "Hosts que se utilizará para el agente usuario personalizado de onebox en todas las solicitudes. (Especialmente útil para hosts que limitan el acceso por usuario agente)." @@ -1529,7 +1529,7 @@ es: allowed_iframes: "Una lista de prefijos de dominios para iframe src que discourse puede permitir de forma segura en publicaciones" allowed_crawler_user_agents: "Agentes de usuario de crawlers a los que se debe permitir el acceso al sitio. ¡ADVERTENCIA! ¡CONFIGURAR ESTO INHABILITARÁ A TODOS LOS CRAWLERS QUE NO SE ENCUENTRAN AQUÍ!" blocked_crawler_user_agents: "Palabra única insensible a mayúsculas y minúsculas en la cadena del agente de usuario que identifica a los que no se debe permitir el acceso al sitio. No se aplica si se define una lista de permitidos." - slow_down_crawler_user_agents: "Agentes de usuario de rastreadores web a los que se les debe limitar la velocidad según lo configurado en el ajuste «slow down crawler rate». Cada valor debe tener al menos 3 caracteres." + slow_down_crawler_user_agents: 'Agentes de usuario de rastreadores web a los que se les debe limitar la velocidad según lo configurado en el ajuste «slow down crawler rate». Cada valor debe tener al menos 3 caracteres.' slow_down_crawler_rate: "Si slow_down_crawler_user_agents es especificado, esta proporción aplicará para todos los crawlers (número de segundos de demora entre solicitudes)" content_security_policy: "Activar la política de seguridad de contenido (CSP)" content_security_policy_report_only: "Activar solo el informe de la política de seguridad de contenido (CPS)" @@ -2541,6 +2541,9 @@ es: system_messages: reviewables_reminder: subject_template: "Hay elementos pendientes en la cola de revisión" + text_body_template: + one: "Hay %{mentions} elemento recibido hace más de %{count} hora. [Por favor, revísalo](%{base_url}/review)." + other: "Hay %{mentions} elementos recibidos hace más de %{count} horas. [Por favor, revísalos](%{base_url}/review)." private_topic_title: "Tema #%{id}" contents_hidden: "Visita la publicación para ver su contenido." post_hidden: @@ -2623,6 +2626,26 @@ es: flags_agreed_and_post_deleted_for_responders: title: "Respuesta a un mensaje denunciado eliminada por el personal" subject_template: "Respuesta a un mensaje denunciado eliminada por el personal" + text_body_template: | + Hola, + + Esto es un mensaje automático enviado desde %{site_name} para hacerte saber que una [publicación](%{base_url}%{url}) a la que has respondido ha sido eliminada. + + %{flag_reason} + + La publicación fue denunciada por la comunidad y un miembro del equipo decidió eliminarla. + + ```markdown + %{flagged_post_raw_content} + ``` + + Esta era tu respuesta: + + ```markdown + %{flagged_post_response_raw_content} + ``` + + Para más detalles sobre el motivo del borrado, consulta las [directrices de la comunidad](%{base_url}/guidelines). usage_tips: text_body_template: | Si quieres unos consejos para empezar, [echa un vistazo a esta entrada de blog](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 378a10ebc8..d36cd02fd7 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -883,7 +883,6 @@ fa_IR: max_topic_title_length: "حداکثر طول مجاز عنوان موضوع به نویسه" min_personal_message_title_length: "حداقل طول مجاز عنوان برای پیام به نویسه" min_search_term_length: "حداقل طول واژه جستجوی معتبر به نویسه" - search_tokenize_chinese_japanese_korean: "اجبار جستجو برای tokenize کردن چینی/ژاپنی/کره‌ای حتی در سایت هایی که از این زبان‌ها استفاده نمی‌کنند" search_prefer_recent_posts: "اگر در انجمن بزرگ شما سرعت جست‌وجو کم است، این گزینه در ابتدا نمایه‌ی آخرین فرسته‌ها را می‌گردد." search_recent_posts_size: "تعداد فرسته‌های اخیر برای قرار گرفتن در نمایه" log_search_queries: "کارنامه جستجوهای انجام شده توسط کاربران" @@ -910,7 +909,6 @@ fa_IR: show_pinned_excerpt_mobile: "نمایش خلاصه موضوعات سنجاق شده در موبایل." show_pinned_excerpt_desktop: "نمایش خلاصه موضوعات سنجاق شده در کامپیوتر." post_onebox_maxlength: "حداکثر طول متن پیشنمایش لینک به نویسه." - blocked_onebox_domains: "لیست دامنه‌هایی که نباید پیش‌نمایش داشته باشند." max_oneboxes_per_post: "حداکثر تعداد پیش نمایش پیوند در یک نوشته" notification_email: "از: ایمیل آدرس زمان ارسال تمام ایمیل های ضروری سیستم استفاده شد. .دامنه مشخص شده در اینجا باید SPF, DKIM داشته باشد و سوابق PTR معکوس به درستی برای ورود ایمیل تنظیم شده باشد. " email_custom_headers: "فهرست pipe-delimited از سربرگ‌های ایمیل سفارشی " diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 4367012ce0..84cc118935 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -331,8 +331,6 @@ fi: too_many_links: one: "Uudet käyttäjät voivat lisätä vain yhden linkin viestiin." other: "Uudet käyttäjät voivat lisätä vain %{count} linkkiä viestiin." - contains_blocked_word: "Viestissäsi on kielletty sana: %{word}" - contains_blocked_words: "Viestissäsi on kiellettyjä sanoja: %{words}" spamming_host: "Et voi lisätä linkkiä tähän isäntään." user_is_suspended: "Hyllytetyt käyttäjät eivät saa luoda viestejä." topic_not_found: "Jokin on mennyt pieleen. Ehkä tämä ketju on suljettu tai poistettu sillä välin, kun katselit sitä?" @@ -1375,7 +1373,6 @@ fi: min_personal_message_title_length: "Yksityisviestin otsikon vähimmäismerkkimäärä" max_emojis_in_title: "Kuinka monta emojia enintään otsikossa" min_search_term_length: "Haun merkkien minimimäärä" - search_tokenize_chinese_japanese_korean: "Pakota haku käsittelemään kiinaa/japania/koreaa myös muunkielisillä sivustoilla" search_prefer_recent_posts: "Jos hakeminen suurelta foorumilta on hidasta, tämä asetus kokeilee indeksiä, jossa tuoreimmat viestit ovat ensin" search_recent_posts_size: "Kuinka monta tuoretta viestiä pidetään indeksissä" log_search_queries: "Pidä lokia käyttäjien tekemistä hauista" @@ -1417,7 +1414,6 @@ fi: show_pinned_excerpt_mobile: "Näytä katkelma kiinnitetyistä ketjuista mobiilinäkymässä." 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ä." - blocked_onebox_domains: "Verkko-osoitteet, joista ei luoda Onebox-esikatselua." allowed_inline_onebox_domains: "Verkko-osoitteet, joista luodaan minimoitu Onebox-esikatselu, jos niihin linkitetään määrittämättä otsikkoa." enable_inline_onebox_on_all_domains: "Ohita inline_onebox_domain_whitelist-asetus ja salli Onebox-esikatselut kaikista verkko-osoitteista." force_custom_user_agent_hosts: "Isännät, joille käytetään mukautettua onebox-käyttäjäagenttia kaikissa pyynnöissä. (Erityisen hyödyllinen isännille, jotka rajoittavat käyttäjäagentin pääsyä)." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index fda32a58f5..f6d82010a0 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -331,8 +331,6 @@ fr: too_many_links: one: "Nous sommes désolés, les nouveaux utilisateurs ne peuvent insérer qu'un seul lien par message." other: "Nous sommes désolés, les nouveaux utilisateurs ne peuvent insérer que %{count} liens par message." - contains_blocked_word: "Votre message contient un mot qui n'est pas permis : %{word}" - contains_blocked_words: "Votre message contient plusieurs mots qui ne sont pas permis : %{words}" spamming_host: "Désolé, vous ne pouvez pas insérer de lien vers ce domaine." user_is_suspended: "Les utilisateurs suspendus ne sont pas autorisés à publier des messages." topic_not_found: "Une erreur est survenue. Peut-être que ce sujet a été fermé ou supprimé pendant que vous le regardiez ?" @@ -1375,7 +1373,6 @@ fr: min_personal_message_title_length: "Longueur minimale pour un titre de message en nombre de caractères" max_emojis_in_title: "Nombre maximal d'émojis permis dans le titre d'un sujet" min_search_term_length: "Longueur minimale en caractères du terme à rechercher" - search_tokenize_chinese_japanese_korean: "Forcer la recherche à segmenter les textes chinois, japonais et coréens, même sur les sites n'utilisant pas ces langues par défaut." search_prefer_recent_posts: "Si les recherches dans votre forum sont lentes, cette option effectue l'indexation des messages les plus récents en premier" search_recent_posts_size: "Combien de messages récents à garder dans l'index" log_search_queries: "Archiver les requêtes de recherche des utilisateurs" @@ -1417,7 +1414,6 @@ fr: show_pinned_excerpt_mobile: "Afficher les extraits des sujets épinglés en vue mobile." show_pinned_excerpt_desktop: "Afficher les extraits des sujets épinglés en vue bureau." post_onebox_maxlength: "Longueur maximale en nombre de caractères d'un message Discourse intégré comme Onebox." - blocked_onebox_domains: "Une liste de domaines qui ne seront jamais transformés en Onebox." allowed_inline_onebox_domains: "Une liste de domaines qui seront transformés en Onebox s'ils ont été insérés sans titre" enable_inline_onebox_on_all_domains: "Ignorer le paramètre « inline_onebox_domain_allowlist » et permettre des Onebox en ligne pour tous les domaines." force_custom_user_agent_hosts: "Domaines pour lesquels utiliser un agent utilisateur personnalisé pour les requêtes de Onebox. (Cela est utile pour les domaines limitant les accès par agent utilisateur.)" diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml index 004112c92a..b3ff8475aa 100644 --- a/config/locales/server.gl.yml +++ b/config/locales/server.gl.yml @@ -317,8 +317,6 @@ gl: too_many_links: one: "Sentímolo, mais os novos usuarios só poden poñer unha ligazón nunha publicación." other: "Sentímolo, mais os novos usuarios só poden poñer %{count} ligazóns nunha publicación." - contains_blocked_word: "A súa publicación contén unha palabra que non está permitida: %{word}" - contains_blocked_words: "A súa publicación contén varias palabras que non están permitidas: %{words}" spamming_host: "Sentímolo, non pode publicar unha ligazón a ese servidor." user_is_suspended: "Aos usuarios suspendidos non se lles permite publicar." topic_not_found: "Algo foi mal. Talvez este tema se pechou ou se eliminou mentres o estaba a consultar?" @@ -1332,7 +1330,6 @@ gl: min_personal_message_title_length: "Extensión mínima do título dunha mensaxe en número de caracteres" max_emojis_in_title: "Número máximo de emojis permitido no título dun tema" min_search_term_length: "Extensión mínima dunha busca válida en número de caracteres" - search_tokenize_chinese_japanese_korean: "Forzar a busca para tokenizar chinés/xaponés/coreano incluso en sitios que non sexan CXC" search_prefer_recent_posts: "Se a busca no foro é lenta, esta opción proba primeiro nun índice das publicacións máis recentes" search_recent_posts_size: "Número de publicacións recentes que se manteñen no índice" log_search_queries: "Rexistro de buscas realizadas polos usuarios" @@ -1372,7 +1369,6 @@ gl: show_pinned_excerpt_mobile: "Mostrar o extracto de temas pegados na vista móbil." show_pinned_excerpt_desktop: "Mostrar o extracto de temas pegados na vista de escritorio." post_onebox_maxlength: "Lonxitude máxima en caracteres dunha publicación de Discourse en formato caixa (onebox)." - blocked_onebox_domains: "Unha listaxe de dominios que nunca se mostrarán en formato onebox." allowed_inline_onebox_domains: "Unha listaxe de dominios que se mostrarán en formato onebox en miniatura se son ligados sen un título" enable_inline_onebox_on_all_domains: "Ignorar o axuste do sitio inline_onebox_domain_whitelist e permitir incluír onebox en todos os dominios." force_custom_user_agent_hosts: "Hosts que se utilizarán para o axente de usuario personalizado de onebox en todas as solicitudes (especialmente útil para hosts que limitan o acceso por axente de usuario)." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 93424fec7a..dcabd77318 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -376,8 +376,8 @@ he: two: "משתמשים חדשים יכולים להוסיף רק %{count} קישורים בפוסט, עמך הסליחה." many: "משתמשים חדשים יכולים להוסיף רק %{count} קישורים בפוסט, עמך הסליחה." other: "משתמשים חדשים יכולים להוסיף רק %{count} קישורים בפוסט, עמך הסליחה." - contains_blocked_word: "הפוסט שלך מכיל מילים אסורות: %{word}" - contains_blocked_words: "הפוסט שלך מכיל מספר מילים שאסור להשתמש בהן: %{words}" + contains_blocked_word: "אסור לפרסם את המילה ‚%{word}’, עמך הסליחה." + contains_blocked_words: "אי אפשר לפרסם את זה, עמך הסליחה. מילים אסורות: %{words}." spamming_host: "אין לך אפשרות לפרסם קישור לאתר זה, עמך הסליחה." user_is_suspended: "משתמשים מושעים אינם מורשים לפרסם." topic_not_found: "משהו השתבש אולי נושא זה נסגר או נמחק בזמן שקראת אותו?" @@ -1465,6 +1465,7 @@ he: unreachable_themes: "לא הצלחתי לבדוק אם יש עדכונים לערכות העיצוב הבאות:" watched_word_regexp_error: "הביטוי הרגולרי למילים במעקב ‚%{action}’ שגוי. נא לבדוק את הגדרות המילים במעקב או ךהשבית את הגדרת האתר ‚ביטוי רגולרי למילים במעקב’." site_settings: + allow_bulk_invite: "לאפשר הזמנות במרוכז על ידי העלאת קובץ CSV" disabled: "מושבת" display_local_time_in_user_card: "להציג את הזמן המקומי בהתאם לאזור הזמן של המשתמש כאשר כרטיס המשתמש שלו נפתח." censored_words: "מלים שיוחלפו אוטומטית ב־■■■■" @@ -1485,7 +1486,8 @@ he: min_personal_message_title_length: "אורך הכותרת המזערי המותר להודעה בתווים" max_emojis_in_title: "מספר האמוג׳ים המרבי בכותרת נושא" min_search_term_length: "מספר התווים המינמלי התקין כאורך מונח לחיפוש" - search_tokenize_chinese_japanese_korean: "אלצו את החיפוש לנתח סינית/יפנית/קוריאנית גם באתרים שאינם בשפות אלו" + search_tokenize_chinese: "לכפות על החיפוש לפרק טקסט בסינית אפילו באתרים שאינם סיניים" + search_tokenize_japanese: "לכפות על החיפוש לפרק טקסט ביפנית אפילו באתרים שאינם יפניים" search_prefer_recent_posts: "אם חיפוש בפורום הגדול שלך אטי, אפשרות זו מנסה לסדר במפתח את הפוסטים החדשים יותר בהתחלה" search_recent_posts_size: "כמה פוסטים חדשים לשמור באינדקס" log_search_queries: "רישום שאילתות חיפוש שמתבצעות על ידי משתמשים" @@ -1512,7 +1514,7 @@ he: editing_grace_period_max_diff: "מספר תווים מרבי שמותר לשנות בטווח עריכת חסד, אם נערכו למעלה מהכמות הזאת יש ליצור מהדורה חדשה לפוסט (דרגות אמון 0 ו־1)" editing_grace_period_max_diff_high_trust: "מספר תווים מרבי שמותר לשנות בטווח עריכת חסד, אם נערכו למעלה מהכמות הזאת יש ליצור מהדורה חדשה לפוסט (דרגות אמון 2 ומעלה)" staff_edit_locks_post: "פוסטים יינעלו מפני עריכה אם נערכו על ידי הסגל." - post_edit_time_limit: "יוצרי פוסטים בדרגת אמון 1 או 0 יכולים לערוך את הפוסטים שלהם עד (n) דקות לאחר הפרסום. 0 כדי לאפשר לערוך לעד.
" + post_edit_time_limit: "יוצרי פוסטים בדרגת אמון 1 או 0 יכולים לערוך את הפוסטים שלהם עד (n) דקות לאחר הפרסום. 0 כדי לאפשר לערוך לעד." tl2_post_edit_time_limit: "מחברים בדרגת אמון 2 ומעלה יכולים לערוך את הפוסטים שלהם (n) דקות לאחר פרסומם. 0 ישבית את ההגבלה הזאת." edit_history_visible_to_public: "לאפשר לכולם לראות גרסאות קודמות של פוסט ערוך. כאשר אפשרות זו מושבתת, רק חברי סגל יכולים לצפות בהן." delete_removed_posts_after: "פוסטים שהוסרו על ידי מחבריהם ימחקו באופן אוטומטי לאחר (n) שעות. אם הגדרה זו מכוונת ל-0, פוסטים ימחקו מיידית." @@ -1523,13 +1525,13 @@ he: fixed_category_positions: "אם אפשרות זו מסומנת, תוכלו לארגן את הקטגוריות כך שיופיעו בסדר קבוע. אם האופציות אינן מסומנות, הקטגוריות יסודרו על פי סדר הפעילות שהתבצעה בהן." fixed_category_positions_on_create: "אם האפשרות תסומן, סדר הקטגוריות יוגדר בתפריט יצירת נושא (דורש fixed_category_positions)." add_rel_nofollow_to_user_content: 'הוספת התווית rel nofollow לכל תוכן שפורסם על ידי המשתמש/ת, פרט לקישורים פנימיים (כולל מתחם הורה parent domains). אם תשנו אפשרות זו, עליכם לאפות מחדש את כל הפרסומים עם: "rake posts:rebake"' - exclude_rel_nofollow_domains: "רשימת שמות התחום שיש להוסיף לקישורים אליהם nofollow. הוספת example.com תאפשר אוטומטית גם את sub.example.com. בתור התחלה, עליך להוסיף את שם התחום של האתר הזה כדי לסייע לסורקי רשת (crawlers) למצוא את כל התוכן. אם חלקים אחרים מהאתר שלך מתארחים בשמות תחום אחרים, אז גם אותם." + exclude_rel_nofollow_domains: "רשימת שמות התחום שאין להוסיף לקישורים אליהם nofollow. הוספת example.com תאפשר אוטומטית גם את sub.example.com. בתור התחלה, עליך להוסיף את שם התחום של האתר הזה כדי לסייע לסורקי רשת (crawlers) למצוא את כל התוכן. אם חלקים אחרים מהאתר שלך מתארחים בשמות תחום אחרים, אז גם אותם." post_excerpt_maxlength: "אורך מקסימלי של קטע פוסט / סיכום." topic_excerpt_maxlength: "האורך המרבי של מקטע מתוך / סיכום של נושא, נוצר מהפוסט הראשון בנושא." show_pinned_excerpt_mobile: "הצגת קטע בנושאים נעוצים במבט ניידים." show_pinned_excerpt_desktop: "הצגת קטע בנושאים נעוצים בתצוגת מחשב-שולחני." post_onebox_maxlength: "אורך מרבי של פוסט ב־Discourse בתיבת תחימה בתווים." - blocked_onebox_domains: "רשימת שמות תחום שלעולם לא יופיעו בתיבות תחימה." + blocked_onebox_domains: "רשימה של שמות תחום שלא יתחמו בתיבה, למשל: wikipedia.org\n(אין תמיכה בתווי הכללה כגון * או ?)" allowed_inline_onebox_domains: "רשימת שמות מתחם שיופיעו בתיבות תחימה בתצורה המזערית שלהם אם הקישור מופיע ללא כותרת" enable_inline_onebox_on_all_domains: "להתעלם מהגדרת האתר inline_onebox_domain_allowlist (רשימת היתר של שמות תחום לתיבת תחימה משולבת) ולאפשר תיבות תחימה לכל שמות המתחם." force_custom_user_agent_hosts: "מארחים שמולם משתמשים בסוכן משתמש onebox בהתאמה אישית לכל הבקשות. (חיוני במיוחד למארחים שמגבילים גישה בהתאם לסוכן משתמש)." @@ -1565,7 +1567,7 @@ he: enable_system_message_replies: "מאפשר למשתמשים להגיב להודעות מערכת אפילו כשהודעות אישיות מושבתות" enable_long_polling: "באס הודעות שמשמש להתראות יכול להשתמש בתשאול ארוך (long polling)" enable_chunked_encoding: "הפעלת תגובות קידוד מחולקות מצד השרת. תכונה זו עובדת ברוב התצורות אך חלק מהמתווכים עשויים לכלוא כצעד ביניים, מה שעלול לגרום להאטה" - long_polling_base_url: "בסיס ה-URL שנמצא בשימוש עבור long polling (כאשר CDN מחזיר תוכן דינמי, זכרו להגדיר את ערך זה ל-Origin pull, דוגמת http://origin.site.com)" + long_polling_base_url: "כתובת הבסיס שמשמשת לתשאול ארוך (כאשר CDN מגיש תוכן דינמי, יש להגדיר זאת למשיכה המקורית) למשל: http://origin.site.com" long_polling_interval: "כמות הזמן שהשרת צריך לחכות לפני שעונה ללקוחות, כאשר אין מידע לשליחה (משתמשים רשומים מחוברים למערכת בלבד)" polling_interval: "כאשר לא מבצעים תשאול ארוך (long polling), כל כמה זמן לקוחות מחוברים למערכת יבצעו poll, במילי-שניות" anon_polling_interval: "באיזו תכיפות לקוחות אלמוניים יתשאלו, במילישניות" @@ -1573,12 +1575,12 @@ he: hide_post_sensitivity: "הסבירות שפוסט שסומן בדגל יוסתר" silence_new_user_sensitivity: "הסבירות שמשתמש חדש יושתק עקב סימוני דגל ספאם" auto_close_topic_sensitivity: "הסבירות שנושא שסומן בדגל ייסגר אוטומטית" - cooldown_minutes_after_hiding_posts: "מספר הדקות שמשתמשים חייבים לחכות לפני שהם יכולים לערוך פוסט שהוסתר בגלל דיגלול קהילתי" + cooldown_minutes_after_hiding_posts: "מספר הדקות שמשתמשים חייבים לחכות לפני שהם יכולים לערוך פוסט שהוסתר בגלל סימון קהילתי" max_topics_in_first_day: "הכמות המקסימלית של נושאים שמשתמשים מורשים ליצור ב 24 השעות הראשונות לאחר הפוסט הראשון שלהם" max_replies_in_first_day: "הכמות המקסימלית של תגובות שמשתמשים מורשים ליצור ב 24 השעות הראשונות אחרי יצירת הפוסט הראשון שלהם" - tl2_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור tl2 (משתמש) באמצעות הכפלה במספר זה. " + tl2_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור דרגת אמון 2 (חברים) באמצעות הכפלה במספר זה" tl3_additional_likes_per_day_multiplier: "הכפלה במספר הזה מגדילה את מגבלת הלייקים ליום עבור דרגת אמון 3 (רגיל)" - tl4_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור tl4 (מנהיג) באמצעות הכפלה במספר זה. " + tl4_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור דרגת אמון 4 (הנהגה) באמצעות הכפלה במספר זה" tl2_additional_edits_per_day_multiplier: "להגדיל את כמות העריכות האפשרית ביום עבור דרגת אמון 2 (חברים) באמצעות הכפלה במספר זה" tl3_additional_edits_per_day_multiplier: "להגדיל את כמות העריכות האפשרית ביום עבור דרגת אמון 3 (רגיל) באמצעות הכפלה במספר זה" tl4_additional_edits_per_day_multiplier: "להגדיל את כמות העריכות האפשרית ביום עבור דרגת אמון 4 (הנהגה) באמצעות הכפלה במספר זה" @@ -1616,14 +1618,14 @@ he: allowed_iframes: "רשימה של קידומות שמות מתחם בתור מקור של iframe שאפשר לאפשר בבטחה בפוסטים ב־Discourse" allowed_crawler_user_agents: "סוכני משתמשים של סורקי רשת שמורשים לגשת לאתר. אזהרה! שינוי הגדרה זו תחסום את כל הסורקים שאינם מופיעים כאן!" blocked_crawler_user_agents: "מילה ייחודית שאינה תלוית רישיות במחרוזת סוכן המשתמש (user agent) שמגדירה לאילו סורקי רשת אין הרשאת גישה לאתר. לא חל במקרה שהגדרת רשימת היתר." - slow_down_crawler_user_agents: "סוכני משתמשים של סורקי רשת שכמות הגישה שלהם תוגבל בהתאם להגדרה „האטת קצב סורקי רשת”. כל ערך חייב להיות באורך של 3 תווים לפחות." + slow_down_crawler_user_agents: 'סוכני משתמשים של סורקי רשת שכמות הגישה שלהם תוגבל בהתאם להגדרה „האטת קצב סורקי רשת”. כל ערך חייב להיות באורך של 3 תווים לפחות.' slow_down_crawler_rate: "אם הוגדר slow_down_crawler_user_agents (האטת סורקי אינטרנט) קצב זה יחול על כל סורקי האינטרנט (כמות ההמתנה בשניות בין בקשות)" content_security_policy: "הפעלת Content-Security-Policy (מדיניות אבטחת תוכן)" content_security_policy_report_only: "הפעלת Content-Security-Policy-Report-Only (מדיניות אבטחת תוכן בדיווח בלבד)" content_security_policy_collect_reports: "הפעלת איסוף דוחות הפרה של CSP (מדיניות אבטחת תוכן) תחת ‎/csp_reports" content_security_policy_frame_ancestors: "להגביל מי יכול להטמיע את האתר הזה עם מסגרות iframes דרך CSP. ניתן לשלוח במארחים המורשים דרך הטמעה" content_security_policy_script_src: "מקורות סקריפטים נוספים ברשימת ההיתר. המארח וה־CDN הנוכחיים נכללים כבררת מחדל. ניתן לעיין בהתקפות XSS בעזרת CSP - מדיניות אבטחת תוכן." - invalidate_inactive_admin_email_after_days: "חשבונות מנהלים שלא ביקרו באתר מעל כמות כזו של ימים יאלצו לאמת מחדש את כתובת הדוא״ל שלהם כדי לשוב ולהיכנס. להגדיר כ־0 כדי לנטרל." + invalidate_inactive_admin_email_after_days: "חשבונות מנהלים שלא ביקרו באתר מעל כמות כזו של ימים יאלצו לאמת מחדש את כתובת הדוא״ל שלהם כדי לשוב ולהיכנס. להגדיר כ־0 כדי להשבית." top_menu: "החליטו אילו פריטים יופיעו בניווט עמוד הבית ובאיזה סדר לדוגמה |אחרונים|חדשים|קטגוריות|מובילים|נקראו|פורסמו|סימניות" post_menu: "החליטו אילו פריטים מופיעים בתפריט הפוסט, ובאיזה סדר. למשל like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "פריטי התפריט להסתרה כברירת מחדל בתפריט הפוסט, אלא אם כן נלחץ לחצן ההרחבה." @@ -1754,8 +1756,8 @@ he: max_post_deletions_per_day: "מספר הפוסטים המרבי שמשתמש יכול למחוק ביום אחד. 0 ישבית מחיקת פוסטים." invite_link_max_redemptions_limit: "כמות הניצולים המרבית שמורשית לקישורים הזמנה לא יכולה לעבור את הערך הזה." invite_link_max_redemptions_limit_users: "כמות הניצולים המרבית שמורשית לקישורי הזמנה שנוצרו על ידי משתמשים רגילים לא יכולה לעבור את הערך הזה." - alert_admins_if_errors_per_minute: "מספר השגיאות בדקה לפני ששולחים התראה למנהל. ערך של 0 מנטרל אפשרות זו. שימו לב: מצריך הרצה מחדש." - alert_admins_if_errors_per_hour: "מספר השגיאות בשעה לפני ששולחים התראה למנהל. ערך של 0 מנטרל אפשרות זו. שימו לב: מצריך הרצה מחדש." + alert_admins_if_errors_per_minute: "מספר השגיאות בדקה לפני שליחת התראה למנהל. 0 משבית אפשרות זו. לתשומת לבך: דורש הפעלה מחדש." + alert_admins_if_errors_per_hour: "מספר השגיאות בשעה לפני שליחת התראה למנהל. 0 משבית אפשרות זו. לתשומת לבך: דורש הפעלה מחדש." categories_topics: "מספר הנושאים להציג בעמוד ה־‎/categories (קטגוריות). אם הוגדר ל־0, יתבצע ניסיון למצוא ערך שישאיר את שתי העמודות סימטריות (קטגוריות ונושאים)." suggested_topics: "מספר הנושאים המוצעים שיופיעו בתחתית הנושא המוצג." limit_suggested_to_category: "להציג רק נושאים מהקטגוריה הנוכחית ברשימת הנושאים המומלצים." @@ -1848,7 +1850,7 @@ he: max_here_mentioned: "מספר האנשים המרבי שמוזכרים על ידי ‎@here." min_trust_level_for_here_mention: "דרגת האמון המזערית המורשית להזכיר את ‎@here." create_thumbnails: "יצירת תמונות מוקטנות והארת תמונות גדולות מידי מלהיכלל בפוסט." - email_time_window_mins: "המתינו (n) דקות לפני משלוח כל התראת מייל, כדי לאפשר למשתמשים הזדמנות לערוך ולוודא באופן סופי את הפוסטים שלהם." + email_time_window_mins: "להמתין (n) דקות בטרם כל שליחת התראה בדוא״ל, כדי לאפשר למשתמשים הזדמנות לערוך ולוודא באופן סופי את הפוסטים שלהם." personal_email_time_window_seconds: "להמתין (n) שניות בטרם שליחת הודעות בדוא״ל עם התראות על הודעות אישיות, כדי לתת למשתמשים הזדמנות לסיים את עריכת ההודעות שלהם." email_posts_context: "כמה תגובות קודמות יש לכלול כהקשר בהתראות בדוא״ל." flush_timings_secs: "באיזו תדירות אנחנו מזרימים מידע לשרת, בשניות." @@ -1862,14 +1864,14 @@ he: min_title_similar_length: "האורך המינימלי של כותרת לפני שהיא נבדקת עבור איתור נושאים דומים." desktop_category_page_style: "סגנון ויזואלי לדף /categories." category_colors: "רשימה של ערכי צבעים הקסדצימליים מותרים לסימון קטגוריות." - category_style: "סגנון ויזואלי עבור עיטורי קטגוריות." + category_style: "סגנון חזותי עבור עיטורי קטגוריות." default_dark_mode_color_scheme_id: "ערכת הצבעים לשימוש במצב כהה." dark_mode_none: "בלי" max_image_size_kb: "גודל ההעלאה המרבי בקילובתים. יש להגדיר זאת ב־nginx‏ (client_max_body_size) / apache או גם למתווך. תמונות שחורגות מהגודל הזה וקטנות מההגדרה client_max_body_size יערכו כך שגודלן יתאים להעלאה." max_attachment_size_kb: "הגודל המקסימלי בקילו-בתים (kBs) של קבצים להעלאה. הגדרה זו חייבת להיות מוגדרת ב-nginx (client_max_body_size) / apache או בפרוקסי." authorized_extensions: "רשימה של סיומות מותרות להעלאה (השתמשו ב '*' כדי לאפשר את כל סוגי הקבצים)" authorized_extensions_for_staff: "רשימת סיומות קבצים שמותר לחברי סגל להעלות בנוסף לרשימה שמוגדרת תחת הגדרת האתר `authorized_extensions` (‚*’ כדי לקבל את כל סוגי הקבצים)" - theme_authorized_extensions: "רשימת סיומות קבצים שמורשות עבור העלאת תמות (השתמשו ב '*' כדי לאפשר את כל סוגי הקבצים)" + theme_authorized_extensions: "רשימת סיומות קבצים שמורשים במסגרת העלאת ערכת עיצוב (‚*’ כדי לאפשר את כל סוגי הקבצים)" max_similar_results: "כמה נושאים דומים להציג מעל לעורך כאשר מחברים נושא חדש. ההשוואה מבוססת על הכותרת וגוף הפוסט." max_image_megapixels: "מספר המגה פיקסלים המרבי שמותר בתמונה. תמונות עם מגה פיקסלים מעבר לכמות זו תידחנה." title_prettify: "מניעת טעויות נפוצות בכותרת, בכללן טעויות עם אותיות גדולות באנגלית, מספר ! ו ?, נקודה מיותרת בסוף, וכד׳" @@ -1904,7 +1906,7 @@ he: num_flaggers_to_close_topic: "מספר מינימלי של דגלים שונים שנדרש כדי להשהות באופן אוטומטי אפשות להתערב בנושא" num_hours_to_close_topic: "מספר שעות לעצירת נושא לצורך התערבות." auto_respond_to_flag_actions: "הפעלת תגובה אוטומטית עם הסרת דגל." - min_first_post_typing_time: "זמן מינימלי במילי-שניות שמשתמש חייב להקיש בזמן הפוסט הראשון, אם הסף לא נעבר הפוסט אוטומטית יכנס לתור של אלו שצריכים אישור. קיבעו 0 כדי לנטרל (לא מומלץ)" + min_first_post_typing_time: "הזמן המזערי במילישניות שעל משתמש להקליד במהלך הפוסט הראשון, אם לא הגיע לסף הפוסט ייכנס אוטומטית לתור ההמתנה לאישור. 0 להשבתה (לא מומלץ)" auto_silence_fast_typers_on_first_post: "להשתיק אוטומטית משתמשים שאינם עונים ל־min_first_post_typing_time (זמן התגובה המזערי לפוסט ראשון)" auto_silence_fast_typers_max_trust_level: "דרגת האמון המרבית להשתקה אוטומטית של קלדנים נמרצים" reviewable_claiming: "האם צריך לדרוש תוכן שמיועד לסקירה לפני שאפשר לפעול לגביו?" @@ -1919,7 +1921,7 @@ he: alternative_reply_by_email_addresses: "רשימה של כמה תבניות לתגובות במייל באמצעות כתובות מייל נכנסות. למשל: %%{reply_key}@reply.example.com|replies+%%{reply_key}@example.com" incoming_email_prefer_html: "להשתמש ב־HTML במקום בטקסט בהודעות דוא״ל נכנסות." strip_incoming_email_lines: "להסיר רווחים מובילים וסוגרים בכל שורה בהודעות דוא״ל נכנסות." - disable_emails: "למנוע מ־Discourse לשלוח כל סוג של הודעות דוא״ל. יש לבחור ב־‚כן’ כדי לנטרל את כל הודעות הדוא״ל לכל המשתמשים. ניתן גם לבחור ב־‚ללא סגל’ כדי לנטרל את ההודעות למשתמשים שאינם נמנים עם הסגל." + disable_emails: "למנוע מ־Discourse לשלוח כל סוג של הודעות דוא״ל. יש לבחור ב־‚כן’ כדי להשבית את כל הודעות הדוא״ל לכל המשתמשים. ניתן גם לבחור ב־‚ללא סגל’ כדי להשבית את ההודעות למשתמשים שאינם נמנים עם הסגל." strip_images_from_short_emails: "הסרת תמונות מהודעות דוא״ל שגודלן אינו עולה על 2800 בתים" short_email_length: "אורכו של דוא״ל קצר בבתים" display_name_on_email_from: "להציג שמות מלאים בשדה „מאת” בדוא״ל" @@ -2024,7 +2026,7 @@ he: allow_profile_backgrounds: "אפשרו למשתמשים להעלות רקעים לפרופיל." sequential_replies_threshold: "מספר הפוסטים שעל משתמש לפרסם אחד-לאחר-השני בנושא לפני שמזכירים לו שהוא משאיר יותר מידי תגובות ברצף." get_a_room_threshold: "מספר פוסטים שמשתמשים צריכים להכין לאותו אדם באותו הנושא לפני שהם מוזהרים." - enable_mobile_theme: "מכשירים ניידים משתמשים בתבנית ידידותית אליהם, עם היכולת להחליף אותה לאתר המלא. נטרלו זאת אם אתם מעוניינים ב stylesheet מותאם שהוא רספונסיבי לחלוטין." + enable_mobile_theme: "מכשירים ניידים משתמשים בערכת עיצוב מותאמת לניידים לצד האפשרות לעבור לאתר המלא. ניתן להשבית זאת כדי להשתמש בערכת סגנון מסתגלת לחלוטין." dominating_topic_minimum_percent: "איזה אחוז מהפוסטים משתמש צריך לייצר בנושא לפני שיקבל תזכורת לגבי שתלטנות יתר על הנושא." disable_avatar_education_message: "ניטרול הודעה שמלמדת על שינוי דמות." suppress_uncategorized_badge: "אל תציגו את העיטור לנושאים נטולי קטגוריה ברשימת הנושאים." @@ -2044,17 +2046,17 @@ he: app_association_android: "תכני נקודת הקצה ‎.well-known/assetlinks.json שמשמשת לטובת ה־API של Google's Digital Asset Links." app_association_ios: "תכני נקודת הקצה apple-app-site-association שמשמשת ליצירת Universal Links (קישורים אוניברסליים) בין האתר הזה ויישומוני iOS." share_anonymized_statistics: "שיתוף סטטיסטיקת שימוש אלמונית." - auto_handle_queued_age: "לטפל אוטומטית ברשומות שממתינות לסקירה לאחר כמות כזו של ימים. ללא התחשבות בדגלים. פוסטים ומשתמשים שממתינים בתור יידחו. יש להגדיר ל־0 כדי לנטרל את התכונה הזו." + auto_handle_queued_age: "לטפל אוטומטית ברשומות שממתינות לסקירה לאחר כמות כזו של ימים. ללא התחשבות בדגלים. פוסטים ומשתמשים שממתינים בתור יידחו. יש להגדיר ל־0 כדי להשבית את התכונה הזו." penalty_step_hours: "עונשי ברירת מחדל להשתקה או השעייה של משתמשים בשעות. ברירת המחדל לעבירה הראשונה היא הערך הראשון, ברירת המחדל לעבירה השנייה היא הערך השני וכן הלאה וכן הלאה." svg_icon_subset: "הוספת סמלים נוספים מתוך FontAwesome 5 אותם ברצונך לכלול בין המשאבים שלך. יש להשתמש בקידומת ‚fa-‎’ לסמלים אחידים, ‚far-‎’ לסמלים רגילים וב־‚fab-‎’ לסמלים ממותגים." - max_prints_per_hour_per_user: "מספר מקסימלי של צפיות בדף /print (הדפסה) (קיבעו ל 0 כדי לנטרל)" + max_prints_per_hour_per_user: "מספר החשיפות המרבי לדף ‎/print (0 להשבתה)" full_name_required: "שם מלא הוא שדה נדרש לפרופיל משתמש/ת." enable_names: "הצגת השם המלא של המשתמש בפרופיל, כרטיס המשתמש והודעות הדוא״ל של המשתמש. ניתן להשבית כדי להסתיר את השם המלא בכל מקום." display_name_on_posts: "הצגת שמם המלא של משתמשים בפוסטים שלהם, בנוסף ל@שם_המשתמש שלהם." show_time_gap_days: "אם שני פוסטים נוצרים בהפרש כזה של ימים זה מזה, להציג את הפרש הזמן בנושא." short_progress_text_threshold: "לאחר שמספר הפוסטים בנושא עוברים את המספר הזה, מד ההתקדמות יציג רק את המספר של הפוסט הנוכחי. אם תשנו את רוחב מד ההתקדמות, ייתכן שתצטרכו לשנות ערך זה." default_code_lang: "הדגשת תחביר כבררת מחדל שתחול על מקטעי קוד מ־GitHub (auto,‏ nohighlight,‏ ruby,‏ python וכו׳)" - warn_reviving_old_topic_age: "כאשר מישהם מתחילים להגיב לנושא שבו התגובה האחרונה היא בת יותר מכמה ימים, אזהרה תוצג. נטרלו באמצעות הזנה של 0." + warn_reviving_old_topic_age: "כשמשתמש מתחיל להגיב לנושא בו התגובה האחרונה נכתבה לפני למעלה מכמות זו של ימים, תופיע אזהרה. 0 להשבתה." autohighlight_all_code: "לחייב שימוש בקוד הדגשה לכל קוד מעוצב מראש (preformatted code blocks) אפילו אם הם אינם מציינים את השפה." highlighted_languages: "כללי הדגשת תחביר להכללה. (אזהרה: הכללה של שפות רבות מדי פוגעת בביצועים) להדגמה: https://highlightjs.org/static/demo" show_copy_button_on_codeblocks: "הוספת כפתור למקטעי קוד כדי להעתיק את תוכני המקטע ללוח הגזירים של המשתמש." @@ -2072,12 +2074,12 @@ he: show_create_topics_notice: "אם לאתר פחות מ־5 נושאים פומביים, יש להציג מודעה המבקשת מהמנהלים ליצור נושאים נוספים." delete_drafts_older_than_n_days: "מחקו טיוטות בנות יותר מ (n) ימים." delete_merged_stub_topics_after_days: "מספר הימים להמתנה לפני מחיקה אוטומטית של נושאים מקוצרים שמוזגו במלואם. יש להגדיר ל־0 כדי לא למחוק נושאים מקוצרים לעולם." - bootstrap_mode_min_users: "מספר משתמשים מינימלי שנדרש כדי לנטרל מצב איתחול (קבעו ל 0 כדי לנטרל)" + bootstrap_mode_min_users: "מספר משתמשים מזערי שנדרש כדי להשבית מצב ראשוניות (0 להשבתה)" prevent_anons_from_downloading_files: "למנוע ממשתמשים אלמוניים להוריד קבצים מצורפים." secure_media: 'מגביל גישה לכל ההעלאות (תמונות, סרטונים, שמע, טקסט, קובצי PDF, ארכיוני zip ועוד). אם מופעלת האפשרות „דרישת כניסה”, רק משתמשים שנכנסו למערכת יכולים לגשת להעלאות. אחרת, הגישה תוגבל רק להעלאות מסוגי מדיה בהודעות פרטיות ובקטגוריות פרטיות. אזהרה: מדובר בהגדרה מסובכת שדורשת הבנה עמוקה בניהול המערכת. יש לעיין בנושא המדיה המאובטחת ב־Meta לקבלת פרטים נוספים.' secure_media_allow_embed_images_in_emails: "מאפשר להטמיע תמונות מאובטחות שבדרך כלל יידחסו אם הגודל שלהן קטן מההגדרה ‚secure media max email embed image size kb’ (גודל תמונה מרבי להטמעה כמדיה מאובטחת בקילובתים)." secure_media_max_email_embed_image_size_kb: "גודל החיתוך לתמונות מאובטחות שיוטמעו בהודעות דוא״ל אם ההגדרה ‚secure media allow embed in emails’ (לאפשר הטמעת מדיה מאובטחת בהודעות דוא״ל) פעילה. אם ההגדרה הזאת אינה פעולה להגדרה זו אין שום משמעות." - slug_generation_method: "נא לבחור שיטת יצירת כתובת מופשטת. ‚מוצפן’ ייצר מחרוזת עם קידוד באחוזים, ‚ללא’ ישבית את הכתובת המופשטת לחלוטין." + slug_generation_method: "נא לבחור שיטת יצירת כתובת מופשטות. ‚מוצפן’ ייצר מחרוזת עם קידוד באחוזים, ‚ללא’ ישבית את הכתובת המופשטת לחלוטין." enable_emoji: "הפעלת אמוג׳י" enable_emoji_shortcuts: "חייכנים נפוצים כגון ‎:) :p :(‎ יומרו לאמוג׳ים" emoji_set: "איך בא לך את האמוג׳י שלך?" @@ -2087,7 +2089,7 @@ he: approve_unless_trust_level: "פוסטים של משתמשים מתחת לדרגת אמון זו חייבים לעבור אישור" approve_new_topics_unless_trust_level: "נושאים חדשים של משתמשים מתחת לדרגת אמון זו חייבים לעבור אישור" approve_unless_staged: "יש לאשר נושאים ופוסטים חדשים עבור משתמשים מבוימים" - notify_about_queued_posts_after: "אם יש פוסטים שממתינים לסקירה מעבר לכמות כזו של שעות, יש לשלוח התראה לכל המפקחים. יש להגדיר ל־0 כדי לנטרל את ההתראות האלה." + notify_about_queued_posts_after: "אם יש פוסטים שממתינים לסקירה מעבר לכמות כזו של שעות, יש לשלוח התראה לכל המפקחים. 0 משבית את ההתראות האלה." auto_close_messages_post_count: "מספר פוסטים מקסימלי בהודעה לפני שהיא נסגרת אוטומטית (0 לניטרול)" auto_close_topics_post_count: "מספר מקסימלי של פוסטים בנושא לפני שהוא נסגר אוטומטית (0 להשבתה)" auto_close_topics_create_linked_topic: "ליצור נושא חדש מקושר כאשר נושא נסגר אוטומטית עקב ההגדרה ‚לסגור אוטומטית נושאים בהתאם לכמות פוסטים’" @@ -2179,6 +2181,7 @@ he: share_quote_visibility: "קביעה מתי להציג כפתורי שיתוף ציטוטים: אף פעם למשתמשים אלמוניים בלבד או לכל המשתמשים. " create_revision_on_bulk_topic_moves: "יצירת מהדורה לפוסטים הראשונים כאשר הנושאים מועברים לקטגוריה חדשה במרוכז." allow_changing_staged_user_tracking: "לאפשר שינוי העדפות של קטגוריית המשתמשים המבוימים ותגיות על ידי ההנהלה." + use_email_for_username_and_name_suggestions: "להשתמש בחלק הראשון של כתובת הדוא״ל כשם משתמש והצעה לשם. נא לשים לב שזה מקל על אנשים לנחש את כתובות הדוא״ל המלאות (כיוון שרוב האנשים חולקים שירותים נפוצים כגון `gmail.com`)." errors: invalid_css_color: "צבע שגוי. נא למלא את שם הצבע או ערך הקסדצימלי." invalid_email: "כתובת דוא״ל שגויה." @@ -2198,6 +2201,7 @@ he: invalid_json: "JSON פגום." invalid_reply_by_email_address: "הערך חייב להכיל '%{reply_key}' ולהיות שונה ממייל ההתראה." invalid_alternative_reply_by_email_addresses: "על כל הערכים לכלול ‚%{reply_key}’ ולהיות שונים מהדוא״ל לעדכונים." + invalid_domain_hostname: "אסור לכלול את התווים * או ?." pop3_polling_host_is_empty: "עליכם לקבוע 'שרת תשאול pop3' לפני שתאפשרו תשאול POP3." pop3_polling_username_is_empty: "עליכם לקבוע 'שם-משתמש לתשאול pop3' לפני שתאפשרו תשאול POP3." pop3_polling_password_is_empty: "עליכם לקבוע 'סיסמת תשאול pop3' לפני שתאפשרו תשאול POP3." @@ -2206,7 +2210,7 @@ he: email_polling_disabled: "עליכם לאפשר ידני או תשאול POP3 לפני שתאפשרו מענה באמצעות מייל." user_locale_not_enabled: "לפני שתוכלו להפעיל אפשרות זאת, יש לאפשר \"הגדרות מקומיות מותאמות משתמש/ת\"" invalid_regex: "ביטוי רגולרי לא תקין או לא מותר." - email_editable_enabled: "עליך לנטרל את ‚עריכת דוא״ל אפשרית’ בטרם הפעלת הגדרה זו." + email_editable_enabled: "עליך להשבית את ‚עריכת דוא״ל אפשרית’ בטרם הפעלת הגדרה זו." staged_users_disabled: "עליך לאפשר ‚משתמשים מבוימים’ בטרם הפעלת הגדרה זו." reply_by_email_disabled: "עליך להפעיל את ‚תגובה בדוא״ל’ בטרם הפעלת הגדרה זו." discourse_connect_url_is_empty: "עליך להגדיר ‚כתובת discourse connect’ בטרם הפעלת הגדרה זו." @@ -2227,6 +2231,8 @@ he: unicode_usernames_avatars: "התמונות הייצוגיות הפנימיות של המערכת אינן תומכות בשמות משתמשים עם יוניקוד." list_value_count: "הרשימה חייבת להכיל בדיוק %{count} ערכים." google_oauth2_hd_groups: "עליך להגדיר את ‚google oauth2 hd’ בטרם הפעלת ההגדרה הזאת." + search_tokenize_chinese_enabled: "עליך להשבית את ‚search_tokenize_chinese’ (חיפוש מפרק סינית) בטרם הפעלת הגדרה זו." + search_tokenize_japanese_enabled: "עליך להשבית את ‚search_tokenize_japanese’ (חיפוש מפרק יפנית) בטרם הפעלת הגדרה זו." placeholder: discourse_connect_provider_secrets: key: "www.example.com" @@ -3637,9 +3643,9 @@ he: title: "התראת דוא״ל ישן" subject_template: "[%{email_prefix}] כתובת הדוא״ל שלך הוחלפה" text_body_template: | - זוהי הודעה אוטומטית כדי ליידע אתכם שכתובת המייל שלכם עבור %{site_name} שונתה. אם זה נעשה בטעות, אנא צרו קשר עם מנהל האתר. + זוהי הודעה אוטומטית כדי ליידע אותך שכתובת הדוא״ל שלך עבור %{site_name} הוחלפה. אם זה נעשה בטעות, נא ליצור קשר עם הנהלת האתר. - כתובת המייל שלכם שונתה ל: + כתובת הדוא״ל שלך שונתה לכדי: %{new_email} notify_old_email_add: @@ -4439,7 +4445,7 @@ he: name: "המשתמש/ת החדש/ה של החודש" description: תרומות יוצאות מהכלל בחודש הראשון שלהם long_description: | - עיטור זה מוענק כדי לברך שני משתמשים חדשים בכל חודש בעבור תרומות מצטיינות שלהם, שנמדדות לפי תדירות הלייקים לפוסטים שלכם, וממי הם ניתנו. + עיטור זה מוענק כדי לברך שני משתמשים חדשים בכל חודש בעבור תרומות מצטיינות שלהם, שנמדדות לפי תדירות הלייקים לפוסטים שלהם, וממי הם ניתנו. enthusiast: name: נלהב description: ביקור במשך 10 ימים ברצף כל יום @@ -4515,8 +4521,8 @@ he: title: "כנסו למצב בטוח" description: "מצב בטוח מאפשר לכם לבחון את האתר שלכם מבלי לטעון תוספים או התאמות אתר." no_customizations: "ניטרול של תמה נוכחית" - only_official: "נטרלו תוספים לא רשמיים" - no_plugins: "נטרלו את כל התוספים" + only_official: "השבתת תוספים בלתי רשמיים" + no_plugins: "השבתת כל התוספים" enter: "כנסו למצב בטוח" must_select: "עליך לבחור באפשרות אחת לפחות כדי להיכנס למצב בטוח." wizard: @@ -4546,7 +4552,7 @@ he: fields: welcome: label: "נושא ברוכים-הבאים" - description: "

כיצד הייתם מתארים את הקהילה שלכם לאדם זר במעלית אם יש לכם רק דקה 1?

  • עבור מי נועדו דיונים אלו?
  • מה אוכל למצוא פה?
  • למה כדאי לי לבקר?

נושא הברוכים-הבאים שלכם הוא הדבר הראשון שמבקרים חדשים יקראו. חישבו עליו כפסקה אחת של ׳נאום המעלית׳ או ׳הצהרת הכוונות׳ שלכם.

" + description: "

איך לדעתך ראוי לתאר את הקהילה שלך לאנשים זרים שפגשת במעלית למשך בערך דקה?

  • למי מיועדים הדיונים?
  • מה אפשר למצוא כאן?
  • מדוע כדאי לי לבקר?

נושא קבלת הפנים שלך הוא הדבר הראשון שייקראו המתעניינים החדשים. כדאי להתייחס אליו בתור ‚נאום מעלית’ או ‚הצהרת כוונות’ של פסקה אחת.

" one_paragraph: "אנא הגבילו את הודעת הברוכים-הבאים שלכם לפסקה אחת." extra_description: "במקרה של ספק ניתן לדלג על השלב הזה ולערוך את קבלת הפנים ואת הנושא בהמשך." privacy: diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml index 7e706e5591..3569072bc5 100644 --- a/config/locales/server.hu.yml +++ b/config/locales/server.hu.yml @@ -221,7 +221,6 @@ hu: too_many_links: one: "Sajnáljuk, de az új felhasználók csak egy hivatkozást tehetnek egy bejegyzésbe." other: "Sajnáljuk, de az új felhasználók csak %{count} hivatkozást tehetnek egy bejegyzésbe." - contains_blocked_word: "A bejegyzésed egy olyan szót tartalmaz, amely nem megengedett: %{word}" user_is_suspended: "A felfüggesztett felhasználók nem hozhatnak létre új bejegyzést." topic_not_found: "Valami hiba történt. Esetleg lezárták vagy törölték ezt a témát, míg megnézte?" create_pm_on_existing_topic: "Sajnáljuk, nem hozhatsz létre PM-et egy meglévő témakörben." @@ -885,12 +884,15 @@ hu: site_settings: disabled: "kikapcsolva" max_emojis_in_title: "A témacímekben lévő emodzsik legnagyobb száma" + search_tokenize_chinese: "A keresés kényszerítése, hogy tokenizálja a kínait, még a nem kínai oldalakon is" + search_tokenize_japanese: "A keresés kényszerítése, hogy tokenizálja a japánt, még a nem japán oldalakon is" notify_users_after_responses_deleted_on_flagged_post: "Ha egy bejegyzést jelentenek, majd eltávolítanak, akkor minden olyan felhasználó értesítést kap, aki válaszolt a bejegyzésre, és ezért a válaszait eltávolították." post_excerpt_maxlength: "A bejegyzés kivonatának / összefoglalójának maximális hossza." topic_excerpt_maxlength: "A témakör kivonatának / összefoglalójának maximális hossza a téma első bejegyzéséből generálva." + blocked_onebox_domains: "Azok a domainek, amelyek soha nem kerülnek dobozba, például wikipedia.org\n(A * ? helyettesítő karakterek nem támogatottak)" enforce_second_factor: "Kényszeríti a felhasználókat, hogy engedélyezzék a kétfaktoros hitelesítést. Válassza az 'all' lehetőséget, hogy érvényesítse az összes felhasználó számára. Válassza a „személyzet” lehetőséget, hogy csak a személyzet felhasználói számára érvényesítse." use_admin_ip_allowlist: "Az adminokk csak akkor tudnak bejelentkezni, ha a Screened IPs listában megadott IP-címmel vannak megadva (Admin > Naplók > Screened Ips)." - slow_down_crawler_user_agents: "A forgalmi korlátozás alá elő webes robotok felhasználói ügynökei, ahogy a „robotok sebességének lelassítása” beállításban meg van adva. Minden egyes értéknek legalább 3 karakteresnek kell lennie." + slow_down_crawler_user_agents: 'A forgalmi korlátozás alá elő webes robotok felhasználói ügynökei, ahogy a „robotok sebességének lelassítása” beállításban meg van adva. Minden egyes értéknek legalább 3 karakteresnek kell lennie.' enable_badges: "A jelvény rendszer engedélyezése" normalize_emails: "Ellenőrizze, hogy a normalizált e-mail egyedi-e. A normalizált e-mail eltávolítja az összes pontot a felhasználónévből, és mindent, ami a + és a @ szimbólumok között található." hide_email_address_taken: "Ne tájékoztassa a felhasználókat arról, hogy létezik fiók egy adott e-mail-címmel a regisztráció során vagy az elfelejtett jelszó folyamata során. Teljes e-mail-cím megkövetelése az „elfelejtett jelszó” kérésekhez." @@ -926,6 +928,7 @@ hu: default_other_skip_new_user_tips: "A tippek és jelvények kihagyása az új felhasználók regisztrációjakor" default_other_like_notification_frequency: "A felhasználók értesítése a kedvelésekről alapértelmezett esetben." company_name: "Cégnév" + use_email_for_username_and_name_suggestions: "Használja az e-mail-címek első részét a felhasználónév- és névjavaslatokhoz. Vegye figyelembe, hogy ez megkönnyíti a nyilvánosság számára a teljes felhasználói e-mail-címek kitalálását (mivel az emberek nagy része olyan gyakori szolgáltatásokat használ, mint a „gmail.com”)." errors: invalid_email: "Érvénytelen e-mail cím." invalid_username: "Nincs ilyen nevű felhasználó." @@ -939,9 +942,12 @@ hu: invalid_string_min_max: "A karakterek számának %{min} és %{max} között kell lennie." invalid_string_min: "Nem lehet rövidebb, mint %{min} karakter." invalid_string_max: "Nem lehet hosszabb, mint %{max} karakter." + invalid_domain_hostname: "Nem tartalmazhatja a * vagy a ? karaktereket." min_username_length_range: "Nem lehet a legnagyobb értéknél nagyobb legkisebb értéket beállítani." max_username_length_range: "Nem lehet a legkisebb értéknél kisebb legnagyobb értéket beállítani." google_oauth2_hd_groups: "A beállítás engedélyezése előtt be kell ezt állítania: „google oauth2 hd”." + search_tokenize_chinese_enabled: "A beállítás engedélyezése előtt le kell tiltania a „search_tokenize_chinese” beállítást." + search_tokenize_japanese_enabled: "A beállítás engedélyezése előtt le kell tiltania a „search_tokenize_japanese” beállítást." search: within_post: "#%{post_number}, szerző: %{username}" types: diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml index 3910ca4ee5..332fc5ae11 100644 --- a/config/locales/server.hy.yml +++ b/config/locales/server.hy.yml @@ -241,7 +241,6 @@ hy: too_many_links: one: "Ներողություն, նոր օգտատերերը գրառման մեջ կարող են դնել միայն մեկ հղում:" other: "Ներողություն, նոր օգտատերերը գրառման մեջ կարող են դնել միայն %{count} հղում:" - contains_blocked_word: "Ձեր գրառումը պարունակում է անթույլատրելի բառ՝%{word}" spamming_host: "Ներողություն, Դուք չեք կարող հրապարակել այդ հոսթի հղում:" user_is_suspended: "Սառեցված օգտատերերին թույլատրված չէ գրառում կատարել:" topic_not_found: "Ինչ-որ բան այնպես չէ ընթացել: Միգուցե այս թեման փակվել կամ ջնջվե՞լ է, մինչ Դուք դիտում էիք այն:" @@ -1070,7 +1069,6 @@ hy: min_personal_message_title_length: "Հաղորդագրության վերնագրի նվազագույն թույլատրելի երկարությունը սիվոլներով" max_emojis_in_title: "Թեմայի վերնագրում առավելագույն թույլատրելի էմոջիները" min_search_term_length: "Որոնման վավեր արտահայտության նվազագույն երկարությունը սիմվոլներով" - search_tokenize_chinese_japanese_korean: "Պարտադրված որոնում՝ կոդանշանավորելու չինարենը/ճապոներենը/կորեերենը նույնիսկ ոչ ՉՃԿ կայքերում" search_prefer_recent_posts: "Եթե Ձեր մեծ ֆորումում որոնելը դանդաղ է, այս տարբերակը սկզբում փորձում է փնտրել ավելի թարմ գրառումների աստիճանացանկում:" search_recent_posts_size: "Քանի վերջերս կատարված գրառում պահել ինդեքսում " log_search_queries: "Պահպահել օգտատերերի կողմից իրականացվող որոնման հարցումները" @@ -1107,7 +1105,6 @@ hy: show_pinned_excerpt_mobile: "Ցուցադրել հատվածը ամրագրված թեմաների վրա հեռախոսային տարբերակում:" show_pinned_excerpt_desktop: "Ցուցադրել հատվածը ամրագրված թեմաների վրա համակարգչային տարբերակում:" post_onebox_maxlength: "Սմարթ ներդիրի վերածված Discourse գրառման առավելագույն երկարությունը սիմվոլներով:" - blocked_onebox_domains: "Դոմենների ցանկը, որոնք երբեք չեն վերածվի սմարթ-ներդիրի:" allowed_inline_onebox_domains: "Դոմենների ցանկ, որոնք կվերածվեն սմարթ-ներդիրի փոքրացված տեսքով՝ առանց վերնագրի կապակցված լինելու դեպքում" max_oneboxes_per_post: "Գրառման մեջ սմարթ-ներդիրների առավելագույն քանակը:" logo: "Ձեր կայքի վերևի ձախ կողմում գտնվող լոգոյի նկարը: Օգտագործեք լայն ուղղանկյուն նկար 120 երկարությամբ և 3:1 -ից բարձր հարաբերակցությամբ: Եթե թողնեք դատարկ, կցուցադրվի կայքի վերնագրի տեքստը:" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index e44a38ccb4..08debae36a 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -337,8 +337,8 @@ it: too_many_links: one: "Spiacenti, i nuovi utenti possono inserire al massimo un link in un messaggio." other: "Spiacenti, i nuovi utenti possono inserire al massimo %{count} link in un messaggio." - contains_blocked_word: "Il tuo messaggio contiene una parola non consentita: %{word}" - contains_blocked_words: "Il tuo messaggio contiene parole non consentite: %{words}" + contains_blocked_word: "Purtroppo non è consentito pubblicare la parola '%{word}'." + contains_blocked_words: "Purtroppo non puoi pubblicare questo messaggio. Parole non consentite: %{words}." spamming_host: "Spiacenti, non puoi inserire un collegamento verso quell'host." 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." @@ -1396,7 +1396,8 @@ it: min_personal_message_title_length: "Numero minimo di caratteri per un messaggio" max_emojis_in_title: "Numero massimo di emoji permessi nel titolo di un Argomento" min_search_term_length: "Numero minimo di caratteri per le parole cercate" - search_tokenize_chinese_japanese_korean: "Attiva la tokenizzazione dei caratteri Cinesi/Giapponesi/Coreani nella ricerca anche sui siti non CJK" + search_tokenize_chinese: "Forza la ricerca per tokenizzare il cinese anche su siti non cinesi" + search_tokenize_japanese: "Forza la ricerca per tokenizzare il giapponese anche su siti non giapponesi" 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" @@ -1440,7 +1441,6 @@ it: show_pinned_excerpt_mobile: "Mostra estratti sugli argomenti appuntati nella visualizzazione in mobilità." 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." - blocked_onebox_domains: "Un elenco di domini che non saranno mai trasformati in onebox." allowed_inline_onebox_domains: "Un elenco di domini che saranno trasformati in onebox in forma di miniatura se collegati senza un titolo" enable_inline_onebox_on_all_domains: "Ignora l'impostazione del sito inline_onebox_domain_allowlist e consenti onebox in linea per tutti i domini." force_custom_user_agent_hosts: "Host per i quali utilizzare l'user agent onebox personalizzato su tutte le richieste. (Particolarmente utile per gli host che limitano l'accesso con user agent)." @@ -1526,7 +1526,7 @@ it: allowed_iframes: "Un elenco di prefissi di dominio src iframe che discourse può consentire in modo sicuro nei messaggi" allowed_crawler_user_agents: "User Agent dei crawler Web che dovrebbero poter accedere al sito. ATTENZIONE! IMPOSTANDO TALE OPZIONE SARANNO BLOCCATI TUTTI I CRAWLER NON ELENCATI QUI!" blocked_crawler_user_agents: "Parola univoca, senza distinzione tra maiuscole e minuscole, nella stringa user agent che identifica i web crawler cui non si vuole consentire l'accesso al sito. L'impostazione non si applica se è stata definita una lista di elementi consentiti." - slow_down_crawler_user_agents: "Gli user agent dei web crawler a cui impostare limiti di velocità come configurato nell'impostazione \"rallenta la velocità dei crawler\". Ogni valore deve essere lungo almeno 3 caratteri." + slow_down_crawler_user_agents: 'Gli user agent dei web crawler a cui impostare limiti di velocità come configurato nell''impostazione "rallenta la velocità dei crawler". Ogni valore deve essere lungo almeno 3 caratteri.' slow_down_crawler_rate: "Se viene specificato slow_down_crawler_user_agents, tale frequenza verrà applicata a tutti i crawler (numero di secondi di ritardo tra le richieste)" content_security_policy: "Abilita Content-Security-Policy" content_security_policy_report_only: "Abilita Content-Security-Policy-Report-Only" @@ -2129,6 +2129,8 @@ it: leading_trailing_slash: "L'espressione regolare non può iniziare o terminare con una barra." unicode_usernames_avatars: "Gli avatar di sistema interni non supportano i nomi utente Unicode." list_value_count: "La lista deve contenere esattamente %{count} valori." + search_tokenize_chinese_enabled: "Devi disattivare \"search_tokenize_chinese\" prima di attivare questa impostazione." + search_tokenize_japanese_enabled: "Devi disattivare \"search_tokenize_japanese\" prima di attivare questa impostazione." placeholder: discourse_connect_provider_secrets: key: "www.example.com" diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index f22fa023d4..07b7898dd6 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -312,8 +312,6 @@ ja: links_require_trust: "投稿にリンクを含めることはできません。" too_many_links: other: "新規ユーザーは投稿につき %{count} 件のリンクしか追加できません。" - contains_blocked_word: "投稿に許可されていない言葉が含まれています: %{word}" - contains_blocked_words: "投稿に許可されていない語が複数含まれています: %{words}" spamming_host: "このホストへのリンクを投稿できません。" user_is_suspended: "凍結中のユーザーは投稿できません。" topic_not_found: "問題が発生しました。トピックがクローズしたか、閲覧中に削除された可能性があります。" @@ -1324,7 +1322,6 @@ ja: min_personal_message_title_length: "メッセージのタイトルの最小文字数" max_emojis_in_title: "トピックタイトルに許可される最大絵文字数" min_search_term_length: "有効な検索語の最小文字数" - search_tokenize_chinese_japanese_korean: "CJK 以外のサイトでも強制検索で中国語/日本語/韓国語をトークン化する" search_prefer_recent_posts: "大規模なフォーラムの検索が遅い場合、このオプションは先により直近の投稿のインデックスを試す" search_recent_posts_size: "インデックスに保持する直近の投稿の数" log_search_queries: "ユーザーが実行した検索クエリをログに記録する" @@ -1367,7 +1364,6 @@ ja: show_pinned_excerpt_mobile: "モバイルビューで固定トピックの抜粋を表示する。" show_pinned_excerpt_desktop: "デスクトップビューで固定トピックの抜粋を表示する。" post_onebox_maxlength: "Discourse OneBox 投稿の最大文字数。" - blocked_onebox_domains: "ワンボックス化されないドメインのリスト。" allowed_inline_onebox_domains: "タイトルなしでリンクされた場合、ミニチュア形式でワンボックス化されるドメインのリスト" enable_inline_onebox_on_all_domains: "inline_onebox_domain_allowlist サイト設定を無視し、すべてのドメインでインラインのワンボックスを許可する。" force_custom_user_agent_hosts: "すべてのリクエストでカスタムワンボックスユーザーエージェントを使用するホスト (特にユーザーエージェントごとにアクセスを制限するホストで役立ちます)。" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index a422024c32..6450f5f73c 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -313,8 +313,6 @@ ko: links_require_trust: "죄송합니다. 게시물에 링크를 포함 할 수 없습니다." too_many_links: other: "죄송합니다. 신규회원은 글 하나에 링크를 %{count}개까지 넣을 수 있습니다." - contains_blocked_word: "게시물에 허용되지 않는 단어가 포함되어 있습니다 : %{word}" - contains_blocked_words: "게시물에 허용되지 않는 단어가 여러 개 있습니다 : %{words}" spamming_host: "죄송합니다. 회원님은 링크를 첨부할 수 없습니다." user_is_suspended: "가입이 보류된 회원은 글을 쓸 수 없습니다." topic_not_found: "뭔가 잘못됐네요. 아마 읽려는 도중에 토픽이 닫혔거나 지워진 건 아닐까요?" @@ -1340,7 +1338,6 @@ ko: min_personal_message_title_length: "메세지 제목의 최소 길이" max_emojis_in_title: "제목에 허용되는 최대 이모티콘" min_search_term_length: "검색을 하기 위한 최소 글자 수" - search_tokenize_chinese_japanese_korean: "CJK(한중일) 설정이 되어 있지 않은 사이트라도, 검색시 한중일 언어를 토크나이징하도록 강제합니다." search_prefer_recent_posts: "이 옵션을 켜면, 포럼이 너무 커서 검색이 느리게 작동할 때 최근의 포스트부터 먼저 검색합니다." search_recent_posts_size: "대문에 실을 최신 포스트 수" log_search_queries: "사용자에 의해 발생된 로그 탐색 쿼리" @@ -1384,7 +1381,6 @@ ko: show_pinned_excerpt_mobile: "모바일 뷰에서 고정 토픽의 발췌 내용을 보여주기" show_pinned_excerpt_desktop: "데스크톱 뷰에서 고정 토픽의 발췌 내용을 보여주기" post_onebox_maxlength: "onebox가 적용된 Discourse 글에 허용되는 최대 글자수" - blocked_onebox_domains: "원박스화에서 제외 할 도메인 목록입니다." allowed_inline_onebox_domains: "제목없이 링크 된 경우 미니어처 형식으로 oneboxed 될 도메인 목록" enable_inline_onebox_on_all_domains: "inline_onebox_domain_whitelist 사이트 설정을 무시하고 모든 도메인에서 인라인 onebox를 허용하십시오." force_custom_user_agent_hosts: "모든 요청에서 사용자 정의 onebox 사용자 에이전트를 사용할 호스트. (특히 사용자 에이전트의 액세스를 제한하는 호스트에 유용합니다)." diff --git a/config/locales/server.lt.yml b/config/locales/server.lt.yml index 104d24ea1d..c8362c2db5 100644 --- a/config/locales/server.lt.yml +++ b/config/locales/server.lt.yml @@ -272,8 +272,6 @@ lt: no_attachments_allowed: "Atsiprašome, bet nauji vartotojai negali įkelti priedų pranešimuose." no_links_allowed: "Atsiprašome, bet nauji vartotojai negali įkelti nuorodų į pranešimus." links_require_trust: "Atsiprašome, negalite įtraukti nuorodų į savo įrašus." - contains_blocked_word: "Jūsų įraše yra neleistinas žodis: %{word}" - contains_blocked_words: "Jūsų įraše yra keli neleidžiami žodžiai: %{words}" user_is_suspended: "Sustabdyti vartotojai negali rašyti pranešimų." topic_not_found: "Kažkas nepavyko. Galbūt ši tema buvo uždaryta arba ištrinta, kol į ją žiūrėjote?" not_accepting_pms: "Atsiprašome, %{username} šiuo metu nepriima pranešimų." diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 9456fa1187..c7af4cfd7a 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -221,7 +221,6 @@ nb_NO: too_many_links: one: "Beklager, nye brukere kan bare legge til én lenke i et innlegg." other: "Beklager, nye brukere kan bare legge til %{count} lenker i et innlegg." - contains_blocked_word: "Innlegget ditt inneholder et ord som ikke er tillatt: %{word}" spamming_host: "Beklager, du kan ikke lenke til det domenet." user_is_suspended: "Utestengte brukere kan ikke skrive innlegg." topic_not_found: "Noe gikk galt. Kanskje emnet ble lukket eller slettet mens du leste det?" @@ -947,7 +946,6 @@ nb_NO: min_personal_message_title_length: "Minste lengde i antall tegn for en meldingstittel" max_emojis_in_title: "Maksimalt antall emojier i en emnetittel" min_search_term_length: "Minimum lengde på søkeord i tegn" - search_tokenize_chinese_japanese_korean: "Tving søk til å tokenisere Kinesisk/Japansk/Koreansk selv på nettsteder uten KJK" search_prefer_recent_posts: "Hvis søking er tregt på et stort forum, vil dette valget forsøke å søke i en index av de nyeste innleggene først" search_recent_posts_size: "Hvor mange innlegg som beholdes i indeksen" log_search_queries: "Logg søkeord som benyttes av brukerne" @@ -979,7 +977,6 @@ nb_NO: show_pinned_excerpt_mobile: "Vis utdrag fra festede emner i mobil visning." show_pinned_excerpt_desktop: "Vis utdrag fra festede emner i desktop visning." post_onebox_maxlength: "Maksimal lengde på et onebox Discourse innlegg i antall tegn." - blocked_onebox_domains: "En liste over domener som aldri vil bli forsøkt oneboxet." allowed_inline_onebox_domains: "En liste over domener hvor onebox brukes i miniatyrstørrelse hvis lenken ikke har en tittel" max_oneboxes_per_post: "Maksimalt antall oneboxer i hvert innlegg." notification_email: "Adressen som skal brukes som avsender: på alle viktige system e-poster. Domenet som spesifiseres her må ha SPF, DKIM og reverse PTR record satt korrekt for at e-post skal nå frem." diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 5611c5e49f..c0ac725ad4 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -305,8 +305,6 @@ nl: too_many_links: one: "Sorry, nieuwe gebruikers kunnen maar één koppeling in een bericht plaatsen." other: "Sorry, nieuwe gebruikers kunnen maar %{count} koppelingen in een bericht plaatsen." - contains_blocked_word: "Uw bericht bevat een woord dat niet is toegestaan: %{word}" - contains_blocked_words: "Uw bericht bevat meerdere woorden die niet zijn toegestaan: %{words}" spamming_host: "Sorry, u kunt geen koppeling naar die host plaatsen." user_is_suspended: "Geschorste gebruikers mogen geen berichten plaatsen." topic_not_found: "Er is iets fout gegaan. Misschien is het topic gesloten of verwijderd terwijl u het bekeek?" @@ -1287,7 +1285,6 @@ nl: min_personal_message_title_length: "Minimaal toegestane titellengte voor een bericht in tekens" max_emojis_in_title: "Maximale aantal toegestane emoji in topictitel" min_search_term_length: "Minimaal geldige lengte van een zoekterm in tekens" - search_tokenize_chinese_japanese_korean: "Zoeken voor het tokeniseren van Chinees/Japans/Koreaans ook op niet-CJK-websites afdwingen" search_prefer_recent_posts: "Als doorzoeken van uw grote forum traag werkt, probeert deze optie eerst een index van recentere berichten" search_recent_posts_size: "Het aantal in de index te behouden recente berichten" log_search_queries: "Zoekopdrachten van gebruikers opslaan" @@ -1327,7 +1324,6 @@ nl: show_pinned_excerpt_mobile: "Fragment tonen bij vastgemaakte topics in mobiele weergave." show_pinned_excerpt_desktop: "Fragment tonen bij vastgemaakte topics in desktopweergave." post_onebox_maxlength: "Maximale lengte van een 'oneboxed Discourse'-bericht in tekens." - blocked_onebox_domains: "Een lijst van domeinen die nooit in een onebox worden omgezet." allowed_inline_onebox_domains: "Een lijst van domeinen die in een onebox als miniatuur worden omgezet wanneer gekoppeld zonder titel" enable_inline_onebox_on_all_domains: "Website-instelling inline_onebox_domain_allowlist negeren en inline onebox op alle domeinen toestaan." force_custom_user_agent_hosts: "Hosts waarvoor bij alle aanvragen de aangepaste onebox-useragent wordt gebruikt. (Met name handig voor hosts die toegang beperken via de useragent)." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index c0682d224b..16475c7945 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -220,6 +220,7 @@ pl_PL: local_login_cannot_be_disabled_if_second_factor_enforced: "Nie można wyłączyć lokalnego logowania, jeśli wymuszone jest 2FA. Wyłącz wymuszone 2FA przed wyłączeniem lokalnych logowań." cannot_enable_s3_uploads_when_s3_enabled_globally: "Nie można włączyć przesyłania S3, ponieważ przesyłanie S3 jest już włączone globalnie, a włączenie tego poziomu witryny może powodować krytyczne problemy z przesyłaniem" cors_origins_should_not_have_trailing_slash: "Nie należy dodawać końcowego ukośnika (/) do źródeł CORS." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "User-Agent musi mieć co najmniej 3 znaki, aby uniknąć niewłaściwego ograniczania prawdziwych użytkowników." slow_down_crawler_user_agent_cannot_be_popular_browsers: "Do ustawienia nie można dodać żadnej z następujących wartości: %{values}." conflicting_google_user_id: 'Identyfikator konta Google dla tego konta został zmieniony; interwencja personelu jest wymagana ze względów bezpieczeństwa. Skontaktuj się z personelem i wskaż go
https://meta.discourse.org/t/76575' onebox: @@ -243,6 +244,8 @@ pl_PL:

Jeśli pamiętasz hasło, możesz się zalogować.

W przeciwnym razie zresetuj hasło.

+ not_found_template_link: | +

To zaproszenie do %{site_name} nie może już zostać zrealizowane. Poproś osobę, która Cię zaprosiła, aby wysłała Ci nowe zaproszenie.

user_exists: "Nie ma potrzeby zapraszania %{email}, obecnie posiada konto!" invite_exists: "Zaprosiłeś już %{email}." invalid_email: "%{email} nie jest prawidłowym adresem e-mail." @@ -257,6 +260,7 @@ pl_PL: discourse_connect_enabled: "Zaproszenia są wyłączone, ponieważ DiscourseConnect jest włączony." invalid_access: "Nie masz uprawnień do przeglądania żądanego zasobu." requires_groups: "Zaproszenie zapisane. Aby dać dostęp do określonego tematu, dodaj jedną z następujących grup: %{groups}." + domain_not_allowed: "Twój adres e-mail nie może zostać użyty do zrealizowania tego zaproszenia." bulk_invite: file_should_be_csv: "Wgrywany plik powinien być formatu csv." max_rows: "Wysłano %{max_bulk_invites} pierwszych zaproszeń. Sprobuj podzielić plik na mniejsze części." @@ -372,8 +376,8 @@ pl_PL: few: "Przepraszamy, nowi użytkownicy mogą dodawać tylko %{count} linki do wpisu." many: "Przepraszamy, nowi użytkownicy mogą dodawać tylko %{count} linków do wpisu." other: "Przepraszamy, nowi użytkownicy mogą dodawać tylko %{count} linków do wpisu." - contains_blocked_word: "Twój wpis zawiera niedozwolone słowo: %{word}" - contains_blocked_words: "Twój wpis zawiera niedozwolone słowa: %{words}" + contains_blocked_word: "Niestety, nie możesz opublikować słowa '%{word}'; jest ono niedozwolone." + contains_blocked_words: "Przepraszamy, nie możesz tego opublikować. Niedozwolone słowa: %{words}." spamming_host: "Przepraszamy, nie możesz umieścić linka do tej strony." user_is_suspended: "Zawieszeni użytkownicy nie mogą wysyłać wiadomości." topic_not_found: "Coś poszło nie tak. Być może temat został zamknięty lub usunięty w międzyczasie?" @@ -711,6 +715,7 @@ pl_PL: post: image_placeholder: broken: "Ten obraz jest uszkodzony" + hidden_bidi_character: "Znaki dwukierunkowe mogą zmienić kolejność renderowania tekstu. Może to służyć do ukrywania złośliwego kodu." has_likes: one: "%{count} polubienie" few: "%{count} polubienia" @@ -1432,19 +1437,26 @@ pl_PL: mutes_count: Licznik wyciszonych description: "Użytkownicy, którzy zostali wyciszeni i / lub zignorowani przez wielu innych użytkowników." top_users_by_likes_received: + title: "Najlepsi użytkownicy według otrzymanych polubień" labels: user: Użytkownik qtt_like: Polubienia otrzymane + description: "Top 10 użytkowników, którzy otrzymali więcej polubień." top_users_by_likes_received_from_inferior_trust_level: + title: "Najlepsi użytkownicy według polubień otrzymanych od użytkownika o niższym poziomie zaufania" labels: user: Użytkownik trust_level: Poziom zaufania qtt_like: Polubienia otrzymane + description: "Top 10 użytkowników na wyższym poziomie zaufania jest lubianych przez osoby o niższym poziomie zaufania." top_users_by_likes_received_from_a_variety_of_people: + title: "Najlepsi użytkownicy według polubień otrzymanych od różnych osób" labels: user: Użytkownik qtt_like: Polubienia otrzymane + description: "Top 10 użytkowników, którzy mają polubienia od szerokiego grona osób." dashboard: + group_email_credentials_warning: 'Wystąpił problem z poświadczeniami e-mail dla grupy %{group_full_name}. Żadne e-maile nie będą wysyłane ze skrzynki odbiorczej grupy, dopóki ten problem nie zostanie rozwiązany. %{error}' rails_env_warning: "Twój serwer działa w trybie %{env}" host_names_warning: "Twój plik config/database.yml używa domyślnej nazwy serwera localhost. Zmień go by używał nazwy serwera Twojej strony." sidekiq_warning: 'Sidekiq nie działa. Wiele zadań, takich jak wysyłanie emaili, jest wykonywane asynchronicznie przez sidekiqa. Zagwarantuj, że przynajmniej jeden proces sidekiqa działa. Dowiedz się więcej o Sidekiqu.' @@ -1474,6 +1486,7 @@ pl_PL: unreachable_themes: "Nie byliśmy w stanie sprawdzić aktualizacji następujących tematów:" watched_word_regexp_error: "Wyrażenie regularne dla '%{action}' jest nieprawidłowe. Sprawdź ustawienia obserwowanych słów lub wyłącz ustawienie witryny „obserwowane słowa regularne”." site_settings: + allow_bulk_invite: "Zezwalaj na zaproszenia zbiorcze, przesyłając plik CSV" disabled: "wyłączone" display_local_time_in_user_card: "Wyświetl czas lokalny w oparciu o strefę czasową użytkownika po otwarciu karty użytkownika." censored_words: "Wskazane słowa będą automatycznie zamieniane na ■■■■" @@ -1481,20 +1494,19 @@ pl_PL: default_locale: "Domyślny język tego wystąpienia dyskursu. Tekst kategorii i tematów generowanych przez system można zastąpić w opcji Dostosuj / Tekst." allow_user_locale: "Zezwól użytkownikom na zmianę języka interfejsu we własnych ustawieniach" set_locale_from_accept_language_header: "ustaw język interfejsu dla anonimowych użytkowników na podstawie nagłówków języka z ich przeglądarki" - support_mixed_text_direction: "Obsługa mieszanych wskazówek tekstowych od lewej do prawej i od prawej do lewej." + support_mixed_text_direction: "Wspieraj mieszane kierunki tekstu od lewej do prawej i od prawej do lewej strony." min_post_length: "Minimalna długość wpisu w znakach" min_first_post_length: "Minimalna długość treści (liczba znaków) pierwszego wpisu w temacie " min_personal_message_post_length: "Minimalna długość treści wiadomości " max_post_length: "Maksymalna długość wpisu, w znakach" topic_featured_link_enabled: "Włącz dodawanie linków w tematach." show_topic_featured_link_in_digest: "Pokazuj polecany link tematu w podsumowaniu e-mailowym." - min_topic_views_for_delete_confirm: "Minimalna liczba wyświetleń, które musi mieć wątek aby pojawił się po usunięciu okna potwierdzającego" + min_topic_views_for_delete_confirm: "Minimalna liczba wyświetleń tematu, aby podczas jego usuwania pojawiło się okienko z potwierdzeniem." min_topic_title_length: "Minimalna długość tytułu tematu, w znakach" max_topic_title_length: "Maksymalna długość tytułu tematu, w znakach" min_personal_message_title_length: "Minimalna liczba znaków w temacie wiadomości " max_emojis_in_title: "Maksymalna liczba emoji w tytule tematu" min_search_term_length: "Minimalna długość wyszukiwanego tekstu, w znakach" - search_tokenize_chinese_japanese_korean: "Wymuś wyszukiwanie żeby tokenizowało Chiński/Japoński/Koreański nawet na stronach nie-CJK" search_prefer_recent_posts: "Jeśli wyszukiwanie na twoim dużym forum jest wolne, ta opcja pozwala zindeskować najpierw ostanie wpisy." search_recent_posts_size: "Jak dużo ostanich wpisów trzymać w indeksie" log_search_queries: "Dzienniki wyszukiwanych przez użytkowników treści" @@ -1523,13 +1535,14 @@ pl_PL: staff_edit_locks_post: "Wpisy zostaną zablokowane przed edycją, jeśli będą edytowane przez członków personelu" post_edit_time_limit: "Autor tl0 lub tl1 może edytować swój wpis przez (n) minut po opublikowaniu. Ustaw na zawsze na 0." tl2_post_edit_time_limit: "Autor tl2 + może edytować swój wpis przez (n) minut po opublikowaniu. Ustaw na zawsze na 0." - edit_history_visible_to_public: "Pozwól wszystkim przeglądać poprzednie wersje edytowanych wpisów. Gdy wyłączone, jedynie obsługa/staff może." + edit_history_visible_to_public: "Pozwól wszystkim zobaczyć poprzednie wersje edytowanego posta. Gdy wyłączone, tylko członkowie personelu mogą je przeglądać." delete_removed_posts_after: "Wpisy usunięte przez autora będą automatycznie zlikwidowane po (n) godzinach. Jeżeli liczba godzin będzie wynosić 0, to zostaną usunięte natychmiast. " + notify_users_after_responses_deleted_on_flagged_post: "Gdy post zostanie oznaczony, a następnie usunięty, wszyscy użytkownicy, którzy na niego odpowiedzieli, a ich odpowiedzi zostały usunięte, zostaną powiadomieni." max_image_width: "Maksymalna szerokość miniaturki grafiki we wpisie. " max_image_height: "Maksymalna wysokość miniaturki grafiki we wpisie. " responsive_post_image_sizes: "Zmień rozmiar podglądu lightbox, aby umożliwić wyświetlanie ekranów o wysokiej rozdzielczości DPI o następujących proporcjach pikseli. Usuń wszystkie wartości, aby wyłączyć responsywne obrazy." fixed_category_positions: "Zaznacz, aby ręcznie ustawiać kolejność kategorii. Odznacz, aby kategorie były sortowane na podstawie aktywności. " - fixed_category_positions_on_create: "Jeżeli jest sprawdzone to porządkowanie kategorii będzie zależało od stworzonego tematu (requires fixed_category_positions). " + fixed_category_positions_on_create: "Jeśli zaznaczone, kolejność kategorii będzie utrzymywana w oknie dialogowym tworzenia tematu (wymaga fixed_category_positions)." add_rel_nofollow_to_user_content: 'Dodaj rel nofollow do wszystkich treści użytkownika, poza wewnętrznymi linkami (wliczając domeny nadrzędne). Jeśli to zmienisz, musisz rebake wszystkie wpisy poprzez: "rake post:rebake"' exclude_rel_nofollow_domains: "Lista domen, w których nofollow nie powinien być dodawany do linków. example.com automatycznie zezwoli również na sub.example.com. Jako minimum należy dodać domenę tej witryny, aby roboty indeksujące mogły znaleźć całą treść. Jeśli inne części witryny znajdują się w innych domenach, dodaj je również." post_excerpt_maxlength: "Maksymalna długość podsumowania / streszczenia wpisu." @@ -1537,7 +1550,6 @@ pl_PL: show_pinned_excerpt_mobile: "Pokaż fragment przypiętych tematów w widoku mobilnym" show_pinned_excerpt_desktop: "Pokaż fragment przypiętych tematów w widoku \"wersja komputerowa\"" post_onebox_maxlength: "Maksymalna długość (ilość znaków) treści wpisu osadzonego via Onebox" - blocked_onebox_domains: "Lista domen, które nigdy nie będą osadzane w poście." allowed_inline_onebox_domains: "Lista domen, które zostaną zamienione na osadzony onebox w zmniejszonej formie, jeśli będą umieszczone w poście bez tytułu" enable_inline_onebox_on_all_domains: "Ignoruj ustawienie witryny `inline_onebox_domain_whitelist` i zezwalaj na osadzanie ramek we wszystkich domenach." force_custom_user_agent_hosts: "Hosty, dla których ma być używany niestandardowy klient użytkownika Onebox przy wszystkich żądaniach. (Szczególnie przydatne dla hostów, które ograniczają dostęp do agenta użytkownika)." @@ -1570,6 +1582,7 @@ pl_PL: summary_percent_filter: "Gdy użytkownik kliknie na 'Podsumowaniu tematu', pokaż % najlepszych wpisów" summary_max_results: "Maksymalna liczba wpisów zwróconych przez „Podsumuj ten temat”" summary_timeline_button: "Pokaż przycisk „Podsumuj” na osi czasu" + enable_personal_messages: "Zezwalaj użytkownikom na poziomie zaufania 1 (konfigurowalnym za pomocą min. zaufania do wysyłania wiadomości) na tworzenie i odpowiadanie na wiadomości. Należy pamiętać, że personel zawsze może wysyłać wiadomości bez względu na wszystko." enable_system_message_replies: "Pozwala użytkownikom odpowiadać na wiadomości systemowe, nawet jeśli wiadomości osobiste są wyłączone" enable_long_polling: "Message bus used for notification can use long polling" enable_chunked_encoding: "Włącz odpowiedzi fragmentaryczne serwera. Ta funkcja działa w większości konfiguracji, jednak niektóre serwery proxy mogą buforować odpowiedzi, powodując opóźnienia." @@ -1624,6 +1637,7 @@ pl_PL: allowed_iframes: "Lista prefiksów domeny src iframe, które dyskurs może bezpiecznie dopuścić w wpisach" allowed_crawler_user_agents: "Agenci użytkowników dla robotów indeksujących, które powinny mieć dostęp do strony. OSTRZEŻENIE! USTAWIENIE TEGO SPOWODUJE BLOKADE ROBOTÓW, KTÓRE NIE SĄ NA LIŚCIE!" blocked_crawler_user_agents: "Unikalne słowo bez rozróżniania wielkości liter w ciągu znaków, które identyfikują crawlery internetowe, które nie powinny mieć dostępu do witryny. Nie ma zastosowania, jeśli dozwolona lista jest zdefiniowana." + slow_down_crawler_user_agents: 'User-Agent''y robotów indeksujących, którym należy ograniczyć prędkość, zgodnie z ustawieniami "spowolnij szybkość indeksowania". Każda wartość musi mieć co najmniej 3 znaki.' slow_down_crawler_rate: "Jeśli określono parametr slow_down_crawler_user_agents, ta stawka będzie obowiązywać dla wszystkich robotów (liczba sekund opóźnienia między żądaniami)" content_security_policy: "Włącz politykę bezpieczeństwa treści" content_security_policy_report_only: "Włącz tylko treść-bezpieczeństwo-zasady-tylko raport" @@ -1650,7 +1664,7 @@ pl_PL: redirect_users_to_top_page: "Automatycznie kieruj nowych i długo nieobecnych użytkowników na stronę główną." top_page_default_timeframe: "Domyślne ramy czasowe dla strony widoku z góry." moderators_view_emails: "Zezwalaj moderatorom na przeglądanie wiadomości e-mail użytkowników" - prioritize_username_in_ux: "Pokaż nazwę użytkownika jako pierwszą na stronie użytkownika, karcie użytkownika i we wpisach (kiedy wyłączone imię jest pokazane jako pierwsze)" + prioritize_username_in_ux: "Pokaż nazwę użytkownika jako pierwszą na stronie użytkownika, jego karcie i w postach (gdy opcja jest wyłączona, pełna nazwa jest wyświetlana jako pierwsza)" enable_rich_text_paste: "Włącz automatyczną konwersję HTML do Markdown podczas wklejania tekstu do kompozytora. (Eksperymentalny)" send_old_credential_reminder_days: "Przypomnij o starych danych logowania po dniach" email_token_valid_hours: "Tokeny resetujące hasło / aktywujące konto są ważne przez (n) godzin." @@ -1660,7 +1674,9 @@ pl_PL: allow_index_in_robots_txt: "W pliku robots.txt określ, że ta witryna może być indeksowana przez wyszukiwarki internetowe. W wyjątkowych przypadkach możesz trwale zastąpić plik robots.txt." blocked_email_domains: "Lista domen poczty e-mail rozdzielonych pionową kreską, z których użytkownicy nie mogą rejestrować kont. Przykład: mailinator.com|trashmail.net" allowed_email_domains: "Lista rozdzielonych pionową kreską domen e-mail, z których użytkownicy MUSZĄ się rejestrować. UWAGA: Użytkownicy z domenami e-mail innymi niż wypisane tutaj nie będą mogli się zarejestrować!" + normalize_emails: "Sprawdź, czy znormalizowany e-mail jest unikalny. Znormalizowany e-mail usuwa wszystkie kropki z nazwy użytkownika i wszystko między symbolami + i @." auto_approve_email_domains: "Użytkownicy z adresami e-mail z tej listy domen zostaną automatycznie zatwierdzeni." + hide_email_address_taken: "Nie informuj użytkowników, że istnieje konto z podanym adresem e-mail podczas rejestracji lub podczas procesu przypominania hasła. Wymagaj podania pełnego adresu email przy prośbach o 'przypomnienie hasła'." log_out_strict: "Po wylogowaniu wyloguj WSZYSTKIE sesje użytkownika na wszystkich urządzeniach." version_checks: "Wyślij ping do Discourse Hub w celu uzyskania aktualizacji wersji i pokaż komunikaty o nowej wersji w panelu admina" new_version_emails: "Wyślij email na adres contact_email, kiedy nowa wersja Discourse będzie dostępna." @@ -1700,18 +1716,19 @@ pl_PL: enable_local_logins_via_email: "Zezwalaj użytkownikom na żądanie linku logowania do jednego kliknięcia, który zostanie im przesłany pocztą e-mail." allow_new_registrations: "Zezwól na rejestrację nowych użytkowników. Odznacz opcję żeby uniemożliwić rejestrację nowych kont." enable_signup_cta: "Pokazuj wiadomość dla powracających użytkowników anonimowych, zachęcającą ich do założenia konta." - enable_google_oauth2_logins: "Włącz uwierzytelnianie Google Oauth2. Jest to metoda uwierzytelniania obsługiwana obecnie przez Google. Wymaga klucza i tajemnicy. Zobacz Konfigurowanie logowania Google do dyskursu ." + enable_google_oauth2_logins: "Włącz uwierzytelnianie Google Oauth2. Jest to metoda uwierzytelniania obsługiwana obecnie przez Google. Wymaga podania klucza i sekretu. Zobacz Konfigurowanie logowania Google dla Discourse." google_oauth2_client_id: "Client ID twojej aplikacji w Google" google_oauth2_client_secret: "Client Secret twojej aplikacji w Google" google_oauth2_prompt: "Opcjonalna lista wartości łańcuchowych rozdzielanych spacjami, która określa, czy serwer autoryzacji monituje użytkownika o ponowne uwierzytelnienie i zgodę. Możliwe wartości można znaleźć na https://developers.google.com/identity/protocols/OpenIDConnect#prompt ." google_oauth2_hd: "Opcjonalna domena Google Apps Hosted, do której logowanie będzie ograniczone. Więcej informacji można znaleźć na https://developers.google.com/identity/protocols/OpenIDConnect#hd-param" - enable_twitter_logins: "Włącz uwierzytelnianie na Twitterze, wymaga twitter_consumer_key i twitter_consumer_secret. Zobacz Konfigurowanie logowania na Twitterze (i bogatych osadzeń) dla dyskursu ." + google_oauth2_hd_groups: "(eksperymentalne) Pobierz grupy dyskusyjne Google użytkowników w domenie hostowanej po uwierzytelnieniu. Pobrane grupy dyskusyjne Google mogą być używane do automatycznego przyznawania członkostwa w grupach Discourse (zobacz ustawienia grupy)." + enable_twitter_logins: "Włącz uwierzytelnianie przez Twittera, wymaga twitter_consumer_key i twitter_consumer_secret. Zobacz Konfigurowanie logowania przez Twittera (i bogatych embedów) dla Discourse." twitter_consumer_key: "Klucz klienta do uwierzytelnienia na Twitterze, zarejestrowany na https://developer.twitter.com/apps" twitter_consumer_secret: "Sekret klienta dotyczący uwierzytelniania na Twitterze, zarejestrowany na stronie https://developer.twitter.com/apps" - enable_facebook_logins: "Włącz uwierzytelnianie na Facebooku, wymaga facebook_app_id i facebook_app_secret. Zobacz Konfigurowanie logowania na Facebooku dla dyskursu ." + enable_facebook_logins: "Włącz uwierzytelnianie przez Facebooka, wymaga facebook_app_id i facebook_app_secret. Zobacz Konfigurowanie logowania przez Facebooka dla Discourse." facebook_app_id: "Identyfikator aplikacji do uwierzytelniania i udostępniania na Facebooku, zarejestrowany pod adresem https://developers.facebook.com/apps" facebook_app_secret: "Tajna aplikacja do uwierzytelniania na Facebooku, zarejestrowana na https://developers.facebook.com/apps" - enable_github_logins: "Włącz uwierzytelnianie GitHub, wymaga github_client_id i github_client_secret. Zobacz konfigurowanie logowania GitHub dla Discourse." + enable_github_logins: "Włącz uwierzytelnianie przez GitHub, wymaga github_client_id i github_client_secret. Zobacz Konfigurowanie logowania przez GitHub dla Discourse." github_client_id: "Identyfikator klienta do uwierzytelniania przez GitHub, zarejestrowany pod adresem https://github.com/settings/developers" github_client_secret: "Sekret klienta do uwierzytelniania przez GitHub, zarejestrowany pod adresem https://github.com/settings/developers" enable_discord_logins: "Zezwolić użytkownikom na uwierzytelnianie przy użyciu Discord?" @@ -1768,7 +1785,7 @@ pl_PL: clean_up_uploads: "Usuń osierocone pliki aby zapobiec wykorzystywaniu forum jako hosting. UWAGA: przed włączeniem tej opcji zaleca się wykonanie kopii katalogu /uploads." clean_orphan_uploads_grace_period_hours: "Okres karencji (w dniach) przed wysłaniem sierot zostanie skasowany." purge_deleted_uploads_grace_period_days: "Okres karencji (w dniach) przed usunięciem upload zostanie skasowany." - purge_unactivated_users_grace_period_days: "Okres karencji w dniach, po upływie którego nieaktywowane konto użytkownika zostaje usunięte. Ustaw 0, by nigdy nie usuwań nieaktywnych użytkowników." + purge_unactivated_users_grace_period_days: "Okres karencji w dniach, po upływie którego nieaktywowane konto użytkownika zostaje usunięte. Ustaw na 0, aby nigdy nie usuwać nieaktywowanych użytkowników." enable_s3_uploads: "Umieść przesyły w pamięci Amazon S3. Ważne: wymaga ważnych danych uwierzytelniających (zarówno klucza id i tajnego klucza dostępu)" s3_use_iam_profile: 'Użyj profilu instancji AWS EC2, aby przyznać dostęp do segmentu S3. UWAGA: włączenie tej opcji wymaga, aby dyskurs działał w odpowiednio skonfigurowanej instancji EC2 i zastępuje ustawienia „Identyfikatora klucza dostępu s3” i „Tajnego klucza dostępu s3”.' s3_upload_bucket: "Nazwa koszyka Amazon S3, do którego zostaną przesłane pliki. Ostrzeżenie: bez wielkich liter, kropek czy podkreślenia." @@ -1848,6 +1865,9 @@ pl_PL: max_mentions_per_post: "Maksymalna liczba powiadomień poprzez @nazwę w jednym wpisie (dla wszystkich)." max_users_notified_per_group_mention: "Maksymalna liczba użytkowników, którzy mogą otrzymać powiadomienie jeśli ktoś wspomniał(a) o grupie (jeśli próg został osiągnięty, nie będzie żadnych powiadomień)" enable_mentions: "Zezwól użytkownikom na wzmianki innych użytkowników." + here_mention: "Nazwa użyta do wzmianki @here. Nie może być istniejącą nazwą użytkownika." + max_here_mentioned: "Maksymalna liczba osób oznaczonych przez @here." + min_trust_level_for_here_mention: "Minimalny poziom zaufania do oznaczania @here." create_thumbnails: "Stwórz miniatury i obrazy lightbox, które są za duże, aby pasować do wpisu." email_time_window_mins: "Odczekaj (n) minut przed wysłaniem e-maila z powiadomieniem, aby dać użytkownikom szansę na edytowanie i ukończenie wpisów." personal_email_time_window_seconds: "Poczekaj (n) sekund przed wysłaniem powiadomienia na e-mail o prywatnej wiadomości, by użytkownicy mogli edytować i dokończyć swoje wiadomości." @@ -1898,6 +1918,7 @@ pl_PL: topic_view_duration_hours: "Licz wyświetlanie nowego tematu na IP/Użytkownika co N godzin" user_profile_view_duration_hours: "Licz nowe wyświetlenia profilu użytkownika na IP/Użytkownika co N godzin" levenshtein_distance_spammer_emails: "Przy dopasowywaniu emaili spamowych, różnica liczby znaków, która w dalszym ciągu pozwoli na przybliżone dopasowanie." + max_new_accounts_per_registration_ip: "Jeśli istnieje już (x) kont o poziomie zaufania 0 z tego adresu IP (i żadne z nich nie jest członkiem personelu ani na poziomie TL2 lub wyższym), przestań akceptować nowe rejestracje z tego adresu IP. Ustaw na 0, aby wyłączyć limit." min_ban_entries_for_roll_up: "Podczas naciskania na przycisk Przewiń do góry, utwórz nowy wpis blokady podsieci jeśli jest co najmniej (N) wpisów." max_age_unmatched_emails: "Usuń niedopasowany ekranowany wpis email po (N) dniach." max_age_unmatched_ips: "Usuń niedopasowany ekranowany wpis IP po (N) dniach." @@ -1982,7 +2003,7 @@ pl_PL: delete_all_posts_max: "Maksymalna liczba wpisów jaka może zostać usunięta jednorazowo przyciskiem \"Usuń Wszystkie Wpisy\". Jeśli użytkownik ma więcej wpisów, ani one ani użytkownik nie mogą one być usunięte." delete_user_self_max_post_count: "Maksymalna liczba wpisów, jaką może posiadać użytkownik, umożliwiając usunięcie konta samoobsługowego. Ustaw na -1, aby wyłączyć usuwanie konta samoobsługowego." username_change_period: "Maksymalna liczba dni po rejestracji, w których konta mogą zmienić swoją nazwę użytkownika (0, aby uniemożliwić zmianę nazwy użytkownika)." - email_editable: "Allow users to change their e-mail address after registration." + email_editable: "Zezwól użytkownikom na zmianę adresu e-mail po rejestracji." logout_redirect: "Lokalizacja do przekierowania przeglądarki po wylogowaniu (np .: https://example.com/logout)" allow_uploaded_avatars: "Zezwól użytkownikom na przesyłanie niestandardowych zdjęć profilowych." default_avatars: "Adresy URL awatarów wykorzystywanych jako domyślne w przypadku nowych użytkowników do momentu, gdy je zmienią." @@ -2003,18 +2024,18 @@ pl_PL: public_user_custom_fields: "Lista niestandardowych pól użytkownika, które można uzyskać korzystających z interfejsu API." staff_user_custom_fields: "Lista pól niestandardowych użytkownika, które można uzyskać przez członków personelu korzystających z interfejsu API." enable_user_directory: "Dostarcz katalog użytkowników do wyszukiwania" - enable_group_directory: "Dostarcz katalog grup do wyszukiwania" + enable_group_directory: "Udostępnij katalog grup do przeglądania" enable_category_group_moderation: "Zezwalaj grupom na moderowanie treści w określonych kategoriach" group_in_subject: "Ustaw %%{optional_pm} w temacie wiadomości e-mail z nazwą pierwszej grupy w prywatnej wiadomości, zobacz: Dostosuj format tematu dla standardowych wiadomości e-mail" allow_anonymous_posting: "Zezwól użytkownikom na przechodzenie w tryb anonimowości." anonymous_posting_min_trust_level: "Minimalny poziom zaufania potrzebny by aktywować anonimowe wpisy" anonymous_account_duration_minutes: "Aby chronić anonimowość twórz nowe anonimowe konto co N minut dla każdego użytkownika. Przykład: jeśli ustawione na 600, dopóki nie minie 600 minut od ostatniego wpisu i użytkownik przełącza się na anon, zostanie utworzone nowe anonimowe konto." - hide_user_profiles_from_public: "Wyłącz karty, profile i katalogi użytkownika dla użytkownika anonimowego." + hide_user_profiles_from_public: "Wyłącz karty użytkownika, profile użytkowników i katalog użytkowników dla użytkowników anonimowych." allow_users_to_hide_profile: "Zezwalaj użytkownikom na ukrywanie swojego profilu i statusu dostępności." allow_featured_topic_on_user_profiles: "Zezwalaj użytkownikom na umieszczanie linków do tematu na ich karcie i profilu użytkownika." show_inactive_accounts: "Pozwól zalogowanym użytkownikom przeglądać profile nieaktywnych kont." hide_suspension_reasons: "Nie wyświetlaj publicznie powodów zawieszenia w profilach użytkowników." - log_personal_messages_views: "Rejestruj osobiste wyświetlenia wiadomości przez administratora dla innych użytkowników/grup." + log_personal_messages_views: "Rejestruj wyświetlenia wiadomości osobistych przez administratora dla innych użytkowników/grup." ignored_users_count_message_threshold: "Powiadom moderatorów, jeśli dany użytkownik jest ignorowany przez wielu innych użytkowników." ignored_users_message_gap_days: "Ile trzeba czekać, zanim ponownie powiadomimy moderatorów o użytkowniku, który został zignorowany przez wielu członków społeczność." clean_up_inactive_users_after_days: "Liczba dni do usunięcia nieaktywnego użytkownika (poziom zaufania 0 bez żadnych postów). Aby wyłączyć czyszczenie, ustaw na 0." @@ -2034,6 +2055,7 @@ pl_PL: global_notice: "Wyświetlaj PILNE, AWARYJNE lub zawsze widoczny banner informacyjny, który będzie widoczny dla wszystkich odwiedzających. Ustaw to pole jako puste, aby go ukryć. (HTML dozwolony)" disable_system_edit_notifications: "Wyłącz edytowanie powiadomień przez system użytkownika gdy 'download_remote_images_to_local' jest aktywne." disable_category_edit_notifications: "Wyłącz powiadomienia o edycji kategorii w tematach." + disable_tags_edit_notifications: "Wyłącz powiadomienia o edycji tagów w tematach." notification_consolidation_threshold: "Liczba polubień lub powiadomień o członkostwie otrzymanych przed połączeniem powiadomień w jedno. Ustaw 0 aby wyłączyć." likes_notification_consolidation_window_mins: "Czas w minutach, po którym powiadomienia o polubieniach są łączone w jedno powiadomienie. Próg można skonfigurować za pomocą `SiteSetting.notification_consolidation_threshold`." automatically_unpin_topics: "Automatycznie odpinaj tematy, gdy użytkownik dotknie dna." @@ -2048,8 +2070,8 @@ pl_PL: penalty_step_hours: "Domyślne kary za wyciszanie lub zawieszanie użytkowników w godzinach. Pierwsze wykroczenie domyślnie jest pierwszą wartością, drugie wykroczenie jest domyślne do drugiej wartości, itd." svg_icon_subset: "Dodaj dodatkowe ikony FontAwesome 5, które chcesz umieścić w swoich zasobach. Użyj przedrostka 'fa-' dla wypełnionych ikon, 'far-' dla zwykłych ikon i 'fab-' dla ikon marek." max_prints_per_hour_per_user: "Maksymalna liczba /drukowanych stron (ustaw na 0, aby wyłączyć)" - full_name_required: "Pełne imię i nazwisko to wymagane pole w profilu użytkownika." - enable_names: "Pokaż pełną nazwę użytkownika na profilu, karcie użytkownika i w emailach. Wyłącz opcję ukrywania pełnej nazwy wszędzie." + full_name_required: "Pełna nazwa jest wymaganym polem profilu użytkownika." + enable_names: "Pokaż pełną nazwę użytkownika w jego profilu, karcie użytkownika i w e-mailach. Wyłącz, aby ukryć pełną nazwę użytkownika wszędzie." display_name_on_posts: "Pokazuj imię i nazwisko użytkownika przy jego wpisach, a także jego @nazwę użytkownika." show_time_gap_days: "Jeśli dwa wpisy zostały dodane w dużej przerwie, wyświetl okres w temacie." short_progress_text_threshold: "Po tym jak liczba wpisów w tym temacie przekroczy ten numer, ten pasek postępu pokaże tylko bieżącą liczbę wpisów. Jeśli zmienisz szerokość paska postępu, możliwe, że będziesz musiał zmienić tą wartość." @@ -2179,10 +2201,12 @@ pl_PL: share_quote_visibility: "Określ, kiedy wyświetlać przyciski udostępniania cytatów: nigdy, tylko anonimowym użytkownikom lub wszystkim użytkownikom. " create_revision_on_bulk_topic_moves: "Utwórz rewizję dla pierwszych postów, gdy tematy zostaną przeniesione do nowej kategorii zbiorczo." allow_changing_staged_user_tracking: "Zezwalaj na zmianę preferencji powiadomień o kategoriach i tagach użytkownika etapowego przez administratora." + use_email_for_username_and_name_suggestions: "Użyj pierwszej części adresów e-mail jako sugestii dla nazwy użytkownika i pełnej nazwy. Pamiętaj, że ułatwia to odgadnięcie pełnych adresów e-mail użytkowników (ponieważ duża część osób korzysta z wspólnych usług, takich jak `gmail.com`)." errors: invalid_css_color: "Nieprawidłowy kolor. Wpisz nazwę koloru lub wartość szesnastkową." invalid_email: "Nieprawidłowy adres e-mail." invalid_username: "Użytkownik o takiej nazwie nie istnieje." + valid_username: "Istnieje użytkownik o tej nazwie." invalid_group: "Nie ma grupy o takiej nazwie." invalid_integer_min_max: "Wartość musi być między %{min} i %{max}." invalid_integer_min: "Wartość musi wynosić %{min} albo więcej." @@ -2197,6 +2221,7 @@ pl_PL: invalid_json: "Nieprawidłowy JSON." invalid_reply_by_email_address: "Wartość musi zawierać '%{reply_key}' i musi być inna niż e-mail z powiadomieniem." invalid_alternative_reply_by_email_addresses: "Wszystkie wartości muszą zawierać '%{reply_key}' i być inne niż w emailu powiadamiającym." + invalid_domain_hostname: "Nie może zawierać znaków * lub ?" pop3_polling_host_is_empty: "Musisz ustawić 'pop3 polling host' przed włączeniem przeglądania cyklicznego POP3." pop3_polling_username_is_empty: "Musisz ustawić 'pop3 polling username' przed włączeniem przeglądania cyklicznego POP3." pop3_polling_password_is_empty: "Musisz ustawić 'pop3 polling password' przed włączeniem przeglądania cyklicznego POP3." @@ -2225,6 +2250,7 @@ pl_PL: leading_trailing_slash: "Wyrażenie regularne nie może zaczynać się ani kończyć ukośnikiem." unicode_usernames_avatars: "Wewnętrzne awatary systemowe nie obsługują nazw użytkowników zawierających unicode." list_value_count: "Lista musi zawierać dokładnie %{count} wartości." + google_oauth2_hd_groups: "Musisz najpierw ustawić 'google oauth2 hd' przed włączeniem tego ustawienia." placeholder: discourse_connect_provider_secrets: key: "www.example.com" @@ -2599,6 +2625,21 @@ pl_PL: test_mailer: title: "Test Powiadomień" subject_template: "[%{email_prefix}] Test dostarczania poczty" + text_body_template: | + To jest testowa wiadomość e-mail od + + [**%{base_url}**][0] + + Mamy nadzieję, że otrzymałeś ten test dostarczalności wiadomości e-mail! + + Oto [przydatna lista kontrolna do weryfikacji konfiguracji dostarczania poczty e-mail][1]. + + Powodzenia, + + Twoi przyjaciele z [Discourse](https://www.discourse.org) + + [0]: %{base_url} + [1]: https://meta.discourse.org/t/email-delivery-configuration-checklist/209839 new_version_mailer: title: "Nowa wersja powiadomień" subject_template: "[%{email_prefix}] Nowa wersja Discourse jest dostępna" @@ -2636,6 +2677,11 @@ pl_PL: inappropriate: "Twój wpis został oznaczony jako **nieodpowiedni**: społeczność uznała, że zbyt agresywny lub niezgodny z [jej wytycznymi](%{base_path}/guidelines)." spam: "Twój wpis został oznaczony jako **spam**: społeczność czuje, że jest to reklama, czyli coś nadmiernie promującego, a nie użytecznego czy odnoszącego się do tematu." notify_moderators: "Twój wpis został oznaczony jako **do uwagi moderatora**: społeczność czuje, że wpis wymaga ręcznej interwencji członka personelu." + responder: + off_topic: "Post został oznaczony jako **nie na temat**: społeczność uważa, że nie pasuje on do tematu, który obecnie określa tytuł i pierwszy post." + inappropriate: "Post został oznaczony jako **nieodpowiedni**: społeczność uważa, że jest obraźliwy, wulgarny lub narusza [nasze wytyczne dla społeczności](%{base_path}/guidelines)." + spam: "Post został oznaczony jako **spam**: społeczność uważa, że jest to reklama, lub coś, co ma charakter nadmiernie promocyjny, a nie jest przydatne lub związane z tematem zgodnie z oczekiwaniami." + notify_moderators: "Post został oznaczony **w celu zwrócenia uwagi moderatora**: społeczność uważa, że coś w tym poście wymaga ręcznej interwencji członka personelu." flags_dispositions: agreed: "Dziękujemy za zwrócenie uwagi. Zgadzamy się, że jest to problem i przyjrzymy się temu. " agreed_and_deleted: "Dziękujemy za zwrócenie uwagi. Zgadzamy się, że jest to problem i wpis został usunięty. " @@ -2648,6 +2694,13 @@ pl_PL: many: "Ten temat jest tymczasowo zamknięty na co najmniej %{count} godzin z powodu dużej liczby flag społeczności." other: "Ten temat jest tymczasowo zamknięty na co najmniej %{count} godzin z powodu dużej liczby flag społeczności." system_messages: + reviewables_reminder: + subject_template: "W kolejce do sprawdzenia znajdują się elementy, które wymagają uwagi" + text_body_template: + one: "%{mentions} Wiadomości zostały wysłane ponad %{count} godzinę temu. [Sprawdź je](%{base_url}/review)." + few: "%{mentions} Wiadomości zostały wysłane ponad %{count} godziny temu. [Sprawdź je](%{base_url}/review)." + many: "%{mentions} Wiadomości zostały wysłane ponad %{count} godzin temu. [Sprawdź je](%{base_url}/review)." + other: "%{mentions} Wiadomości zostały wysłane ponad %{count} godzin temu. [Sprawdź je](%{base_url}/review)." private_topic_title: "Temat#%{id}" contents_hidden: "Proszę przejść do wpisu, aby wyświetlić jego zawartość." post_hidden: @@ -2727,6 +2780,29 @@ pl_PL: ``` Zapoznaj się z naszymi [wytycznymi społeczności](%{base_url}/guidelines) po więcej szczegółów. + flags_agreed_and_post_deleted_for_responders: + title: "Odpowiedź usunięta z oflagowanego postu przez personel" + subject_template: "Odpowiedź usunięta z oflagowanego postu przez personel" + text_body_template: | + Witaj, + + To jest automatyczna wiadomość od %{site_name} informująca, że [post](%{base_url}%{url}), na który odpowiedziałeś, został usunięty. + + %{flag_reason} + + Ten post został oznaczony przez społeczność, a członek personelu zdecydował się go usunąć. + + ``` markdown + %{flagged_post_raw_content} + ``` + + Na który odpowiedziałeś + + ``` markdown + %{flagged_post_response_raw_content} + ``` + + Aby uzyskać więcej informacji na temat powodu usunięcia, zapoznaj się z naszymi [wytycznymi dla społeczności](%{base_url}/guidelines). usage_tips: text_body_template: | Aby uzyskać kilka szybkich wskazówek, jak zacząć jako nowy użytkownik, [przeczytaj ten post na blogu](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). @@ -3384,6 +3460,7 @@ pl_PL: %{respond_instructions} user_group_mentioned_pm: + title: "Wspomniana grupa użytkowników PW" subject_template: "[%{email_prefix}] [PW] %{topic_title}" text_body_template: | %{header_instructions} @@ -3394,6 +3471,7 @@ pl_PL: %{respond_instructions} user_group_mentioned_pm_group: + title: "Wspomniana grupa użytkowników PW" subject_template: "[%{email_prefix}] [PW] %{topic_title}" text_body_template: | %{header_instructions} diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 120a8a59db..a7f59c92b5 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -319,8 +319,6 @@ pt: too_many_links: one: "Pedimos desculpa, novos utilizadores podem colocar apenas %{count} hiperligação numa mensagem." other: "Pedimos desculpa, novos utilizadores podem colocar apenas %{count} hiperligações numa mensagem." - contains_blocked_word: "Sua postagem contém uma palavra que não é permitida: %{word}" - contains_blocked_words: "Sua postagem contém algumas palavras que não são permitidas: %{words}" 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?" @@ -979,7 +977,6 @@ pt: max_topic_title_length: "Tamanho máximo permitido por título de cada tópico, em caracteres" min_personal_message_title_length: "Tamanho mínimo permitido por título nas mensagens, em caracteres" min_search_term_length: "Tamanho mínimo válido para termos de pesquisa, em caracteres" - search_tokenize_chinese_japanese_korean: "Forçar pesquisa para atomizar Chinês/Japonês/Coreano mesmo em sites que não sejam CJC" search_prefer_recent_posts: "Se pesquisar no seu fórum vasto é lento, esta opção tenta usar um índex de publicações recentes primeiro" search_recent_posts_size: "Quantas publicações recentes a manter no índex" search_query_log_max_size: "O número máximo de consultas de pesquisa a serem mantidas" @@ -1006,7 +1003,6 @@ pt: show_pinned_excerpt_mobile: "Mostrar excerto em publicações afixadas na vista móvel." show_pinned_excerpt_desktop: "Mostar excerto em publicações afixadas na vista de desktop." post_onebox_maxlength: "Tamanho máximo de uma mensagem Discourse de caixa única, em caracteres." - blocked_onebox_domains: "Uma lista de domínios para a qual nunca serão geradas pré-visualizações." max_oneboxes_per_post: "Número máximo de pré-visualizações numa publicação." manifest_screenshots: "Capturas de tela que mostrem os recursos e a funcionalidade da instância na página de incentivo à instalação. Todas as imagens devem ser envios locais e das mesmas dimensões." notification_email: "Para: endereço de email usado ao enviar emails essenciais do sistema. O domínio especificado aqui deverá ter SPF, DKIM e registos PTR inversos configurados corretamente para a chegada do email." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 7b0b954f05..7fd9ff968d 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -338,8 +338,6 @@ pt_BR: too_many_links: one: "Desculpe, usuários(as) novos(as) podem inserir apenas %{count} link em uma postagem." other: "Desculpe, usuários(as) novos(as) podem inserir apenas %{count} links em uma postagem." - contains_blocked_word: "Sua postagem contém uma palavra que não é permitida: %{word}" - contains_blocked_words: "Sua postagem contém várias palavras que não são permitidas: %{words}" spamming_host: "Desculpe, você não pode postar um link para esse host." user_is_suspended: "Usuários(as) suspensos(as) não têm permissão para postar." topic_not_found: "Ocorreu um erro. Talvez este tópico tenha sido fechado ou removido enquanto você estava visualizando." @@ -1401,7 +1399,6 @@ pt_BR: min_personal_message_title_length: "Tamanho mínimo permitido para o título de uma mensagem em caracteres" max_emojis_in_title: "Emojis permitidos máximos no título do tópico" min_search_term_length: "Tamanho mínimo válido para termo de pesquisa em caracteres" - search_tokenize_chinese_japanese_korean: "Forçar a pesquisa a transformar em token caracteres de mandarim/japonês/coreano até para sites que não estão nesses idiomas" search_prefer_recent_posts: "Se as pesquisas no seu fórum estiverem lentas, essa opção tenta indexar as postagens mais recentes primeiro" search_recent_posts_size: "Quantidade de postagens recentes para serem mantidas no índice" log_search_queries: "Registrar consultas de pesquisa realizadas pelos(as) usuários(as)" @@ -1445,7 +1442,6 @@ pt_BR: show_pinned_excerpt_mobile: "Mostre trecho de tópicos fixados na visualização para dispositivos móveis." show_pinned_excerpt_desktop: "Mostrar trecho de tópicos fixados na visualização para desktop." post_onebox_maxlength: "Tamanho máximo para uma postagem do Discourse no Onebox." - blocked_onebox_domains: "Uma lista de domínios que nunca serão transformados em Onebox." allowed_inline_onebox_domains: "Uma lista de domínios que serão colocados em miniatura no Onebox se forem vinculados sem um título" enable_inline_onebox_on_all_domains: "Ignore a configuração do site inline_onebox_domain_whitelist e permita inclusões de Onebox em todos os domínios." force_custom_user_agent_hosts: "Hosts para os quais usar um agente do(a) usuário(a) do Onebox em todos os pedidos. (Especialmente útil para hosts que limitam acesso do agente do(a) usuário(a).)" @@ -1533,7 +1529,7 @@ pt_BR: allowed_iframes: "Uma lista de prefixos de domínio iframe src que o Discourse pode permitir com segurança em postagens" allowed_crawler_user_agents: "Agentes de usuário(a) de rastreadores da web que devem ter permissão para acessar o site. AVISO! ESTA CONFIGURAÇÃO NEGARÁ ACESSO A TODOS OS RASTREADORES NÃO LISTADOS AQUI!" blocked_crawler_user_agents: "Palavra sem diferenciação de maiúsculas e minúsculas na linha do agente do(a) usuário(a) que identifica os rastreadores da web que não devem ter permissão para acessar o site. Não se aplica se a lista de permissões estiver definida." - slow_down_crawler_user_agents: "Agentes de usuário(a) de rastreadores da web que devem ter a taxa limitada conforme a configuração \"reduzir taxa de rastreador\". Cada valor dever ter pelo menos três caracteres." + slow_down_crawler_user_agents: 'Agentes de usuário(a) de rastreadores da web que devem ter a taxa limitada conforme a configuração "reduzir taxa de rastreador". Cada valor dever ter pelo menos três caracteres.' slow_down_crawler_rate: "Se slow_down_crawler_user_agents for especificado, essa taxa será aplicada a todos os rastreadores (quantidade de segundos de atraso entre os pedidos)" content_security_policy: "Ativar Content-Security-Policy" content_security_policy_report_only: "Ativar Content-Security-Policy-Report-Only" diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 9667e4a786..7c03bd170d 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -810,7 +810,6 @@ ro: max_topic_title_length: "Maximul de caractere permis în titlul unui subiect" min_personal_message_title_length: "Numărul minim de caractere permis în titlul unui mesaj" min_search_term_length: "Lungimea minimă a unui termen de căutare valid, în caractere." - search_tokenize_chinese_japanese_korean: "Forțează căutarea să utilizeze tokens chineză/japoneză/coreană chiar și pe site-uri care nu sunt CJK." search_prefer_recent_posts: "Dacă ai un forum mare și căutarea este prea lentă, această opțiune încearcă să indexeze mai multe postări recente întâi." search_recent_posts_size: "Câte postări recente să fie ținute în index" allow_uncategorized_topics: "Permite crearea de subiecte fără categorie. ATENȚIE: Dacă o să ai postări fără categorie, va trebui să le identifici înainte să poți dezactiva din nou această setare." @@ -835,7 +834,6 @@ ro: show_pinned_excerpt_mobile: "Arată fragmente din subiectele fixate în vizualizarea de mobil." show_pinned_excerpt_desktop: "Arată fragmente din subiectele fixate în vizualizarea de desktop." post_onebox_maxlength: "Lungimea maximă a unei postări afișate în Onebox (în caractere)" - blocked_onebox_domains: "O listă cu domenii care nu vor putea fi utilizate cu onebox." max_oneboxes_per_post: "Numărul maxim de utilizări onebox per post." notification_email: "Adresă expeditor: adresa de email folosită când se trimit toate emailurile esențiale ale sistemului. Domeniul specificat aici trebuie să aibă setate corect SPF, DKIM și reverse PTR pentru ca emailul să poată fi livrat." email_custom_headers: "O listă delimitată cu simbolul | (pipe) a header-elor personalizate de email" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 8919612545..0be9e49884 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -376,8 +376,8 @@ ru: few: "Извините, новые пользователи могут размещать только %{count} ссылки в сообщении." many: "Извините, новые пользователи могут размещать только %{count} ссылок в сообщении." other: "Извините, новые пользователи могут размещать только %{count} ссылок в сообщении." - contains_blocked_word: "Ваше сообщение содержит запрещённое слово: %{word}" - contains_blocked_words: "Ваше сообщение содержит несколько запрещённых слов: %{words}" + contains_blocked_word: "Извините, но вы не можете использовать слово '%{word}'; оно недопустимо." + contains_blocked_words: "Извините, но вы не можете опубликовать это. Не допускаются следующие слова: %{words}." spamming_host: "Извините, но вы не можете разместить ссылку на этот ресурс." user_is_suspended: "Замороженные пользователи не могут создавать сообщения." topic_not_found: "Что-то пошло не так. Возможно, эта тема была закрыта или удалена, пока вы её читали?" @@ -1487,6 +1487,7 @@ ru: unreachable_themes: "Нам не удалось проверить наличие обновлений для следующих тем:" watched_word_regexp_error: "Недопустимое регулярное выражение для контролируемых слов в разделе '%{action}'. Пожалуйста, проверьте корректность регулярных выражений, или отключите в настройках сайта настройку 'Контролируемые слова представлены регулярными выражениями'." site_settings: + allow_bulk_invite: "Разрешить массовые приглашения путём загрузки CSV-файла" disabled: "отключён" display_local_time_in_user_card: "Отображать местное время в карточке пользователя." censored_words: "Слова, которые будут автоматически заменены на ■■■■" @@ -1507,7 +1508,8 @@ ru: min_personal_message_title_length: "Минимально допустимое количество символов в заголовке личного сообщения" max_emojis_in_title: "Максимальное количество эмодзи в названии темы" min_search_term_length: "Минимальное количество символов в поисковом запросе" - search_tokenize_chinese_japanese_korean: "Принудительный поиск для токенизации Китайского/Японского /Корейского языков даже на сайтах, не содержащих символы CJK" + search_tokenize_chinese: "Принудительный поиск для токенизации китайского языка даже на некитайских сайтах" + search_tokenize_japanese: "Принудительный поиск для токенизации японского языка даже на неяпонских сайтах" search_prefer_recent_posts: "Сначала индексировать последние сообщения. Актуально при медленном поиске на больших объёмах данных" search_recent_posts_size: "Хранить в индексе указанное здесь количество сообщений" log_search_queries: "Сохранять в журнале поисковые запросы пользователей" @@ -1551,7 +1553,7 @@ ru: show_pinned_excerpt_mobile: "Показывать фрагменты закреплённых тем в мобильном представлении." show_pinned_excerpt_desktop: "Показывать фрагменты закреплённых тем в настольных устройствах." post_onebox_maxlength: "Максимальная длина сообщения в режиме умной вставки." - blocked_onebox_domains: "Список доменов, контент с которых не будет преобразовываться в умную вставку." + blocked_onebox_domains: "Список доменов, которые не будут преобразовываться в умную вставку, например wikipedia.org\n(подстановочные знаки '*' и '?' не поддерживаются)." allowed_inline_onebox_domains: "Список доменов, контент с которых будет преобразовываться в умную вставку, если ссылка указана без заголовка." enable_inline_onebox_on_all_domains: "Игнорировать параметр `inline_onebox_domain_whitelist` и разрешить умную вставку для всех доменов." force_custom_user_agent_hosts: "Хосты, для которых можно использовать пользовательский User-Agent умных вставок для всех запросов. (Особенно полезно для хостов, которые ограничивают доступ пользовательским агентам)." @@ -1639,7 +1641,7 @@ ru: allowed_iframes: "Список iframe src, которым Discourse может разрешить встраивание в сообщения." allowed_crawler_user_agents: "Перечень поисковых ботов, которым должен быть разрешён доступ к сайту. ПРЕДУПРЕЖДЕНИЕ! УСТАНОВКА ЭТОГО ПАРАМЕТРА ЗАПРЕТИТ ДОСТУП ВСЕМ ПОИСКОВЫМ БОТАМ, НЕ УКАЗАННЫМ В ПЕРЕЧНЕ!" blocked_crawler_user_agents: "Уникальное нечувствительное к регистру слово в строке User-Agent, идентифицирующее поисковых ботов, которым запрещён доступ к сайту. Не применяется, если определён белый список." - slow_down_crawler_user_agents: "User agents, скорость работы которых должна быть ограничена в соответствии с настройкой «slow down crawler rate». Каждое значение должно состоять не менее чем из 3 символов." + slow_down_crawler_user_agents: 'User agents, скорость работы которых должна быть ограничена в соответствии с настройкой «slow down crawler rate». Каждое значение должно состоять не менее чем из 3 символов.' slow_down_crawler_rate: "Если указано значение slow_down_crawler_user_agents (количество секунд между запросами), то это значение будет применяться ко всем веб-сканерам" content_security_policy: "Включить политику безопасности контента (CSP)" content_security_policy_report_only: "Включить только отчёт о политике безопасности контента (CSP)" @@ -2203,6 +2205,7 @@ ru: share_quote_visibility: "Определите, когда показывать кнопки обмена цитатами: никогда, только анонимным пользователям или всем пользователям. " create_revision_on_bulk_topic_moves: "Добавлять первые сообщения в историю редактирования при массовом перемещении тем в другой раздел." allow_changing_staged_user_tracking: "Разрешить пользователю с правами администратора изменять настройки уведомлений для сымитированных пользователей в разделах и тегах." + use_email_for_username_and_name_suggestions: "Предлагать первую часть адресов электронной почты в качестве имени и псевдонима пользователя. Обратите внимание, что это упрощает угадывание полных адресов электронной почты пользователей (поскольку большая часть людей использует такие известные службы как `gmail.com`)." errors: invalid_css_color: "Недопустимый цвет. Введите название цвета или шестнадцатеричное значение." invalid_email: "Неправильный адрес электронной почты." @@ -2222,6 +2225,7 @@ ru: invalid_json: "Недопустимый формат JSON." invalid_reply_by_email_address: "Значение должно содержать '%{reply_key}' и должно отличаться от письма уведомления." invalid_alternative_reply_by_email_addresses: "Все значения должны содержать '%{reply_key}' и должны отличаться от письма уведомления." + invalid_domain_hostname: "Доменное имя не должно содержать символы '*' или '?'." pop3_polling_host_is_empty: "Вы должны установить 'pop3 polling host' перед включением POP3 polling." pop3_polling_username_is_empty: "Вы должны установить 'pop3 polling username' перед включением POP3 polling." pop3_polling_password_is_empty: "Вы должны установить 'pop3 polling password' перед включением POP3 polling." @@ -2251,6 +2255,8 @@ ru: unicode_usernames_avatars: "Внутренние системные аватары не поддерживают псевдонимы в формате Unicode." list_value_count: "Список должен содержать именно %{count} значений." google_oauth2_hd_groups: "Перед включением этого параметра необходимо настроить параметр 'google oauth2 hd'." + search_tokenize_chinese_enabled: "Вы должны отключить «search_tokenize_chinese», прежде чем включить этот параметр." + search_tokenize_japanese_enabled: "Вы должны отключить «search_tokenize_japanese», прежде чем включить этот параметр." placeholder: discourse_connect_provider_secrets: key: "www.example.com" @@ -4349,7 +4355,7 @@ ru: name: Первая умная вставка description: Опубликовал ссылку, которая была преобразована в умную вставку long_description: | - Эта награда выдается при первом размещении ссылки в отдельной строке, которая автоматически разворачивается в умную вставку, содержащую сводку, заголовок и (при наличии) изображение. + Эта награда выдаётся при первом размещении ссылки в отдельной строке, которая автоматически разворачивается в умную вставку, содержащую сводку, заголовок и (при наличии) изображение. first_reply_by_email: name: Первый ответ по почте description: Ответил на пост по электронной почте diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 4b7e63dc47..9bb5ea21e3 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -788,7 +788,6 @@ sk: max_topic_title_length: "Maximálny povolený počet znakov v názve témy" min_personal_message_title_length: "Minimálny povolený počet znakov v správe" min_search_term_length: "Minimálny povolený počet znakov vo vyhľadávaní" - search_tokenize_chinese_japanese_korean: "Prinúť vyhľádávanie rozložiť Čínštinu/Japončinu/Kórejčinu dokonca i pre nie CJK stránky" allow_uncategorized_topics: "Pvoliť vytváranie tém bez kategórií. UPOZORNENIE: Pokiaľ existujú nekategorizované témy, musíte ich zaradiť do kategórii skôr než túto možnosť vypnete." allow_duplicate_topic_titles: "Povoliť témy s rovnakými, duplikovanými názvami" unique_posts_mins: "Koľko minút musí byť medzi dvomi rovnakými príspevkami od jedného používateľa." diff --git a/config/locales/server.sl.yml b/config/locales/server.sl.yml index bc875d5cee..f5ae75d697 100644 --- a/config/locales/server.sl.yml +++ b/config/locales/server.sl.yml @@ -172,8 +172,6 @@ sl: two: "Novi uporabniki lahko v prispevek dodajo samo %{count} povezavi." few: "Novi uporabniki lahko v prispevek dodajo samo %{count} povezave." other: "Novi uporabniki lahko v prispevek dodajo samo %{count} povezav." - contains_blocked_word: "Vaš prispevek vsebuje prepovedano besedo: %{word}" - contains_blocked_words: "Vaš prispevek vsebuje prepovedane besede: %{words}" user_is_suspended: "Suspendirani uporabniki ne morejo objavljati." pm_reached_recipients_limit: "Ne morete imeti več kot %{recipients_limit} prejemnikov v sporočilu." removed_direct_reply_full_quotes: "Samodejno odstranjen citat celotnega prejšnega prispevka." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 26831ac624..f5c3c61b1d 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -338,8 +338,8 @@ sv: too_many_links: one: "Tyvärr kan nya användare bara ha en länk i ett inlägg." other: "Tyvärr kan nya användare bara ha %{count} länkar i ett inlägg." - contains_blocked_word: "Ditt inlägg innehåller ett ord som inte är tillåtet: %{word}" - contains_blocked_words: "Ditt inlägg innehåller flera ord som inte är tillåtna: %{words}" + contains_blocked_word: "Tyvärr, du kan inte använda ordet '%{word}'; det är inte tillåtet." + contains_blocked_words: "Tyvärr, du kan inte skriva det. Ej tillåtet: %{words}." spamming_host: "Tyvärr kan du inte publicera en länk till den värden." user_is_suspended: "Avstängda användare har inte tillåtelse att göra inlägg" topic_not_found: "Något har gått fel. Kanske har ämnet stängts eller raderats medan du tittade på det?" @@ -1361,6 +1361,7 @@ sv: unreachable_themes: "Vi kunde inte kontrollera uppdateringar för följande teman:" watched_word_regexp_error: "Det reguljära uttrycket för '%{action}'-bevakade ord är ogiltigt. Kontrollera dina inställningar för bevakade ord eller inaktivera webbplatsinställningen 'Reguljära uttryck för bevakade ord'." site_settings: + allow_bulk_invite: "Tillåt massinbjudningar genom att ladda upp en CSV-fil" disabled: "inaktiverade" display_local_time_in_user_card: "Visa den lokala tiden baserat på en användares tidszon när deras användarkort öppnas." censored_words: "Ord som automatiskt ersätts med ■■■■" @@ -1381,7 +1382,8 @@ sv: min_personal_message_title_length: "Lägst tillåtna längd på rubrik för ett meddelande uttryckt i tecken" max_emojis_in_title: "Maximalt antal tillåtna emojier i ämnesrubriken" min_search_term_length: "Lägsta giltiga teckenlängd på sökterm" - search_tokenize_chinese_japanese_korean: "Framtvinga sökning för att koda kinesiska/japanska/koreanska även på webbplatser som inte är på dessa språk" + search_tokenize_chinese: "Tvinga sökning att tokenisera kinesiska även på icke-kinesiska webbplatser" + search_tokenize_japanese: "Tvinga sökning att tokenisera japanska även på icke-japanska webbplatser" search_prefer_recent_posts: "Om sökningar på ditt stora forum går långsamt, försök detta alternativ som är ett index av de senaste inläggen först." search_recent_posts_size: "Hur många nya inlägg som ska behållas i index" log_search_queries: "Logga sökfrågor utförda av användare" @@ -1425,7 +1427,7 @@ sv: show_pinned_excerpt_mobile: "Visa utdrag för fästa ämnen i mobilen." show_pinned_excerpt_desktop: "Visa utdrag för fästa ämnen på datorn." post_onebox_maxlength: "Maximal teckenlängd för ett Discourse-inlägg i onebox." - blocked_onebox_domains: "En lista över domäner som aldrig placeras i onebox." + blocked_onebox_domains: "En lista över domäner som aldrig kommer att oneboxas t.ex. wikipedia.org\n(Jokertecken * ? stöds inte)" allowed_inline_onebox_domains: "En lista över domäner som placeras i onebox i miniatyrform om de länkas utan en rubrik" enable_inline_onebox_on_all_domains: "Ignorera webbplatsinställningen inline_onebox_domain_allowlist och tillåt infogad onebox på alla domäner." force_custom_user_agent_hosts: "Värdar för vilka anpassad användaragent för onebox ska användas vid alla förfrågningar. (särskilt användbart för värdar som begränsar åtkomst av användaragent)." @@ -1513,7 +1515,7 @@ sv: allowed_iframes: "En lista med iframekällors domänprefix som Discourse säkert kan tillåta i inlägg" allowed_crawler_user_agents: "Användaragenter för sökrobotar som ska få åtkomst till webbplatsen. VARNING! INSTÄLLNING AV DETTA FÖRBJUDER ALLA SÖKMOTORER SOM INTE LISTAS HÄR!" blocked_crawler_user_agents: "Unikt fall av känsligt ord i användaragent-strängen som identifierar webbsökare som inte bör få åtkomst till webbplatsen. Gäller inte om vitlista har definierats." - slow_down_crawler_user_agents: "Användaragenter för sökrobotar som ska vara hastighetsbegränsade vilket konfigurerats i inställningen \"sakta ned sökrobots hastighet\". Varje värde måste vara minst 3 tecken långt." + slow_down_crawler_user_agents: 'Användaragenter för sökrobotar som ska vara hastighetsbegränsade vilket konfigurerats i inställningen "sakta ned sökrobots hastighet". Varje värde måste vara minst 3 tecken långt.' slow_down_crawler_rate: "Om slow_down_crawler_user_agents anges kommer denna hastighet att gälla för alla sökrobotar (fördröjning mellan förfrågningarna uttryckt i sekunder)" content_security_policy: "Aktivera säkerhetspolicy för innehåll" content_security_policy_report_only: "Aktivera enbart säkerhetspolicy för innehåll" @@ -2077,6 +2079,7 @@ sv: share_quote_visibility: "Avgör när du ska visa citatdelningsknappar: aldrig, endast till anonyma användare eller för alla användare. " create_revision_on_bulk_topic_moves: "Skapa ny version av första inlägg när ämnen flyttas till en ny kategori i bulk." allow_changing_staged_user_tracking: "Tillåt att aviseringsinställningar för en arrangerad användares kategori och tagg kan ändras av en administratörsanvändare." + use_email_for_username_and_name_suggestions: "Använd den första delen av e-postadresser för användarnamn och namnförslag. Observera att detta gör det lättare för allmänheten att gissa användares fullständiga e-postadresser (eftersom en stor del av människor delar gemensamma tjänster som `gmail.com`)." errors: invalid_css_color: "Ogiltig färg. Ange ett färgnamn eller ett hexvärde." invalid_email: "Felaktig e-postadress." @@ -2096,6 +2099,7 @@ sv: invalid_json: "Ogiltig JSON." invalid_reply_by_email_address: "Värdet måste innehålla '%{reply_key}' och vara annorlunda från e-postaviseringen." invalid_alternative_reply_by_email_addresses: "Alla värden måste innehålla '%{reply_key}' och vara annorlunda från e-postaviseringen." + invalid_domain_hostname: "Får inte innehålla * eller ? tecken." pop3_polling_host_is_empty: "Du måste ställa in en 'pop3 polling host' innan du aktiverar POP3-polling." pop3_polling_username_is_empty: "Du måste ställa in ett 'pop3 polling username' innan du aktiverar POP3-polling." pop3_polling_password_is_empty: "Du måste ställa in ett 'pop3 polling password' innan du aktiverar POP3-polling." @@ -2125,6 +2129,8 @@ sv: unicode_usernames_avatars: "De interna systemavatarerna stöder inte Unicode-användarnamn." list_value_count: "Listan måste innehålla exakt %{count} värden." google_oauth2_hd_groups: "Du måste först ange 'google oauth2 hd' innan du aktiverar denna inställning." + search_tokenize_chinese_enabled: "Du måste inaktivera 'search_tokenize_chinese' innan du aktiverar den här inställningen." + search_tokenize_japanese_enabled: "Du måste inaktivera 'search_tokenize_japanese' innan du aktiverar den här inställningen." placeholder: discourse_connect_provider_secrets: key: "www.exempel.se" diff --git a/config/locales/server.sw.yml b/config/locales/server.sw.yml index ff7ba61592..6997827af4 100644 --- a/config/locales/server.sw.yml +++ b/config/locales/server.sw.yml @@ -167,7 +167,6 @@ sw: no_attachments_allowed: "Samahani watumiaji wapya hawawezi kuambatanisha picha, sauti, video au dokument kwenye machapisho." no_links_allowed: "Samahani watumiaji wapya hawawezi kuweka viungo kwenye machapisho." links_require_trust: "Samahani, hauwezi kuweka viungo kwenye machapisho yako." - contains_blocked_word: "Chapisho lako lina neno lisiloruhusiwa: %{word}" spamming_host: "Samahani hauwezi kuchapisha kiungo kwenye computer hiyo." user_is_suspended: "Watumiaji waliosimamishwa hawaruhusiwi kuchapisha." topic_not_found: "Kuna tatizo limetokea. Labda hii mada ilifungwa au kufutwa ukiwa unaangalia?" @@ -794,7 +793,6 @@ sw: max_image_width: "Kiwango cha juu cha upana wa kijipicha za picha kwenye chapisho" max_image_height: "Kiwango cha juu cha urefu wa kijipicha za picha kwenye chapisho" add_rel_nofollow_to_user_content: 'Ongeza rel nofollow kwenye maandishi yote mtumiaji aliyotoa, isipokuwa viungo vya ndani (pamoja na kikoa baba). Ukibadilisha hii, lazima urebake machapisho yote na: "rake machapishi:rebake"' - blocked_onebox_domains: "Orodha ya anwani za mitandao ambazo hazijawekwa kwenye boxi." allowed_inline_onebox_domains: "Orodha za anwani za mtandao ambazo zitawekwa kwenye boxi kama zikiunganishwa bila kichwa cha habari" summary_score_threshold: "Alama ya chini ambayo mada inahitaji kabla ya kuwekwa ndani ya 'Fupisha Hii Mada' " summary_percent_filter: "Mtumiaji akibonyeza 'Fupisha Hii Mada', onyesha % machapisho ya juu" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 49846523f0..b2e98fe41e 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -335,8 +335,8 @@ tr_TR: too_many_links: one: "Üzgünüz, yeni kullanıcılar bir gönderiye sadece %{count} bağlantı ekleyebilirler." other: "Üzgünüz, yeni kullanıcılar bir gönderiye sadece %{count} bağlantı ekleyebilirler." - contains_blocked_word: "Gönderiniz izin verilmeyen bir sözcük içeriyor: %{word}" - contains_blocked_words: "Gönderiniz izin verilmeyen birden çok sözcük içeriyor: %{words}" + contains_blocked_word: "Üzgünüz, '%{word}' kelimesini kullanamazsınız; yasaklı." + contains_blocked_words: "Üzgünüm, bunu yayınlayamazsınız. Yasak kelimeler: %{words}." spamming_host: "Üzgünüz bu sunucuya bağlantı veremezsiniz." user_is_suspended: "Uzaklaştırılmış kullanıcılar gönderi yapamazlar." topic_not_found: "Bir şeyler ters gitti. Muhtemelen siz konuya bakarken bu konu kapatıldı ya da silindi." @@ -1025,7 +1025,7 @@ tr_TR: score: Skor description: "Yetkililerin raporlarına verdiği yanıtın oranına göre sıralanan kullanıcıların listesi (Kabul edilmemişten kabul edilmişe doğru)." moderators_activity: - title: "Moderatör Aktivitesi" + title: "Moderatör Etkinliği" labels: moderator: Moderatör flag_count: Raporlar yeniden görüntülendi @@ -1371,6 +1371,7 @@ tr_TR: unreachable_themes: "Aşağıdaki temalar için güncellemeleri kontrol edemedik:" watched_word_regexp_error: "İzlenen %{action} sözcükleri için düzenli ifade geçersiz. Lütfen İzlenen Sözcük ayarlarınızı kontrol edin veya 'izlenen sözcük düzenli ifadeleri' site ayarını devre dışı bırakın." site_settings: + allow_bulk_invite: "CSV dosyası yükleyerek toplu davete izin ver" disabled: "devredışı" display_local_time_in_user_card: "Kullanıcı kartı açıkken yerel saati kullanıcının saat dilimine göre görüntüle." censored_words: "otomatik olarak ■■■■ ile değiştirilecek sözcükler" @@ -1391,7 +1392,8 @@ tr_TR: min_personal_message_title_length: "İleti başlıkları için izin verilen en az karakter sayısı" max_emojis_in_title: "Konu başlığında izin verilen maksimum emoji" min_search_term_length: "Arama için girilecek sözcükte olması gereken en az karakter sayısı" - search_tokenize_chinese_japanese_korean: "CJK olmayan siteler dahil, -Çince/Japonca/Korece için aramayı bilgileri sıfırlamaya zorla" + search_tokenize_chinese: "Çince olmayan sitelerde bile aramayı Çince'ye dönüştürmeye zorla" + search_tokenize_japanese: "Japonca olmayan sitelerde bile aramayı Japonca'ya dönüştürmeye etmeye zorla" search_prefer_recent_posts: "Eğer büyük forumunuzda arama yavaş ise bu seçenek daha yeni gönderilerin dizine eklenmesini deneyecek" search_recent_posts_size: "Kaç tane son gönderi dizinde tutulacak" log_search_queries: "Kullanıcılar tarafından gerçekleştirilen günlük arama sorguları" @@ -1435,7 +1437,6 @@ tr_TR: show_pinned_excerpt_mobile: "Mobil görünümünde başa tutturulmuş konuların özetini göster." show_pinned_excerpt_desktop: "Masaüstü görünümünde başa tutturulmuş konuların özetini göster." post_onebox_maxlength: "Kutulanmış bir Discourse gönderisinin en fazla karakter uzunluğu" - blocked_onebox_domains: "Asla yayınlanmayacak alanların listesi." allowed_inline_onebox_domains: "Başlıksız bağlanırsa minyatür formda yayınlanacak alanların listesi" enable_inline_onebox_on_all_domains: "İnline_onebox_domain_whitelist site ayarını yok say ve tüm alan adlarında inline onebox'a izin ver." force_custom_user_agent_hosts: "Tüm isteklerde özel onebox kullanıcı aracısını kullanacak ana makineler. (Özellikle kullanıcı aracısıyla erişimi sınırlayan ana bilgisayarlar için kullanışlıdır)." @@ -1523,7 +1524,7 @@ tr_TR: allowed_iframes: "Gönderilerde güvenli bir şekilde izin verebilecek iframe src alan adı öneklerinin listesi" allowed_crawler_user_agents: "Siteye erişmesine izin verilmesi gereken ağ tarayıcılarının kullanıcı aracıları. UYARI! BU AYARLAR, BURADA LİSTEN OLMAYAN TÜM TARAYICILARI ÇIKARACAKTIR!" blocked_crawler_user_agents: "Siteye erişmesine izin verilmemesi gereken web tarayıcılarını tanımlayan kullanıcı aracısı dizesindeki benzersiz BÜYÜK/küçük harf duyarsız sözcük. İzin listesi tanımlanmışsa geçerli değildir." - slow_down_crawler_user_agents: "\"Tarayıcı hızını yavaşlat\" ayarında yapılandırıldığı gibi hızı sınırlandırılması gereken web tarayıcılarının kullanıcı aracıları. Her değer en az 3 karakter uzunluğunda olmalıdır." + slow_down_crawler_user_agents: '"Tarayıcı hızını yavaşlat" ayarında yapılandırıldığı gibi hızı sınırlandırılması gereken web tarayıcılarının kullanıcı aracıları. Her değer en az 3 karakter uzunluğunda olmalıdır.' slow_down_crawler_rate: "slow_down_crawler_user_agents belirtilirse bu oran tüm tarayıcılara uygulanır (istekler arasında saniye cinsinden gecikme sayısı)." content_security_policy: "İçerik-Güvenlik-Politikasını Etkinleştir" content_security_policy_report_only: "Yalnızca İçerik-Güvenlik-İlke-Raporunu Etkinleştir" @@ -2070,6 +2071,7 @@ tr_TR: share_quote_buttons: "Alıntı paylaşım widget'ında hangi öğelerin görüneceğini ve hangi sırayla görüneceklerini belirleyin." share_quote_visibility: "Alıntı paylaşma düğmelerinin ne zaman gösterileceğini belirleyin: asla, yalnızca anonim kullanıcılara veya tüm kullanıcılara. " create_revision_on_bulk_topic_moves: "Konular toplu olarak yeni bir kategoriye taşındığında ilk gönderiler için revizyon oluştur." + use_email_for_username_and_name_suggestions: "Kullanıcı adları ve ad önerileri için e-posta adreslerinin ilk kısmını kullanın. Ama unutmayın bu yaklaşım, insanların e-posta adreslerinin tamamını tahmin etmelerini kolaylaştırır (çünkü insanların büyük bir kısmı \"gmail.com\" gibi ortak hizmetleri paylaşır)." errors: invalid_css_color: "Geçersiz renk. Bir renk adı veya hex değer girin." invalid_email: "Geçersiz e-posta adresi." @@ -2089,6 +2091,7 @@ tr_TR: invalid_json: "Geçersiz JSON." invalid_reply_by_email_address: "Değer '%{reply_key}' içermeli ve bildiri e-postasından farklı olmalı." invalid_alternative_reply_by_email_addresses: "Tüm değerler '%{reply_key}' içermeli ve bildirim e-postasından farklı olmalıdır." + invalid_domain_hostname: "* ya da ? karakteri içermemeli." pop3_polling_host_is_empty: "POP3 sorgulamasını etkinleştirmeden önce bir 'pop3 polling host' ayarlamanız gerekir." pop3_polling_username_is_empty: "POP3 sorgulamasını etkinleştirmeden önce bir 'pop3 polling username' ayarlamanız gerekir." pop3_polling_password_is_empty: "POP3 sorgulamasını etkinleştirmeden önce bir 'pop3 polling password' ayarlamanız gerekir." @@ -2118,6 +2121,8 @@ tr_TR: unicode_usernames_avatars: "Dahili sistem avatarları Unicode kullanıcı adlarını desteklemez." list_value_count: "Liste tam olarak %{count} değerleri içermelidir." google_oauth2_hd_groups: "Bu ayarı etkinleştirmeden önce 'google oauth2 hd'yi ayarlamalısınız." + search_tokenize_chinese_enabled: "Bu ayarı etkinleştirmeden önce 'search_tokenize_chinese' özelliğini devre dışı bırakmalısınız." + search_tokenize_japanese_enabled: "Bu ayarı etkinleştirmeden önce 'search_tokenize_japanese' özelliğini devre dışı bırakmalısınız." placeholder: discourse_connect_provider_secrets: key: "www.örnek.com" @@ -2496,6 +2501,8 @@ tr_TR: ``` Ayrıntılar için lütfen [topluluk yönergelerimizi](%{base_url}/guidelines) inceleyin. + flags_agreed_and_post_deleted_for_responders: + title: "Yanıt, personel tarafından işaretlenen gönderiden kaldırıldı" usage_tips: text_body_template: | Yeni bir kullanıcı olarak başlamak için birkaç küçük ipucu, [bu blog gönderisini inceleyin](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 67bb0a44cc..d039add2eb 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -244,6 +244,8 @@ uk:

Якщо ви пам'ятаєте ваш пароль, ви можете Увійти в акаунт.

В іншому випадку Відновіть пароль.

+ not_found_template_link: | +

Це запрошення на %{site_name} більше не можна використати. Попросіть того, хто вас запросив, надіслати вам нове запрошення.

user_exists: "Немає необхідності надсилати запрошення %{email}, такий акаунт вже існує!" invite_exists: "Ви вже запросили %{email}." invalid_email: "%{email} не є дійсною адресою електронної пошти." @@ -374,8 +376,8 @@ uk: few: "Даруйте, нові користувачі можуть вставляти тільки %{count} посилань в допис." many: "Даруйте, нові користувачі можуть вставляти тільки %{count} посилань в допис." other: "Даруйте, нові користувачі можуть вставляти тільки %{count} посилань в допис." - contains_blocked_word: "Ваш пост містить слово, яке заборонено: %{word}" - contains_blocked_words: "Ваш пост містить кілька заборонених слів: %{words}" + contains_blocked_word: "Ви не можете опублікувати слово '%{word}'; це не дозволено." + contains_blocked_words: "Вибачте, ви не можете опублікувати це. Ці слова заборонені: %{words}." spamming_host: "Даруйте, Ви не можете вставити посилання на цей хост." user_is_suspended: "Заблокованим користувачам заборонено писати." topic_not_found: "Щось пішло не так. Можливо цей допис було закрито або видалено, під час того, як ви його переглядали?" @@ -1449,6 +1451,7 @@ uk: qtt_like: Отримані вподобання description: "Топ-10 користувачів, які мали лайки від широкого кола людей." dashboard: + group_email_credentials_warning: 'Виникла проблема з обліковими даними електронної пошти для групи %{group_full_name}. Жодні електронні листи не надсилатимуться зі скриньки групи, доки цю проблему не буде вирішено. %{error}' rails_env_warning: "Ваш сервер працює в режимі %{env}." host_names_warning: "Ваш файл config/database.yml використовує локальне ім’я хоста за замовчуванням. Поміняйте його на ім’я хоста вашого веб-сайту." sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.' @@ -1498,7 +1501,6 @@ uk: min_personal_message_title_length: "Мінімально допустима кількість символів в заголовку повідомлення в бесіді." max_emojis_in_title: "Максимально допустимі смайлики в заголовку теми" min_search_term_length: "Мінімальна дозволена довжина пошукової фрази у символах" - search_tokenize_chinese_japanese_korean: "Примусовий пошук для токенізації Китайського / Японського / Корейського навіть на сайтах, відмінних від CJK" search_prefer_recent_posts: "Якщо пошук на великому форумі виконується повільно, цей параметр спочатку намагається більше індексувати останні повідомлення" search_recent_posts_size: "Скільки останніх постів зберегти в індексі" log_search_queries: "Журнал пошукових запитів, виконаних користувачами" @@ -1542,7 +1544,6 @@ uk: show_pinned_excerpt_mobile: "Показати короткий виклад по закріплених темах в мобільному поданні." show_pinned_excerpt_desktop: "Показати витримку по закріплених темах в режимі робочого столу." post_onebox_maxlength: "Максимальна довжина повідомлення з форуму Discourse в режимі розумної вставки." - blocked_onebox_domains: "Список доменів, контент з яких ніколи не буде перетворено на розумну вставку." allowed_inline_onebox_domains: "Список доменів, контент з яких буде перетворено в мініатюрну вставку, якщо посилання вказане без заголовка" enable_inline_onebox_on_all_domains: "Ігнорувати налаштування сайту inline_onebox_domain_allowlist і дозволити вбудований контент для всіх доменів." force_custom_user_agent_hosts: "Хости, для яких використовується агент користувача розумних вставок для всіх запитів. (Особливо корисно для хостів, які обмежують доступ користувальницького агента)." @@ -1630,7 +1631,7 @@ uk: allowed_iframes: "Список префіксів домену iframe src, які discourse може безпечно дозволити в повідомленнях" allowed_crawler_user_agents: "Користувацькі агенти веб-сканерів, яким слід дозволити доступ до сайту. ПОПЕРЕДЖЕННЯ! ЦЕ ЗАБОРОНИТЬ ДОСТУП ВСІМ ВЕБ-СКАНЕРАМ, ЯКІ НЕ ВКАЗАНІ ТУТ!" blocked_crawler_user_agents: "Унікальне слово, не чутливе до регістру, у рядку агента користувача, що ідентифікує веб-сканери, яким не слід дозволяти доступ до сайту. Не застосовується, якщо визначено білий список." - slow_down_crawler_user_agents: "User agents, швидкість роботи яких повинна бути обмежена відповідно до налаштування \"slow down crawler rate\". Кожне значення має складатися не менше ніж з трьох символів." + slow_down_crawler_user_agents: 'User agents, швидкість роботи яких повинна бути обмежена відповідно до налаштування "slow down crawler rate". Кожне значення має складатися не менше ніж з трьох символів.' slow_down_crawler_rate: "Якщо вказано slow_down_crawler_user_agents, ця пауза застосовуватиметься до всіх сканерів (затримка між запитами кількість секунд)" content_security_policy: "Увімкнути політику безпеки вмісту" content_security_policy_report_only: "Увімкнути тільки звіт про політику безпеки вмісту" @@ -1705,6 +1706,7 @@ uk: discourse_connect_overrides_card_background: "Змінити фон профілю користувача на значення, яке використовується в DiscourseConnect." discourse_connect_not_approved_url: "Перенаправити несхвалені DiscourseConnect-акаунти на цю URL-адресу" discourse_connect_allows_all_return_paths: "Не обмежувати параметр return_paths для домена, надані DiscourseConnect (за замовчуванням шлях повернення має бути на поточному сайті)" + enable_local_logins: "Увімкнути локальні облікові записи на основі імені користувача та пароля. ПОПЕРЕДЖЕННЯ: якщо цей параметр вимкнено, ви не зможете увійти, якщо раніше не налаштували хоча б один альтернативний метод входу." enable_local_logins_via_email: "Дозволити користувачам запитувати посилання для входу в один клік та надсилати їм електронною поштою цього посилання." allow_new_registrations: "Дозволити реєстрацію нових користувачів. Вимкніть, щоб заборонити відвідувачам створювати нові облікові записи." enable_signup_cta: "Покажіть повідомлення анонімним користувачам, які повернулися, з пропозицією зареєструвати обліковий запис." @@ -1713,6 +1715,7 @@ uk: google_oauth2_client_secret: "Client secret для вашого Google додатка" google_oauth2_prompt: "Необов’язковий розділений пробілом список значень, який вказує, чи запитує сервер авторизацію користувача для повторної автентифікації та згоди. Див. https://developers.google.com/identity/protocols/OpenIDConnect#prompt щодо можливих значень." google_oauth2_hd: "Необов’язковий домен, розміщений у Google Apps Hosted, до якого вхід буде обмежений. Див https://developers.google.com/identity/protocols/OpenIDConnect#hd-param для більш докладної інформації." + google_oauth2_hd_groups: "(експериментально) Отримати групи Google користувачів у розміщеному домені під час автентифікації. Отримані групи Google можна використовувати для надання автоматичного членства в групі Discourse (див. налаштування групи)." enable_twitter_logins: "Увімкніть автентифікацію Twitter, потрібні twitter_consumer_key та twitter_consumer_secret. Див. Configuring Twitter login (and rich embeds) for Discourse." twitter_consumer_key: "Ключ користувача для аутентифікації в Twitter, зареєстрований за адресою https://developer.twitter.com/apps" twitter_consumer_secret: "Секретний номер для перевірки справжності Twitter, зареєстрований в https://developer.twitter.com/apps" @@ -1725,19 +1728,28 @@ uk: enable_discord_logins: "Дозволити користувачам вхід за допомогою Discord?" discord_client_id: 'Discord ID клієнта (потрібен такий? відвідайте the Discord developer portal)' discord_secret: "Discord секретний ключ" + discord_trusted_guilds: 'Дозволяйте вхід в систему через Discord лише членам цих гільдій. Використовуйте цифровий ідентифікатор гільдії. Для отримання додаткової інформації перегляньте інструкції тут. Залиште порожнім, щоб дозволити будь-яку гільдію.' enable_backups: "Дозволити адміністраторам створювати резервні копії форуму" + allow_restore: "Дозволити відновлення, яке може замінити ВСІ дані сайту! Залиште вимкненим, якщо ви не плануєте відновити сайт з резервної копії" maximum_backups: "Максимальна кількість резервних копій для зберігання на диску. Старіші резервні копії автоматично видаляються" automatic_backups_enabled: "Запускайте автоматичні резервні копії, як визначено в частоті резервного копіювання" backup_frequency: "Кількість днів між резервними копіями." s3_backup_bucket: "Адреса папки віддаленого сервера для резервних копій. УВАГА: Переконайтеся, що місце призначення захищено від сторонніх." s3_endpoint: "Кінцеву точку можна змінити для резервного копіювання на службу, сумісну з S3, наприклад DigitalOcean Spaces або Minio. УВАГА: Не використовуйте AWS S3." + s3_configure_tombstone_policy: "Увімкнути політику автоматичного видалення завантажень, відмічених як \"видалені\". ВАЖЛИВО. Якщо цей параметр вимкнено, місце не буде звільнятися після видалення завантажень." + s3_disable_cleanup: "Запобігайте видаленню старих резервних копій із S3, якщо резервних копій більше, ніж максимально дозволено." + enable_s3_inventory: "Генерувати звіти та перевіряти завантаження за допомогою інвентарю Amazon S3. ВАЖЛИВО: потрібні дійсні облікові дані S3 (як ідентифікатор ключа доступу, так і секретний ключ доступу)." backup_time_of_day: "Час доби UTC, коли має відбуватися резервне копіювання" backup_with_uploads: "Додайте завантаження до запланованих резервних копій. Якщо вимкнути це, ви можете створити резервну копію бази даних." + backup_location: "Місце, де зберігаються резервні копії. ВАЖЛИВО: S3 вимагає дійсних облікових даних S3, введених у налаштуваннях Файлів." backup_gzip_compression_level_for_uploads: "Рівень стиснення Gzip, який використовується для стискання завантажень." include_thumbnails_in_backups: "Включіть створені ескізи в резервні копії. Якщо вимкнути цю функцію, резервні копії зменшаться, але потрібно буде перебудувати (rebake) всі повідомлення після відновлення." 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)`" + top_topics_formula_first_post_likes_multiplier: "значення коефіцієнта множення переглядів журналу (n) у формулі топ-тем: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" + top_topics_formula_least_likes_per_post_multiplier: "значення коефіцієнта найменшої кількості лайків на допис (n) у формулі топ-тем: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`" enable_safe_mode: "Дозволити користувачам входити в безпечний режим для налагодження плагінів." rate_limit_create_topic: "Користувачі не можуть створювати більше однієї нової теми за вказаний час, в секундах." rate_limit_create_post: "Користувачі не можуть писати більше одного нового допису за вказаний час, в секундах." @@ -1748,12 +1760,18 @@ uk: max_bookmarks_per_day: "Максимальна кількість створених закладок користувачем за день." max_edits_per_day: "Максимальна кількість редагувань користувачем за день." max_topics_per_day: "Максимальна кількість тем, які користувач може створити за один день." + max_personal_messages_per_day: "Максимальна кількість нових особистих повідомлень, які користувач може створювати за день." max_invites_per_day: "Максимальна кількість запрошень, яке може відправити користувач за один день." max_topic_invitations_per_day: "Максимальна кількість запрошень в тему, яке може відправити користувач протягом дня." max_logins_per_ip_per_hour: "Максимальна кількість входів в систему на IP-адресу за годину" max_logins_per_ip_per_minute: "Максимальна кількість входів в систему на IP-адресу за годину" + max_post_deletions_per_minute: "Максимальна кількість повідомлень, які користувач може видалити за хвилину. Встановіть значення 0, щоб відключити видалення повідомлень." + max_post_deletions_per_day: "Максимальна кількість повідомлень, які користувач може видалити за день. Встановіть значення 0, щоб відключити видалення повідомлень." + invite_link_max_redemptions_limit: "Максимальна кількість використання активацій із посилань запрошень." + invite_link_max_redemptions_limit_users: "Максимальна кількість використання активацій посилань із запрошень, створених користувачами." alert_admins_if_errors_per_minute: "Кількість помилок на хвилину для попередження адміністратора. Значення 0 відключає цю опцію. УВАГА: потрібне перезавантаження." alert_admins_if_errors_per_hour: "Кількість помилок на годину для попередження адміністратора. Значення 0 відключає цю опцію. УВАГА: потрібне перезавантаження." + categories_topics: "Кількість тем для показу на сторінці /categories. Якщо встановлено значення 0, тоді система автоматично намагатиметься знайти значення, щоб обидва стовпці (категорії та теми) були симетрично однакової довжини." suggested_topics: "Кількість рекомендованих тем, що відображаються внизу поточної тем." limit_suggested_to_category: "Показувати тільки теми з поточного розділу в списку рекомендованих тем." suggested_topics_max_days_old: "Пропоновані теми не повинні бути старше n днів." @@ -1905,8 +1923,10 @@ uk: reviewable_default_visibility: "Не показуйте елементи для перегляду, якщо вони не відповідають цьому пріоритету" high_trust_flaggers_auto_hide_posts: "Нові повідомлення користувачів автоматично приховуватимуться як спам, після позначення їх користувачем з рівнем довіри 3 та вище" cooldown_hours_until_reflag: "Скільки часу доведеться чекати користувачам, поки вони зможуть повторно додати позначку на повідомлення" + slow_mode_prevents_editing: "Забороняти при «Повільному режимі» редагування після закінчення пільгового періоду редагування editing_grace_period?" 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: "Використовуйте HTML замість тексту для вхідної електронної пошти." strip_incoming_email_lines: "Видаляти проміжні пробіли та пробіли з кожного рядка вхідних електронних листів." disable_emails: "Захистити Discourse від надсилання будь-яких електронних листів. Виберіть 'так', щоб вимкнути електронні листи для всіх користувачів. Виберіть 'не персонал', щоб вимкнути електронні листи лише для користувачів, які не є персоналом." @@ -1923,10 +1943,13 @@ uk: auto_generated_allowlist: "Список електронних адрес, які не перевірятимуться на автоматично створений вміст. Приклад: foo@bar.com|discourse@bar.com" block_auto_generated_emails: "Блокувати вхідні електронні листи, ідентифіковані як автоматично створені." ignore_by_title: "Ігнорувати вхідні електронні листи на основі їх заголовку." + mailgun_api_key: "Ключ API Mailgun Secret, який використовується для перевірки повідомлень webhook." soft_bounce_score: "Кількість відмов додається користувачеві, коли трапляється тимчасова відмова." hard_bounce_score: "Кількість відмов додається користувачеві, коли відбувається постійна відмова." bounce_score_threshold: "Максимальна кількість відмов, перш ніж ми перестанемо надсилати електронну пошту користувачеві." reset_bounce_score_after_days: "Автоматично скинути показник відмов через X днів." + blocked_attachment_content_types: "Список ключових слів, які використовуються для блокування вкладень залежно від типу вмісту." + blocked_attachment_filenames: "Список ключових слів, які використовуються для блокування вкладень на основі імені файлу." forwarded_emails_behaviour: "Як ставитись до пересланого електронного листа до Discourse" always_show_trimmed_content: "Завжди показуйте обрізану частину вхідних електронних листів. ПОПЕРЕДЖЕННЯ: можуть бути показані Email адреси." trim_incoming_emails: "Обрізати частину вхідних листів, якщо вони не актуальні." @@ -1961,10 +1984,12 @@ uk: imap_batch_import_email: "Мінімальна кількість нових листів, які запускають режим імпорту (вимикає поштові сповіщення)." email_prefix: "[Label], який використовується в темі електронних листів. За замовчуванням встановлено значення 'title', якщо воно явно не встановлено." email_site_title: "Назва сайту використовується як відправник електронних листів з сайту. Типове значення «title», якщо не встановлено. Якщо заголовок містить символи, які заборонено в рядках відправника електронної пошти, то буде використано цей параметр." + find_related_post_with_key: "Використовувати лише «ключ відповіді», щоб знайти відповідь на публікацію. УВАГА: відключення цього параметра дозволяє видавати себе за іншого користувача на основі адреси електронної пошти." minimum_topics_similar: "Скільки тем має існувати, перш ніж подібні теми будуть показані під час створення нових тем." relative_date_duration: "Кількість днів після публікації, коли дати публікації будуть відображені як відносні (7d), а не абсолютні (20 лютого)." delete_user_max_post_age: "Не дозволяти видаляти користувачів, чия перша публікація старша (x) днів." delete_all_posts_max: "Максимальне число дописів, які можна видалити за один раз кнопкою \"Видалити всі дописи\". Якщо користувач має більше дописів, ніж це число, їх не можна буде видалити за один раз, і користувача також." + delete_user_self_max_post_count: "Максимальна кількість повідомлень, які користувач може мати, при видаленні власного облікового запису. Встановіть значення -1, щоб вимкнути можливість видалення власного облікового запису." username_change_period: "Максимальна кількість днів після реєстрації, щоб облікові записи могли змінити своє ім'я користувача (0, щоб заборонити зміну імені користувача)." email_editable: "Дозволити користувачам змінювати свою електронну скриньку після реєстрації." logout_redirect: "URL для переадресації браузера після виходу (наприклад: https://example.com/logout)" @@ -1988,10 +2013,13 @@ uk: staff_user_custom_fields: "Список користувальницьких полів, які можна отримати для співробітників з API." enable_user_directory: "Надайте розділ користувачів для перегляду" enable_group_directory: "Надайте розділ груп для перегляду" + enable_category_group_moderation: "Дозволити групам модерувати вміст у певних категоріях" + group_in_subject: "Встановити змінну %%{optional_pm} в темі електронної пошти з ім'ям першої групи в профілі, докладніше: Налаштування формату теми для стандартних електронних листів" allow_anonymous_posting: "Дозволити користувачам переходити в анонімний режим" anonymous_posting_min_trust_level: "Мінімальний рівень довіри, необхідний для дозволу анонімних публікацій" anonymous_account_duration_minutes: "Для захисту анонімності створити новий анонімний обліковий запис кожні N хвилин для кожного користувача. Наприклад: якщо встановлено 600, як тільки минуло 600 хвилин з останнього повідомлення – користувач перейде на новий анонімний запис." hide_user_profiles_from_public: "Вимкнути картки, профілі та розділ користувачів для анонімних користувачів." + allow_users_to_hide_profile: "Дозволити користувачам приховувати свій профіль та присутність на форумі" allow_featured_topic_on_user_profiles: "Дозволити користувачам розміщувати посилання на тему на своїй картці та профілі користувача." show_inactive_accounts: "Дозволити авторизованим користувачам, переглядати профілі неактивних облікових записів." hide_suspension_reasons: "Не показуйте причини призупинення публічно в профілях користувачів." @@ -1999,7 +2027,9 @@ uk: ignored_users_count_message_threshold: "Повідомте модераторів, якщо певний користувач ігнорується більше ніж N користувачами." ignored_users_message_gap_days: "Як довго чекати, перш ніж повторно повідомити модераторів про користувача, якого ігнорувало багато інших." clean_up_inactive_users_after_days: "Кількість днів до видалення неактивного користувача (рівень довіри 0 без жодних публікацій). Щоб вимкнути очищення, встановіть значення 0." + clean_up_unused_staged_users_after_days: "Кількість днів до видалення невикористаного інсценованого користувача (без будь-яких повідомлень). Для відключення очищення встановіть значення 0." user_selected_primary_groups: "Дозволити користувачам створювати власну групу" + max_notifications_per_user: "Максимальна кількість сповіщень на користувача, якщо це число буде перевищено, старі сповіщення будуть видалені щотижня. Встановіть 0, щоб вимкнути" allowed_user_website_domains: "Веб-сайт користувача треба підтверджувати для цих доменів. Список з розмежуванням." allow_profile_backgrounds: "Дозволити користувачам завантажувати фони профілю." sequential_replies_threshold: "Кількість публікацій, які користувач повинен робити підряд по темі, перш ніж нагадувати про занадто багато послідовних відповідей." @@ -2170,6 +2200,7 @@ uk: placeholder: discourse_connect_provider_secrets: key: "www.example.com" + value: "Ключ DiscourseConnect" search: extreme_load_error: "Сайт перебуває під надзвичайним навантаженням, пошук вимкнено, повторіть спробу пізніше" within_post: "# %{post_number} від %{username}" @@ -2178,6 +2209,8 @@ uk: topic: "Результати" user: "Користувачі" results_page: "Результати пошуку для '%{term}'" + audio: "[audio]" + video: "[video]" discourse_connect: login_error: "Помилка входу" not_found: "Ваш обліковий запис не вдалося знайти. Зверніться до адміністратора сайту." @@ -2185,10 +2218,12 @@ uk: unknown_error: "Виникла проблема з вашим обліковим записом. Зверніться до адміністратора сайту." timeout_expired: "Час входу в обліковий запис вийшов. Спробуйте знову увійти." no_email: "Не вказано електронну адресу e-mail. Зверніться до адміністратора сайту." + blank_id_error: "Параметр \"external_id\" є обов'язковим, але поки не встановлений" email_error: "Не вдалося зареєструвати обліковий запис з електронною поштою %{email}. Зверніться до адміністратора сайту." original_poster: "Перший дописувач" most_recent_poster: "Автор останнього допису" frequent_poster: "Частий дописувач" + poster_description_joiner: ", " redirected_to_top_reasons: new_user: "Ласкаво просимо до нашої спільноти! Це найпопулярніші останні теми." not_seen_in_a_month: "З поверненням! Ми вас давно не бачили. Це найпопулярніші теми, відколи ви не відвідували форум." @@ -2201,6 +2236,7 @@ uk: errors: different_topics: "Повідомлення, що належать до різних тем, неможливо об’єднати." different_users: "Повідомлення, що належать різним користувачам, неможливо об’єднати." + max_post_length: "Публікації не можна об’єднати, оскільки сумарна довжина публікації перевищує дозволену." move_posts: new_topic_moderator_post: one: "Допис був розділений на нову тему: %{topic_link}" @@ -2227,6 +2263,7 @@ uk: publish_page: slug_errors: blank: "не може бути порожнім" + unavailable: "недоступний" invalid: "містить неприпустимі символи" topic_statuses: autoclosed_message_max_posts: @@ -2319,8 +2356,10 @@ uk: not_activated: "Ви ще не можете увійти. Ми надіслали Вам активаційного листа. Будь ласка, дотримуйтеся інструкцій у листі, щоб активувати свій обліковий запис." not_allowed_from_ip_address: "Ви не можете увійти як %{username} з цієї IP-адреси." admin_not_allowed_from_ip_address: "Ви не можете увійти як адміністратор з цієї IP-адреси." + reset_not_allowed_from_ip_address: "Ви не можете надіслати запит на скидання пароля з цієї IP-адреси." suspended: "Ви не можете входити до %{date}." suspended_with_reason: "Обліковий запис призупинено до %{date}: %{reason}" + suspended_with_reason_forever: "Обліковий запис призупинено: %{reason}" errors: "%{errors}" not_available: "Not available. Try %{suggestion}?" something_already_taken: "Щось пішло не так, можливо, ім'я користувача або електронна скринька вже зареєстровані. Спробуйте посилання \"Я забув пароль\"." @@ -2329,12 +2368,14 @@ uk: csrf_detected: "Час авторизації закінчено, або ви переключили браузери. Будь ласка спробуйте ще раз." request_error: "Під час запуску авторизації сталася помилка. Будь ласка спробуйте ще раз." invalid_iat: "Неможливо перевірити маркер авторизації через відмінності в часі сервера. Будь ласка спробуйте ще раз." + omniauth_error_unknown: "Сталася помилка під час обробки вашого входу. Спробуйте ще раз." omniauth_confirm_title: "Увійдіть, використовуючи %{provider}" omniauth_confirm_button: "Продовжити" authenticator_error_no_valid_email: "Не допускаються жодні адреси електронної пошти, пов'язані з %{account}. Можливо, вам доведеться налаштувати свій обліковий запис за допомогою іншої адреси електронної пошти." new_registrations_disabled: "Наразі реєстрація нових облікових записів заборонена." password_too_long: "Паролі обмежені 200 символами." email_too_long: "Ви вказали електронну пошту Email занадто довгу. Адреса поштової скриньки повинна містити не більше 254 символів, а доменні імена не більше 253 символів." + wrong_invite_code: "Введений вами код запрошення був неправильним." reserved_username: "Це ім’я користувача заборонено." missing_user_field: "Ви не заповнили всі поля користувача" auth_complete: "Автентифікація завершена." @@ -2342,6 +2383,7 @@ uk: second_factor_title: "Двофакторна автентифікація" second_factor_description: "Введіть необхідний код автентифікації у своєму додатку:" second_factor_backup_description: "Введіть один із резервних кодів:" + second_factor_backup_title: "Двофакторний резервний код" invalid_second_factor_code: "Недійсний код аутентифікації Кожен код може бути використаний лише один раз." invalid_security_key: "Недійсний ключ безпеки." missing_second_factor_name: "Будь ласка, вкажіть ім’я." @@ -2352,6 +2394,18 @@ uk: admin: email: sent_test: "надіслано!" + user: + merge_user: + updating_username: "Оновлення імені користувача..." + changing_post_ownership: "Зміна власника публікації..." + merging_given_daily_likes: "Об’єднання отриманих щоденних симпатій..." + merging_post_timings: "Об’єднання часу створення публікацій..." + merging_user_visits: "Об’єднання статистики відвідувань користувачів..." + updating_site_settings: "Оновлення налаштувань сайту..." + updating_user_stats: "Оновлення статистики користувача..." + merging_user_attributes: "Об'єднання атрибутів користувача..." + updating_user_ids: "Оновлення ідентифікаторів користувачів..." + deleting_source_user: "Видалення вихідного користувача..." user: deactivated: "Відключено через багато електронних листів, відхилених на '%{email}'." deactivated_by_staff: "Деактивовано персоналом" @@ -2362,6 +2416,7 @@ uk: other: "Автоматично деактивується через %{count} днів бездіяльності" activated_by_staff: "Активований персоналом" new_user_typed_too_fast: "Новий користувач набирає текст занадто швидко" + content_matches_auto_silence_regex: "Вміст відповідає автоматичному блокуванню за regex (регулярним виразом)" content_matches_auto_block_regex: "Вміст відповідає автоматичному блоку regex (регулярний вираз)" username: short: "має бути найменше %{min} символів" @@ -2379,6 +2434,7 @@ uk: not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "не допускається." revoked: "Не надсилатимуть електронні листи на '%{email}' до %{date}." + does_not_exist: "Н/Д" ip_address: blocked: "Нові реєстрації заборонені з вашої IP-адреси." max_new_accounts_per_registration_ip: "Не дозволяються нові реєстрації з вашої IP-адреси (досягнуто максимального обмеження). Зверніться до співробітника." @@ -2391,15 +2447,21 @@ uk: same_ip_address: "Ідентична IP-адреса (%{ip_address}), як в інших користувачів" inactive_user: "Неактивний користувач" reviewable_reject_auto: "Автоматична обробка черги перегляду" + reviewable_reject: "Користувача після перегляду відхилено" email_in_spam_header: "Перше повідомлення користувача було позначено як спам" + already_silenced: "Користувач вже був заблокований співробітником %{staff} %{time_ago}." + already_suspended: "Користувач уже був призупинений %{staff} %{time_ago}." unsubscribe_mailer: title: "Відписатися від розсилки" subject_template: "Підтвердіть, що більше не хочете отримувати оновлення електронною поштою від %{site_title}" invite_mailer: + title: "Запросити Mailer" subject_template: "%{inviter_name} запросив вас приєднаися до '%{topic_title}' на %{site_domain_name}" custom_invite_mailer: + title: "Спеціальне запрошення на форум" subject_template: "%{inviter_name} запросив вас приєднаися до '%{topic_title}' на %{site_domain_name}" invite_forum_mailer: + title: "Спеціальне запрошення на форум" subject_template: "%{inviter_name} запрошує вас приднатися до %{site_domain_name}" custom_invite_forum_mailer: title: "Спеціальне запрошення на форум" @@ -2453,12 +2515,16 @@ uk: subject_template: "Повідомлення приховано через скарги спільноти, персонал повідомлено" queued_by_staff: title: "Допис потребує схвалення" + subject_template: "Повідомлення приховане персоналом, очікує схвалення" flags_disagreed: title: "Позначене повідомлення відновлено персоналом" subject_template: "Позначене повідомлення відновлено персоналом" flags_agreed_and_post_deleted: title: "Позначене повідомлення видалено персоналом" subject_template: "Позначене повідомлення видалено персоналом" + flags_agreed_and_post_deleted_for_responders: + title: "Відповідь вилучено з позначеного повідомлення персоналом" + subject_template: "Відповідь вилучено з позначеного повідомлення персоналом" welcome_user: title: "Ласкаво просимо, користувач" subject_template: "Ласкаво просимо до сайта %{site_name}!" @@ -2467,6 +2533,7 @@ uk: subject_template: "Дякуємо, що провели з нами час" welcome_staff: title: "Ласкаво просимо, персонал" + subject_template: "Вітаємо, ви отримали статус %{role}!" welcome_invite: title: "Ласкаво просимо" subject_template: "Ласкаво просимо до сайта %{site_name}!" @@ -2479,12 +2546,30 @@ uk: backup_failed: title: "Помилка резервного копіювання" subject_template: "Резервне копіювання не вдалося" + text_body_template: | + Не вдалося створити резервну копію. + + Ось журнал: + + %{logs} restore_succeeded: title: "Відновлення успішне" subject_template: "Відновлення даних успішно завершено" + text_body_template: | + Відновлення пройшло успішно. + + Ось журнал: + + %{logs} restore_failed: title: "Помилка відновлення" subject_template: "Відновлення даних не вдалося" + text_body_template: | + Помилка відновлення. + + Ось журнал: + + %{logs} bulk_invite_succeeded: title: "Запрошення вдалося успішно" subject_template: "Масове запрошення користувачів оброблено успішно" @@ -2505,6 +2590,8 @@ uk: csv_export_failed: title: "Не вдалося експортувати CSV" subject_template: "Експорт даних не здійснено" + email_reject_user_not_found: + title: "Електронний лист у випадку, коли користувача не знайдено" email_reject_screened_email: subject_template: "[%{email_prefix}] Проблема з електронною поштою — заблокована електронна пошта" email_reject_not_allowed_email: diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index 5c0859f176..3e7cbac370 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -256,8 +256,6 @@ ur: too_many_links: one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک لِنک ڈال سکتے ہیں۔" other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} لِنکس ڈال سکتے ہیں۔" - contains_blocked_word: "آپ کی پوسٹ میں ایک ایسا لفظ شامل ہے جس کی اجازت نہیں ہے: %{word}" - contains_blocked_words: "آپ کی پوسٹ میں متعدد الفاظ شامل ہیں جن کی اجازت نہیں ہے: %{words}" spamming_host: "معذرت، آپ اس ہَوسٹ کیلئے لِنک پوسٹ نہیں کر سکتے۔" user_is_suspended: "معطل صارفین کو پوسٹ کرنے کی اجازت نہیں ہے۔" topic_not_found: "کچھ غلط ہو گیا ہے۔ شاید جس دوران آپ اِسے دیکھ رہے تھے، یہ ٹاپک بند یا حذف کر دیا گیا؟" @@ -1156,7 +1154,6 @@ ur: min_personal_message_title_length: "حروف میں ایک پیغام کیلئے عنوان کی کم از کم لمبائی" max_emojis_in_title: "ٹاپک عنوان میں اِیمَوجیوں کی زیادہ سے زیادہ تعداد" min_search_term_length: "حروف میں درست سرچ ٹَرم کی کم از کم لمبائی" - search_tokenize_chinese_japanese_korean: "CJK سائٹس کے علاوہ بھی سرچ کو چینی/ جاپانی/کورین کو ٹوکنائیز کرنے پر مجبور کریں" search_prefer_recent_posts: "اگر آپ کے بڑے فورم پر سرچ سست ہے، تو یہ آپشن پہلے حالیہ پوسٹس کے ایک اِنڈَیکس پر کوشش کرتا ہے" search_recent_posts_size: "اِنڈَیکس میں کتنی حالیہ پوسٹس رکھی جائیں" log_search_queries: "صارفین کی طرف سے سرچ قُوَیریز کو لاگ کریں" @@ -1194,7 +1191,6 @@ ur: show_pinned_excerpt_mobile: "موبائل وِیو میں پِن ہوے ٹاپکس پر اقتباس دکھائیں۔" show_pinned_excerpt_desktop: "ڈیسک ٹاپ وِیو میں پِن ہوے ٹاپکس پر اقتباس دکھائیں۔" post_onebox_maxlength: "ایک وَن باکسڈ ڈِسکورس پوسٹ کے حروف کی زیادہ سے زیادہ لمبائی۔" - blocked_onebox_domains: "ڈومینز کی ایک فہرست جو کبھی بھی وَن باکسڈ نہیں کیے جائیں گے۔" allowed_inline_onebox_domains: "ڈومینز کی ایک فہرست جو چھوٹے فارم میں وَن باکسڈ کیے جائیں گے اگر وہ عنوان کے بغیر لنک کیے جائیں" max_oneboxes_per_post: "ایک پوسٹ میں وَن باکس کی زیادہ سے زیادہ تعداد۔" logo: "آپ کی سائٹ کے سب سے اوپر بائیں پر لوگو کی تصویر۔ 120 کی اونچائی اور 3:1 سے زائد اَیسپَیکٹ رَیشو والی وسیع مستطیل تصویر کا استعمال کریں۔ اگر خالی چھوڑ دیا گیا ہو تو سائٹ کا عنوان دکھایا جائے گا۔" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index dfa31979ee..566b91ed13 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -887,7 +887,6 @@ vi: max_topic_title_length: "Số kí tự tối đa trong tiêu đề chủ đề." min_personal_message_title_length: "Chiều dài tối thiểu cho phép theo số kí tự của một thông điệp" min_search_term_length: "Số kí tự tối thiểu trong từ khóa tìm kiếm." - search_tokenize_chinese_japanese_korean: "Bắt buộc tìm kiếm tách từ Chinese/Japanese/Korean ngay cả trên các site không phải là CJK" search_recent_posts_size: "Có bao nhiêu bài đăng gần đây để giữ trong chỉ mục" log_search_queries: "Ghi nhật ký các truy vấn tìm kiếm do người dùng thực hiện" search_query_log_max_size: "Số lượng truy vấn tìm kiếm tối đa cần lưu giữ" @@ -912,7 +911,6 @@ vi: add_rel_nofollow_to_user_content: 'Thêm rel=''nofollow'' cho tất cả các nội dung mà người dùng gửi, ngoại trừ các liên kết nội bộ (của tên miền chính). Nếu thay đổi, bạn phải thực hiện lại cho tất cả các bài viết với: "rake posts:rebake"' post_excerpt_maxlength: "Chiều dài tối đa của đoạn trích / tóm tắt chủ đề." post_onebox_maxlength: "Số ký tự tối đa của một bài onebox Discourse." - blocked_onebox_domains: "Danh sách các tên miền sẽ không bao giờ được oneboxed." notification_email: "Địa chỉ email 'Từ:' được dùng để gửi các email thiết yếu của hệ thống. Các tên miền quy định ở đây phải có SPF, DKIM và bản ghi PTR phải được thiết lập chính xác cho email đến." email_custom_headers: "Danh sách xác định email header tùy chỉnh" force_https: "Buộc trang web của bạn chỉ sử dụng HTTPS. CẢNH BÁO: KHÔNG kích hoạt tính năng này cho đến khi bạn xác minh HTTPS được thiết lập đầy đủ và hoạt động hoàn toàn ở mọi nơi! Bạn đã kiểm tra CDN của mình, tất cả thông tin đăng nhập xã hội và bất kỳ biểu trưng / phụ thuộc bên ngoài nào để đảm bảo tất cả chúng đều tương thích với HTTPS?" diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 9a3aa3368c..27cd2d4b9e 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -316,8 +316,6 @@ zh_CN: links_require_trust: "抱歉,您的帖子中不能包含链接。" too_many_links: other: "抱歉,新用户在一个帖子中仅能发布 %{count} 个链接。" - contains_blocked_word: "您的帖子中包含一个不被允许的字词:%{word}" - contains_blocked_words: "您的帖子中包含多个不被允许的字词:%{words}" spamming_host: "抱歉,您不能发布指向该主机的链接。" user_is_suspended: "被封禁的用户不允许发帖。" topic_not_found: "出了点问题。或许此话题在您查看时已被关闭或删除。" @@ -1344,7 +1342,6 @@ zh_CN: min_personal_message_title_length: "消息允许的最小标题长度(以字符为单位)" max_emojis_in_title: "话题标题中允许的最大表情数量" min_search_term_length: "最小有效搜索词长度(以字符为单位)" - search_tokenize_chinese_japanese_korean: "即使在非 CJK 站点上也强制搜索将中文/日语/韩语词例化" search_prefer_recent_posts: "如果搜索大型论坛的速度很慢,此选项会首先尝试索引最近的帖子" search_recent_posts_size: "在索引中保留多少个最近的帖子" log_search_queries: "记录用户执行的搜索查询" @@ -1388,7 +1385,6 @@ zh_CN: show_pinned_excerpt_mobile: "在移动视图中显示置顶话题的摘录。" show_pinned_excerpt_desktop: "在桌面视图中显示置顶话题的摘录。" post_onebox_maxlength: "Onebox Discourse 帖子的最大长度(以字符为单位)。" - blocked_onebox_domains: "永远不会进行 Onebox 处理的网域列表。" allowed_inline_onebox_domains: "如果在没有标题的情况下链接,将以微型形式进行 Onebox 处理的网域列表" enable_inline_onebox_on_all_domains: "忽略 inline_onebox_domain_allowlist 站点设置并在所有网域上允许内联 Onebox。" force_custom_user_agent_hosts: "对所有请求使用自定义 Onebox 用户代理的主机。(对于限制用户代理访问的主机特别有用)。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index f7280cc5f3..bc01e5068d 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -233,7 +233,6 @@ zh_TW: links_require_trust: "抱歉,您不能在貼文中插入連結" too_many_links: other: "抱歉,新使用者只能在貼文中放置 %{count} 個連結。" - contains_blocked_word: "您的貼文包含被禁用的文字:%{word}" spamming_host: "抱歉,你不能張貼該網站之連結。" user_is_suspended: "被停權的使用者無法張貼貼文。" topic_not_found: "出現問題。也許這個話題被關閉或刪除。" @@ -1091,7 +1090,6 @@ zh_TW: min_personal_message_title_length: "訊息標題的字元數下限" max_emojis_in_title: "話題標題可含有的表情符號上限" min_search_term_length: "搜尋條件允許的最小文字數" - search_tokenize_chinese_japanese_korean: "在非中/日/韓語站點強制切割中/日/韓語搜索分詞" search_prefer_recent_posts: "如果搜索大型論壇較慢,這個選項將優先嘗試最新的貼文" search_recent_posts_size: "索引多少最新的主題" log_search_queries: "登入使用者進行搜尋查詢" @@ -1128,7 +1126,6 @@ zh_TW: show_pinned_excerpt_mobile: "在行動版置頂主題顯示摘要。" show_pinned_excerpt_desktop: "在桌面版置頂主題顯示摘要。" post_onebox_maxlength: "展開連結預覽後的 Discourse 貼文最大字數限制。" - blocked_onebox_domains: "不會展開成連結預覽的網域列表。" allowed_inline_onebox_domains: "如果連結未包含 title,會被展開成連結預覽的網域白名單。" max_oneboxes_per_post: "貼文中允許的連結預覽數量上限。" logo: "網站左上角的Logo圖片,圖片高為120、寬高比大於3:1的寬矩形比例。 如果沒有設定 Logo,將顯示網站標題。" diff --git a/config/routes.rb b/config/routes.rb index 32e5a05c1d..641a905765 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -809,6 +809,7 @@ Discourse::Application.routes.draw do # Topic routes get "t/id_for/:slug" => "topics#id_for_slug" + get "t/external_id/:external_id" => "topics#show_by_external_id", format: :json, constrains: { external_id: /\A[\w-]+\z/ } 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+/ } diff --git a/config/site_settings.yml b/config/site_settings.yml index 560ad131b8..62aa97487b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -584,7 +584,7 @@ users: client: true default: true invite_expiry_days: - default: 30 + default: 90 client: true max: 36500 invites_per_page: @@ -686,7 +686,6 @@ users: default: 2000 hidden: true use_email_for_username_and_name_suggestions: - hidden: true default: false groups: @@ -1701,7 +1700,7 @@ onebox: zh_TW: 200 blocked_onebox_domains: default: "" - type: list + type: host_list list_type: compact max_oneboxes_per_post: default: 50 @@ -2035,7 +2034,12 @@ search: zh_TW: 1 ko: 1 ja: 1 - search_tokenize_chinese_japanese_korean: false + search_tokenize_chinese: + default: false + validator: "SearchTokenizeChineseValidator" + search_tokenize_japanese: + default: false + validator: "SearchTokenizeJapaneseValidator" search_prefer_recent_posts: false search_recent_posts_size: default: 1000000 @@ -2348,6 +2352,10 @@ uncategorized: default: false hidden: true + allow_bulk_invite: + default: true + client: true + max_bulk_invites: default: 50000 hidden: true @@ -2580,3 +2588,6 @@ dashboard: - flags - user_to_user_private_messages_with_replies - signups + verbose_user_stat_count_logging: + hidden: true + default: false diff --git a/db/migrate/20220118065658_add_constraints_to_user_stat.rb b/db/migrate/20220118065658_add_constraints_to_user_stat.rb new file mode 100644 index 0000000000..a167608cef --- /dev/null +++ b/db/migrate/20220118065658_add_constraints_to_user_stat.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class AddConstraintsToUserStat < ActiveRecord::Migration[6.1] + def up + execute(<<~SQL) + UPDATE user_stats + SET post_count = 0 + WHERE post_count < 0 + SQL + + execute(<<~SQL) + UPDATE user_stats + SET topic_count = 0 + WHERE topic_count < 0 + SQL + + execute "ALTER TABLE user_stats ADD CONSTRAINT topic_count_positive CHECK (topic_count >= 0)" + execute "ALTER TABLE user_stats ADD CONSTRAINT post_count_positive CHECK (post_count >= 0)" + end + + def down + execute "ALTER TABLE user_stats DROP CONSTRAINT topic_count_positive" + execute "ALTER TABLE user_stats DROP CONSTRAINT post_count_positive" + end +end diff --git a/db/migrate/20220124003259_add_email_from_alias_to_groups.rb b/db/migrate/20220124003259_add_email_from_alias_to_groups.rb new file mode 100644 index 0000000000..260dd01bde --- /dev/null +++ b/db/migrate/20220124003259_add_email_from_alias_to_groups.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddEmailFromAliasToGroups < ActiveRecord::Migration[6.1] + def change + add_column :groups, :email_from_alias, :string, null: true + end +end diff --git a/db/migrate/20220125052845_fix_topic_like_count_including_whispers.rb b/db/migrate/20220125052845_fix_topic_like_count_including_whispers.rb new file mode 100644 index 0000000000..d296e84628 --- /dev/null +++ b/db/migrate/20220125052845_fix_topic_like_count_including_whispers.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class FixTopicLikeCountIncludingWhispers < ActiveRecord::Migration[6.0] + def up + whisper_post_type = 4 + + DB.exec(<<~SQL) + UPDATE topics SET like_count = tbl.like_count + FROM ( + SELECT topic_id, SUM(like_count) like_count + FROM posts + WHERE deleted_at IS NULL + AND post_type <> #{whisper_post_type} + GROUP BY topic_id + ) AS tbl + WHERE topics.id = tbl.topic_id + AND topics.like_count <> tbl.like_count + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20220126052157_change_segment_cjk_site_setting.rb b/db/migrate/20220126052157_change_segment_cjk_site_setting.rb new file mode 100644 index 0000000000..4552c87b8b --- /dev/null +++ b/db/migrate/20220126052157_change_segment_cjk_site_setting.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ChangeSegmentCjkSiteSetting < ActiveRecord::Migration[6.1] + def up + execute <<~SQL + UPDATE site_settings + SET name = 'search_tokenize_chinese' + WHERE name = 'search_tokenize_chinese_japanese_korean' + SQL + + execute <<~SQL + DELETE FROM site_settings + WHERE name = 'search_tokenize_chinese_japanese_korean' + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb b/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb new file mode 100644 index 0000000000..22b19c60db --- /dev/null +++ b/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class SetUseEmailForUsernameAndNameSuggestionsOnExistingSites < ActiveRecord::Migration[6.1] + def up + result = execute <<~SQL + SELECT created_at + FROM schema_migration_details + ORDER BY created_at + LIMIT 1 + SQL + + # make setting enabled for existing sites + if result.first['created_at'].to_datetime < 1.hour.ago + execute <<~SQL + INSERT INTO site_settings(name, data_type, value, created_at, updated_at) + VALUES('use_email_for_username_and_name_suggestions', 5, 't', NOW(), NOW()) + ON CONFLICT (name) DO NOTHING + SQL + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20220202225716_add_external_id_to_topics.rb b/db/migrate/20220202225716_add_external_id_to_topics.rb new file mode 100644 index 0000000000..6ee7c3edb5 --- /dev/null +++ b/db/migrate/20220202225716_add_external_id_to_topics.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddExternalIdToTopics < ActiveRecord::Migration[6.1] + def change + add_column :topics, :external_id, :string, null: true + add_index :topics, :external_id, unique: true, where: 'external_id IS NOT NULL' + end +end diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index a4dac6c68b..db713d2a0a 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -28,8 +28,8 @@ To get your Ubuntu 16.04 or 18.04 LTS install up and running to develop Discours # exit the terminal and open it again to activate RVM - rvm install 2.6.2 - rvm --default use 2.6.2 # If this error out check https://rvm.io/integration/gnome-terminal + rvm install 2.7.2 + rvm --default use 2.7.2 # If this error out check https://rvm.io/integration/gnome-terminal gem install bundler rake # Download and install postgresql-10 from https://wiki.postgresql.org/wiki/Apt diff --git a/install-imagemagick b/install-imagemagick deleted file mode 100755 index 7122613547..0000000000 --- a/install-imagemagick +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -set -e - -# version check: https://github.com/ImageMagick/ImageMagick/releases -IMAGE_MAGICK_VERSION="7.0.11-13" -IMAGE_MAGICK_HASH="fc454be622724c6224fa6c8230bb9c50191a05fbf05b9c9c25aa3e5497090b83" - -# version check: https://github.com/strukturag/libheif/releases -LIBHEIF_VERSION="1.12.0" -LIBHEIF_HASH="086145b0d990182a033b0011caadb1b642da84f39ab83aa66d005610650b3c65" - -# version check: https://aomedia.googlesource.com/aom -LIB_AOM_VERSION="3.1.0" - -# We use debian, but GitHub CI is stuck on Ubuntu Bionic, so this must be compatible with both -LIBJPEGTURBO=$(cat /etc/issue | grep -qi Debian && echo 'libjpeg62-turbo libjpeg62-turbo-dev' || echo 'libjpeg-turbo8 libjpeg-turbo8-dev') - -PREFIX=/usr/local -WDIR=/tmp/imagemagick - -# Install build deps -apt -y -q remove imagemagick -apt -y -q install git make gcc pkg-config autoconf curl g++ \ - yasm cmake \ - libde265-0 libde265-dev ${LIBJPEGTURBO} x265 libx265-dev libtool \ - libpng16-16 libpng-dev ${LIBJPEGTURBO} libwebp6 libwebp-dev libgomp1 libwebpmux3 libwebpdemux2 ghostscript libxml2-dev libxml2-utils \ - libbz2-dev gsfonts libtiff-dev libfreetype6-dev libjpeg-dev - -mkdir -p $WDIR -cd $WDIR - -# Building libaom -git clone https://aomedia.googlesource.com/aom -cd aom && git checkout v${LIB_AOM_VERSION} && cd .. -mkdir build_aom -cd build_aom -cmake ../aom/ -DENABLE_TESTS=0 -DBUILD_SHARED_LIBS=1 && make && make install -ldconfig /usr/local/lib -cd .. -rm -rf aom -rm -rf build_aom - -# Build and install libheif -cd $WDIR -wget -O $WDIR/libheif.tar.gz "https://github.com/strukturag/libheif/archive/v$LIBHEIF_VERSION.tar.gz" -sha256sum $WDIR/libheif.tar.gz -echo "$LIBHEIF_HASH $WDIR/libheif.tar.gz" | sha256sum -c -tar -xzvf $WDIR/libheif.tar.gz -cd libheif-$LIBHEIF_VERSION -./autogen.sh -./configure -make && make install - -# Build and install ImageMagick -wget -O $WDIR/ImageMagick.tar.gz "https://github.com/ImageMagick/ImageMagick/archive/$IMAGE_MAGICK_VERSION.tar.gz" -sha256sum $WDIR/ImageMagick.tar.gz -echo "$IMAGE_MAGICK_HASH $WDIR/ImageMagick.tar.gz" | sha256sum -c -IMDIR=$WDIR/$(tar tzf $WDIR/ImageMagick.tar.gz --wildcards "ImageMagick-*/configure" |cut -d/ -f1) -tar zxf $WDIR/ImageMagick.tar.gz -C $WDIR -cd $IMDIR -PKG_CONF_LIBDIR=$PREFIX/lib LDFLAGS=-L$PREFIX/lib CFLAGS=-I$PREFIX/include ./configure \ - --prefix=$PREFIX \ - --enable-static \ - --enable-bounds-checking \ - --enable-hdri \ - --enable-hugepages \ - --with-threads \ - --with-modules \ - --with-quantum-depth=16 \ - --without-magick-plus-plus \ - --with-bzlib \ - --with-zlib \ - --without-autotrace \ - --with-freetype \ - --with-jpeg \ - --without-lcms \ - --with-lzma \ - --with-png \ - --with-tiff \ - --with-heic \ - --with-webp -make all && make install - -cd $HOME -rm -rf $WDIR -ldconfig /usr/local/lib diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 1a3efcf303..09298af2d4 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -196,7 +196,9 @@ class Auth::Result end def username_suggester_attributes - [username, name, email] + attributes = [username, name] + attributes << email if SiteSetting.use_email_for_username_and_name_suggestions + attributes end def authenticator diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index f0e3537ebf..ffeffe788e 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -415,7 +415,10 @@ class CookedPostProcessor %w{src data-small-upload}.each do |selector| @doc.css("img[#{selector}]").each do |img| - img[selector] = UrlHelper.cook_url(img[selector].to_s, secure: @post.with_secure_media?) + custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) + img[selector] = UrlHelper.cook_url( + img[selector].to_s, secure: @post.with_secure_media? && !custom_emoji + ) end end end diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index a66fed3aef..ed7556d299 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -27,11 +27,6 @@ class Demon::Sidekiq < ::Demon::Base puts "Loading Sidekiq in process id #{Process.pid}" require 'sidekiq/cli' - # CLI will close the logger, if we have one set we can be in big - # trouble, if STDOUT is closed in our process all sort of weird - # will ensue, resetting the logger ensures it will reinit correctly - # parent process is in charge of the file anyway. - Sidekiq.logger = nil cli = Sidekiq::CLI.instance # Unicorn uses USR1 to indicate that log files have been rotated diff --git a/lib/discourse.rb b/lib/discourse.rb index c93b1dc42f..f65add734b 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -835,7 +835,7 @@ module Discourse # logster Rails.logger.add_with_opts( ::Logger::Severity::WARN, - "#{message} : #{e}", + "#{message} : #{e.class.name} : #{e}", "discourse-exception", backtrace: e.backtrace.join("\n"), env: env diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index 869ac8ace4..b1576a573c 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -76,10 +76,11 @@ class DiscourseRedis DiscourseRedis.ignore_readonly { @redis.mget(*args) } end - def del(k) + def del(*keys) DiscourseRedis.ignore_readonly do - k = "#{namespace}:#{k}" if @namespace - @redis.del k + keys = keys.flatten(1) + keys.map! { |k| "#{namespace}:#{k}" } if @namespace + @redis.del(*keys) end end diff --git a/lib/email/message_id_service.rb b/lib/email/message_id_service.rb index 4143d00d8d..72d67912dd 100644 --- a/lib/email/message_id_service.rb +++ b/lib/email/message_id_service.rb @@ -39,8 +39,15 @@ module Email def generate_for_topic(topic, use_incoming_email_if_present: false, canonical: false) first_post = topic.ordered_posts.first + incoming_email = first_post.incoming_email - if use_incoming_email_if_present && first_post.incoming_email&.message_id.present? + # If the incoming email was created by handle_mail, then it was an + # inbound email sent to Discourse and handled by Email::Receiver, + # this is the only case where we want to use the original Message-ID + # because we want to maintain threading in the original mail client. + if use_incoming_email_if_present && + incoming_email&.message_id.present? && + incoming_email&.created_via == IncomingEmail.created_via_types[:handle_mail] return "<#{first_post.incoming_email.message_id}>" end diff --git a/lib/email_updater.rb b/lib/email_updater.rb index f5e41fc812..c3e3ea8abf 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -24,7 +24,7 @@ class EmailUpdater if existing_user = User.find_by_email(email) if SiteSetting.hide_email_address_taken - Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id) + Jobs.enqueue(:critical_user_email, type: "account_exists", user_id: existing_user.id) else error_message = +'change_email.error' error_message << '_staged' if existing_user.staged? @@ -68,10 +68,10 @@ class EmailUpdater if @change_req.change_state == EmailChangeRequest.states[:authorizing_old] @change_req.old_email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update]) - send_email(add ? :confirm_old_email_add : :confirm_old_email, @change_req.old_email_token) + send_email(add ? "confirm_old_email_add" : "confirm_old_email", @change_req.old_email_token) elsif @change_req.change_state == EmailChangeRequest.states[:authorizing_new] @change_req.new_email_token = @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update]) - send_email(:confirm_new_email, @change_req.new_email_token) + send_email("confirm_new_email", @change_req.new_email_token) end @change_req.save! @@ -102,7 +102,7 @@ class EmailUpdater change_state: EmailChangeRequest.states[:authorizing_new], new_email_token: @user.email_tokens.create!(email: @change_req.new_email, scope: EmailToken.scopes[:email_update]) ) - send_email(:confirm_new_email, @change_req.new_email_token) + send_email("confirm_new_email", @change_req.new_email_token) confirm_result = :authorizing_new when EmailChangeRequest.states[:authorizing_new] @change_req.update!(change_state: EmailChangeRequest.states[:complete]) @@ -144,7 +144,7 @@ class EmailUpdater def send_email_notification(old_email, new_email) Jobs.enqueue :critical_user_email, to_address: @user.email, - type: old_email ? :notify_old_email : :notify_old_email_add, + type: old_email ? "notify_old_email" : "notify_old_email_add", user_id: @user.id, new_email: new_email end diff --git a/lib/final_destination.rb b/lib/final_destination.rb index ac31c58a69..078fd4fd78 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -46,9 +46,9 @@ class FinalDestination @opts[:max_redirects] ||= 5 @opts[:lookup_ip] ||= lambda { |host| FinalDestination.lookup_ip(host) } - @ignored = @opts[:ignore_hostnames] || [] @limit = @opts[:max_redirects] + @ignored = [] if @limit > 0 ignore_redirects = [Discourse.base_url_no_prefix] diff --git a/lib/freedom_patches/cose_rsapkcs1.rb b/lib/freedom_patches/cose_rsapkcs1.rb new file mode 100644 index 0000000000..d407260dc8 --- /dev/null +++ b/lib/freedom_patches/cose_rsapkcs1.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'cose' +require 'openssl/signature_algorithm/rsapkcs1' + +# 'cose' gem does not implement all algorithms from the Web Authentication +# (WebAuthn) standard specification. This patch implements one of the missing +# ones, RSASSA-PKCS1-v1_5. +module COSE + module Algorithm + def self.registered_algorithm_ids + @registered_by_id.keys + end + + class RSAPKCS1 < SignatureAlgorithm + attr_reader :hash_function + + def initialize(*args, hash_function:) + super(*args) + + @hash_function = hash_function + end + + private + + def valid_key?(key) + to_cose_key(key).is_a?(COSE::Key::RSA) + end + + def signature_algorithm_class + OpenSSL::SignatureAlgorithm::RSAPKCS1 + end + + def to_pkey(key) + case key + when COSE::Key::RSA + key.to_pkey + when OpenSSL::PKey::RSA + key + else + raise(COSE::Error, 'Incompatible key for algorithm') + end + end + end + + register(RSAPKCS1.new(-257, 'RS256', hash_function: 'SHA256')) + register(RSAPKCS1.new(-258, 'RS384', hash_function: 'SHA384')) + register(RSAPKCS1.new(-259, 'RS512', hash_function: 'SHA512')) + end +end diff --git a/lib/freedom_patches/schema_cache_concurrency.rb b/lib/freedom_patches/schema_cache_concurrency.rb deleted file mode 100644 index b805c66526..0000000000 --- a/lib/freedom_patches/schema_cache_concurrency.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true -# -# Rails has a circular dependency in SchemaCache. -# In certain situation SchemaCache can carry a @connection -# from a different thread. This causes potential concurrency bugs -# in Sidekiq. -# -# This patches it so it is less flexible (theoretically) but always bound to the current connection - -# This patch needs to be reviewed in future versions of Rails. -# We should consider upstreaming this optionally. - -module ActiveRecord - module ConnectionAdapters - class SchemaCache - - def connection=(connection) - # AbstractPool get_schema_cache does schema_cache.connection = connection - Thread.current["schema_cached_connection"] = connection - end - - def connection - Thread.current["schema_cached_connection"] - end - end - end -end diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index b0d20bd12b..3ac00d847a 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -60,6 +60,7 @@ module UserGuardian def can_delete_user?(user) return false if user.nil? || user.admin? + if is_me?(user) !SiteSetting.enable_discourse_connect && !user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count) diff --git a/lib/inline_oneboxer.rb b/lib/inline_oneboxer.rb index 2e2dcf2c60..2ce9b34a32 100644 --- a/lib/inline_oneboxer.rb +++ b/lib/inline_oneboxer.rb @@ -61,7 +61,7 @@ class InlineOneboxer if uri.present? && uri.hostname.present? && (always_allow || allowed_domains.include?(uri.hostname)) && - !domain_is_blocked?(uri.hostname) + !Onebox::DomainChecker.is_blocked?(uri.hostname) title = RetrieveTitle.crawl(url) title = nil if title && title.length < MIN_TITLE_LENGTH return onebox_for(url, title, opts) @@ -73,12 +73,6 @@ class InlineOneboxer private - def self.domain_is_blocked?(hostname) - SiteSetting.blocked_onebox_domains&.split('|').any? do |blocked| - hostname == blocked || hostname.end_with?(".#{blocked}") - end - end - def self.onebox_for(url, title, opts) title = title && Emoji.gsub_emoji_to_unicode(title) if title && opts[:post_number] diff --git a/lib/javascripts/locale/pl.js b/lib/javascripts/locale/pl.js deleted file mode 100644 index a9020d0a5e..0000000000 --- a/lib/javascripts/locale/pl.js +++ /dev/null @@ -1,15 +0,0 @@ -MessageFormat.locale.pl = function (n) { - if (n == 1) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && - ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { - return 'few'; - } - if ((n % 10) === 0 || n != 1 && (n % 10) == 1 || - ((n % 10) >= 5 && (n % 10) <= 9 || (n % 100) >= 12 && (n % 100) <= 14) && - n == Math.floor(n)) { - return 'many'; - } - return 'other'; -}; diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 4ca3cd5864..d5a319b41e 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -289,6 +289,13 @@ module JsLocaleHelper result = +"MessageFormat = {locale: {}};\n" result << "I18n._compiledMFs = {#{formats}};\n" result << File.read(filename) << "\n" + + if locale != "en" + # Include "en" pluralization rules for use in fallbacks + _, en_filename = find_message_format_locale(["en"], fallback_to_english: false) + result << File.read(en_filename) << "\n" + end + result << File.read("#{Rails.root}/lib/javascripts/messageformat-lookup.js") << "\n" end diff --git a/lib/middleware/discourse_public_exceptions.rb b/lib/middleware/discourse_public_exceptions.rb index fcc2b5ed26..9a9ea11571 100644 --- a/lib/middleware/discourse_public_exceptions.rb +++ b/lib/middleware/discourse_public_exceptions.rb @@ -4,6 +4,11 @@ # we need to handle certain exceptions here module Middleware class DiscoursePublicExceptions < ::ActionDispatch::PublicExceptions + INVALID_REQUEST_ERRORS = Set.new([ + Rack::QueryParser::InvalidParameterError, + ActionController::BadRequest, + ActionDispatch::Http::Parameters::ParseError, + ]) def initialize(path) super @@ -18,12 +23,7 @@ module Middleware exception = env["action_dispatch.exception"] response = ActionDispatch::Response.new - # Special handling for invalid params, in this case we can not re-dispatch - # the Request object has a "broken" .params which can not be accessed - exception = nil if Rack::QueryParser::InvalidParameterError === exception - - # We also can not dispatch bad requests as no proper params - exception = nil if ActionController::BadRequest === exception + exception = nil if INVALID_REQUEST_ERRORS.include?(exception) if exception begin @@ -38,6 +38,13 @@ module Middleware return [400, { "Cache-Control" => "private, max-age=0, must-revalidate" }, ["Invalid MIME type"]] end + # Or badly formatted multipart requests + begin + request.POST + rescue EOFError + return [400, { "Cache-Control" => "private, max-age=0, must-revalidate" }, ["Invalid request"]] + end + if ApplicationController.rescue_with_handler(exception, object: fake_controller) body = response.body if String === body @@ -46,6 +53,7 @@ module Middleware return [response.status, response.headers, body] end rescue => e + return super if INVALID_REQUEST_ERRORS.include?(e.class) Discourse.warn_exception(e, message: "Failed to handle exception in exception app middleware") end diff --git a/lib/mini_sql_multisite_connection.rb b/lib/mini_sql_multisite_connection.rb index a4feb1a870..26ad1d5026 100644 --- a/lib/mini_sql_multisite_connection.rb +++ b/lib/mini_sql_multisite_connection.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class MiniSqlMultisiteConnection < MiniSql::Postgres::Connection +class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection class CustomBuilder < MiniSql::Builder @@ -78,8 +78,8 @@ class MiniSqlMultisiteConnection < MiniSql::Postgres::Connection # we need a tiny adapter here so we always run against the # correct multisite connection - def raw_connection - ActiveRecord::Base.connection.raw_connection + def active_record_connection + ActiveRecord::Base.connection end # make for a multisite friendly prepared statement cache diff --git a/lib/onebox/domain_checker.rb b/lib/onebox/domain_checker.rb new file mode 100644 index 0000000000..1dd810491e --- /dev/null +++ b/lib/onebox/domain_checker.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Onebox + class DomainChecker + def self.is_blocked?(hostname) + SiteSetting.blocked_onebox_domains&.split('|').any? do |blocked| + hostname == blocked || hostname.end_with?(".#{blocked}") + end + end + end +end diff --git a/lib/onebox/engine.rb b/lib/onebox/engine.rb index 99064c64b3..5d64fa41f6 100644 --- a/lib/onebox/engine.rb +++ b/lib/onebox/engine.rb @@ -210,3 +210,4 @@ require_relative "engine/kaltura_onebox" require_relative "engine/reddit_media_onebox" require_relative "engine/google_drive_onebox" require_relative "engine/facebook_media_onebox" +require_relative "engine/hackernews_onebox" diff --git a/lib/onebox/engine/hackernews_onebox.rb b/lib/onebox/engine/hackernews_onebox.rb new file mode 100644 index 0000000000..79d8e037a5 --- /dev/null +++ b/lib/onebox/engine/hackernews_onebox.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Onebox + module Engine + class HackernewsOnebox + include Engine + include LayoutSupport + include JSON + + REGEX = /^https?:\/\/news\.ycombinator\.com\/item\?id=(?\d+)/ + + matches_regexp(REGEX) + + # This is their official API: https://blog.ycombinator.com/hacker-news-api/ + def url + "https://hacker-news.firebaseio.com/v0/item/#{match[:item_id]}.json" + end + + private + + def match + @match ||= @url.match(REGEX) + end + + def data + return nil unless %w{story comment}.include?(raw['type']) + + html_entities = HTMLEntities.new + data = { + link: @url, + title: Onebox::Helpers.truncate(raw['title'], 80), + favicon: 'https://news.ycombinator.com/y18.gif', + timestamp: Time.at(raw['time']).strftime("%-l:%M %p - %-d %b %Y"), + author: raw['by'] + } + + data['description'] = html_entities.decode(Onebox::Helpers.truncate(raw['text'], 400)) if raw['text'] + + if raw['type'] == 'story' + data['data_1'] = raw['score'] + data['data_2'] = raw['descendants'] + end + + data + end + end + end +end diff --git a/lib/onebox/engine/wistia_onebox.rb b/lib/onebox/engine/wistia_onebox.rb index 4abb2ff7fc..c8fe3a2ae3 100644 --- a/lib/onebox/engine/wistia_onebox.rb +++ b/lib/onebox/engine/wistia_onebox.rb @@ -11,7 +11,25 @@ module Onebox always_https def to_html - get_oembed.html + oembed = get_oembed + extracted_url = oembed.html.match(/iframe\ src\=\"(.*?)\"/) + + if extracted_url + iframe_src = extracted_url[1] + + <<~HTML + + HTML + else + oembed.html + end end def placeholder_html diff --git a/lib/onebox/templates/hackernews.mustache b/lib/onebox/templates/hackernews.mustache new file mode 100644 index 0000000000..e4431a2601 --- /dev/null +++ b/lib/onebox/templates/hackernews.mustache @@ -0,0 +1,18 @@ +

{{title}}

+ +{{#description}} +

{{description}}

+{{/description}} + + +

+ {{#data_1}} + {{data_1}} points — + {{/data_1}} + {{#data_2}} + {{data_2}} comments — + {{/data_2}} + {{author}} — + {{timestamp}} +

+ diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb index 61393627af..132f30d6b2 100644 --- a/lib/oneboxer.rb +++ b/lib/oneboxer.rb @@ -379,10 +379,6 @@ module Oneboxer end end - def self.blocked_domains - SiteSetting.blocked_onebox_domains.split("|") - end - def self.preserve_fragment_url_hosts @preserve_fragment_url_hosts ||= ['http://github.com'] end @@ -420,7 +416,7 @@ module Oneboxer return error_box end - return blank_onebox if uri.blank? || blocked_domains.any? { |hostname| uri.hostname.match?(hostname) } + return blank_onebox if uri.blank? || Onebox::DomainChecker.is_blocked?(uri.hostname) onebox_options = { max_width: 695, @@ -538,7 +534,6 @@ module Oneboxer def self.get_final_destination_options(url, strategy = nil) fd_options = { ignore_redirects: ignore_redirects, - ignore_hostnames: blocked_domains, force_get_hosts: force_get_hosts, force_custom_user_agent_hosts: force_custom_user_agent_hosts, preserve_fragment_url_hosts: preserve_fragment_url_hosts, diff --git a/lib/pbkdf2.rb b/lib/pbkdf2.rb index 62551d052f..933630fc1b 100644 --- a/lib/pbkdf2.rb +++ b/lib/pbkdf2.rb @@ -5,11 +5,8 @@ require 'openssl' require 'xorcist' -require 'xorcist/refinements' class Pbkdf2 - using Xorcist::Refinements - def self.hash_password(password, salt, iterations, algorithm = "sha256") h = OpenSSL::Digest.new(algorithm) @@ -18,7 +15,7 @@ class Pbkdf2 2.upto(iterations) do u = prf(h, password, u) - ret.xor!(u) + Xorcist.xor!(ret, u) end ret.bytes.map { |b| ("0" + b.to_s(16))[-2..-1] }.join("") diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index 76bf296556..43688cb343 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -79,6 +79,7 @@ class Plugin::Metadata "discourse-user-notes", "discourse-vk-auth", "discourse-voting", + "discourse-whos-online", "discourse-yearly-review", "discourse-zendesk-plugin", "docker_manager", diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 3bedd2a1d1..39271eae90 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -36,6 +36,7 @@ class PostCreator # hidden_reason_id - Reason for hiding the post (optional) # skip_validations - Do not validate any of the content in the post # draft_key - the key of the draft we are creating (will be deleted on success) + # advance_draft - Destroy draft after creating post or topic # silent - Do not update topic stats and fields like last_post_user_id # # When replying to a topic: @@ -218,7 +219,7 @@ class PostCreator delete_owned_bookmarks ensure_in_allowed_users if guardian.is_staff? unarchive_message if !@opts[:import_mode] - DraftSequence.next!(@user, draft_key) if !@opts[:import_mode] + DraftSequence.next!(@user, draft_key) if !@opts[:import_mode] && @opts[:advance_draft] @post.save_reply_relationships end end @@ -599,16 +600,13 @@ class PostCreator @user.create_user_stat if @user.user_stat.nil? if @user.user_stat.first_post_created_at.nil? - @user.user_stat.first_post_created_at = @post.created_at + @user.user_stat.update!(first_post_created_at: @post.created_at) end - unless @post.topic.private_message? - @user.user_stat.post_count += 1 if @post.post_type == Post.types[:regular] && !@post.is_first_post? - @user.user_stat.topic_count += 1 if @post.is_first_post? + if !@post.hidden || @post.topic.visible + UserStatCountUpdater.increment!(@post) end - @user.user_stat.save! - if !@topic.private_message? && @post.post_type != Post.types[:whisper] @user.update(last_posted_at: @post.created_at) end diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index d9614b4543..8921a71512 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -155,8 +155,9 @@ class PostDestroyer make_previous_post_the_last_one mark_topic_changed clear_user_posted_flag - Topic.reset_highest(@post.topic_id) end + + Topic.reset_highest(@post.topic_id) trash_public_post_actions trash_revisions trash_user_actions @@ -177,7 +178,7 @@ class PostDestroyer end TopicLink.where(link_post_id: @post.id).destroy_all update_associated_category_latest_topic - update_user_counts + update_user_counts if !permanent? TopicUser.update_post_action_cache(post_id: @post.id) DB.after_commit do @@ -387,17 +388,10 @@ class PostDestroyer author.create_user_stat if author.user_stat.nil? if @post.created_at == author.user_stat.first_post_created_at - author.user_stat.first_post_created_at = author.posts.order('created_at ASC').first.try(:created_at) + author.user_stat.update!(first_post_created_at: author.posts.order('created_at ASC').first.try(:created_at)) end - if @post.topic && !@post.topic.private_message? - if @post.post_type == Post.types[:regular] && !@post.is_first_post? && !@topic.nil? - author.user_stat.post_count -= 1 - end - author.user_stat.topic_count -= 1 if @post.is_first_post? - end - - author.user_stat.save! + UserStatCountUpdater.decrement!(@post) if @post.created_at == author.last_posted_at author.last_posted_at = author.posts.order('created_at DESC').first.try(:created_at) @@ -407,6 +401,7 @@ class PostDestroyer if @post.is_first_post? && @post.topic && !@post.topic.private_message? # Update stats of all people who replied counts = Post.where(post_type: Post.types[:regular], topic_id: @post.topic_id).where('post_number > 1').group(:user_id).count + counts.each do |user_id, count| if user_stat = UserStat.where(user_id: user_id).first user_stat.update(post_count: user_stat.post_count - count) diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index f3b8aa7333..8c046392a4 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -437,22 +437,19 @@ class PostRevisor private_message = @topic.private_message? prev_owner_user_stat = prev_owner.user_stat + unless private_message - prev_owner_user_stat.post_count -= 1 if @post.post_type == Post.types[:regular] - prev_owner_user_stat.topic_count -= 1 if @post.is_first_post? + UserStatCountUpdater.decrement!(@post, user_stat: prev_owner_user_stat) if !@post.trashed? prev_owner_user_stat.likes_received -= likes end if @post.created_at == prev_owner.user_stat.first_post_created_at - prev_owner_user_stat.first_post_created_at = prev_owner.posts.order('created_at ASC').first.try(:created_at) + prev_owner_user_stat.update!(first_post_created_at: prev_owner.posts.order('created_at ASC').first.try(:created_at)) end - prev_owner_user_stat.save! - new_owner_user_stat = new_owner.user_stat unless private_message - new_owner_user_stat.post_count += 1 if @post.post_type == Post.types[:regular] - new_owner_user_stat.topic_count += 1 if @post.is_first_post? + UserStatCountUpdater.increment!(@post, user_stat: new_owner_user_stat) if !@post.trashed? new_owner_user_stat.likes_received += likes end new_owner_user_stat.save! diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index a5885ce22d..f42ff5b905 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -82,10 +82,18 @@ module PrettyText ctx.eval("window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina - if Rails.env.development? || Rails.env.test? - ctx.attach("console.log", proc { |l| p l }) - ctx.eval('window.console = console;') - end + ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) }) + ctx.attach("rails.logger.warn", proc { |err| Rails.logger.warn(err.to_s) }) + ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) }) + ctx.eval <<~JS + console = { + prefix: "[PrettyText] ", + log: function(...args){ rails.logger.info(console.prefix + args.join(" ")); }, + warn: function(...args){ rails.logger.warn(console.prefix + args.join(" ")); }, + error: function(...args){ rails.logger.error(console.prefix + args.join(" ")); } + } + JS + ctx.eval("__PRETTY_TEXT = true") PrettyText::Helpers.instance_methods.each do |method| @@ -201,6 +209,7 @@ module PrettyText __optInput.censoredRegexp = #{WordWatcher.word_matcher_regexp(:censor)&.source.to_json}; __optInput.watchedWordsReplace = #{WordWatcher.word_matcher_regexps(:replace).to_json}; __optInput.watchedWordsLink = #{WordWatcher.word_matcher_regexps(:link).to_json}; + __optInput.additionalOptions = #{Site.markdown_additional_options.to_json}; JS if opts[:topic_id] diff --git a/lib/rate_limiter.rb b/lib/rate_limiter.rb index f7572d4b05..06f70dc4ad 100644 --- a/lib/rate_limiter.rb +++ b/lib/rate_limiter.rb @@ -37,7 +37,7 @@ class RateLimiter "#{RateLimiter.key_prefix}:#{@user && @user.id}:#{type}" end - def initialize(user, type, max, secs, global: false, aggressive: false, error_code: nil) + def initialize(user, type, max, secs, global: false, aggressive: false, error_code: nil, apply_limit_to_staff: false, staff_limit: { max: nil, secs: nil }) @user = user @type = type @key = build_key(type) @@ -46,6 +46,14 @@ class RateLimiter @global = global @aggressive = aggressive @error_code = error_code + @apply_limit_to_staff = apply_limit_to_staff + @staff_limit = staff_limit + + # override the default values if staff user, and staff specific max is passed + if @user&.staff? && !@apply_limit_to_staff && @staff_limit[:max].present? + @max = @staff_limit[:max] + @secs = @staff_limit[:secs] + end end def clear! @@ -115,8 +123,7 @@ class RateLimiter def performed!(raise_error: true) return true if rate_unlimited? now = Time.now.to_i - - if ((max || 0) <= 0) || rate_limiter_allowed?(now) + if ((@max || 0) <= 0) || rate_limiter_allowed?(now) raise RateLimiter::LimitExceeded.new(seconds_to_wait(now), @type, @error_code) if raise_error false else @@ -153,7 +160,6 @@ class RateLimiter private def rate_limiter_allowed?(now) - lua, lua_sha = nil if @aggressive lua = PERFORM_LUA_AGGRESSIVE @@ -193,7 +199,7 @@ class RateLimiter end def rate_unlimited? - !!(RateLimiter.disabled? || (@user && @user.staff?)) + !!(RateLimiter.disabled? || (@user&.staff? && !@apply_limit_to_staff && @staff_limit[:max].nil?)) end def eval_lua(lua, sha, keys, args) diff --git a/lib/retrieve_title.rb b/lib/retrieve_title.rb index 2fd361ca90..74ad06c462 100644 --- a/lib/retrieve_title.rb +++ b/lib/retrieve_title.rb @@ -41,11 +41,10 @@ module RetrieveTitle private def self.max_chunk_size(uri) - - # Amazon and YouTube leave the title until very late. Exceptions are bad - # but these are large sites. - return 500 if uri.host =~ /amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/ - return 300 if uri.host =~ /youtube\.com$/ || uri.host =~ /youtu.be/ + # Exception for sites that leave the title until very late. + return 500 if uri.host =~ /(^|\.)amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/ + return 300 if uri.host =~ /(^|\.)youtube\.com$/ || uri.host =~ /(^|\.)youtu\.be$/ + return 50 if uri.host =~ /(^|\.)github\.com$/ # default is 20k 20 @@ -60,7 +59,7 @@ module RetrieveTitle encoding = nil fd.get do |_response, chunk, uri| - if (uri.present? && InlineOneboxer.domain_is_blocked?(uri.hostname)) + if (uri.present? && Onebox::DomainChecker.is_blocked?(uri.hostname)) throw :done end diff --git a/lib/search.rb b/lib/search.rb index 4a3ab96da1..e9a9d30bd5 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -64,9 +64,17 @@ class Search end end - def self.segment_cjk? - ['zh_TW', 'zh_CN', 'ja'].include?(SiteSetting.default_locale) || - SiteSetting.search_tokenize_chinese_japanese_korean + def self.segment_chinese? + ['zh_TW', 'zh_CN'].include?(SiteSetting.default_locale) || SiteSetting.search_tokenize_chinese + end + + def self.segment_japanese? + SiteSetting.default_locale == "ja" || SiteSetting.search_tokenize_japanese + end + + def self.japanese_punctuation_regexp + # Regexp adapted from https://github.com/6/tiny_segmenter/blob/15a5b825993dfd2c662df3766f232051716bef5b/lib/tiny_segmenter.rb#L7 + @japanese_punctuation_regexp ||= Regexp.compile("[-–—―.。・()()[]{}{}【】⟨⟩、、,,،…‥〽「」『』〜~!!::??\"'|__“”‘’;/⁄/«»]") end def self.prepare_data(search_data, purpose = nil) @@ -74,22 +82,35 @@ class Search data.force_encoding("UTF-8") if purpose != :topic - # TODO cppjieba_rb is designed for chinese, we need something else for Japanese - # Korean appears to be safe cause words are already space separated - # For Japanese we should investigate using kakasi - if segment_cjk? + if segment_chinese? require 'cppjieba_rb' unless defined? CppjiebaRb - data = CppjiebaRb.segment(search_data, mode: :mix) - # TODO: we still want to tokenize here but the current stopword list is too wide - # in cppjieba leading to words such as volume to be skipped. PG already has an English - # stopword list so use that vs relying on cppjieba - if ts_config != 'english' - data = CppjiebaRb.filter_stop_word(data) - else - data = data.filter { |s| s.present? } + segmented_data = [] + + # We need to split up the string here because Cppjieba has a bug where text starting with numeric chars will + # be split into two segments. For example, '123abc' becomes '123' and 'abc' after segmentation. + data.scan(/(?[\p{Han}。,、“”《》…\.:?!;()]+)|([^\p{Han}]+)/) do + match_data = $LAST_MATCH_INFO + + if match_data[:chinese] + segments = CppjiebaRb.segment(match_data.to_s, mode: :mix) + + if ts_config != 'english' + segments = CppjiebaRb.filter_stop_word(segments) + end + + segments = segments.filter { |s| s.present? } + segmented_data << segments.join(' ') + else + segmented_data << match_data.to_s.squish + end end + data = segmented_data.join(' ') + elsif segment_japanese? + data.gsub!(japanese_punctuation_regexp, " ") + data = TinyJapaneseSegmenter.segment(data) + data = data.filter { |s| s.present? } data = data.join(' ') else data.squish! @@ -263,7 +284,7 @@ class Search end unless @filters.present? || @opts[:search_for_id] - min_length = @opts[:min_search_term_length] || SiteSetting.min_search_term_length + min_length = min_search_term_length terms = (@term || '').split(/\s(?=(?:[^"]|"[^"]*")*$)/).reject { |t| t.length < min_length } if terms.blank? @@ -571,7 +592,7 @@ class Search SQL # a bit yucky but we got to add the term back in - elsif match.to_s.length >= SiteSetting.min_search_term_length + elsif match.to_s.length >= min_search_term_length posts.where <<~SQL posts.id IN ( SELECT post_id FROM post_search_data pd1 @@ -1304,4 +1325,18 @@ class Search !readonly_mode && @opts[:type_filter] != "exclude_topics" end + + def min_search_term_length + return @opts[:min_search_term_length] if @opts[:min_search_term_length] + + if SiteSetting.search_tokenize_chinese + return SiteSetting.defaults.get('min_search_term_length', 'zh_CN') + end + + if SiteSetting.search_tokenize_japanese + return SiteSetting.defaults.get('min_search_term_length', 'ja') + end + + SiteSetting.min_search_term_length + end end diff --git a/lib/search/grouped_search_results.rb b/lib/search/grouped_search_results.rb index 8182fe90bc..611ee168e8 100644 --- a/lib/search/grouped_search_results.rb +++ b/lib/search/grouped_search_results.rb @@ -87,7 +87,7 @@ class Search blurb_length: @blurb_length } - if post.post_search_data.version > SearchIndexer::MIN_POST_REINDEX_VERSION && !Search.segment_cjk? + if post.post_search_data.version > SearchIndexer::MIN_POST_REINDEX_VERSION && !Search.segment_chinese? && !Search.segment_japanese? if SiteSetting.use_pg_headlines_for_excerpt scrubbed_headline = post.headline.gsub(SCRUB_HEADLINE_REGEXP, '\1') prefix_omission = scrubbed_headline.start_with?(post.leading_raw_data) ? '' : OMISSION diff --git a/lib/site_settings/deprecated_settings.rb b/lib/site_settings/deprecated_settings.rb index 9e13541b35..45bfc46953 100644 --- a/lib/site_settings/deprecated_settings.rb +++ b/lib/site_settings/deprecated_settings.rb @@ -4,33 +4,8 @@ module SiteSettings; end module SiteSettings::DeprecatedSettings SETTINGS = [ - ['show_email_on_profile', 'moderators_view_emails', true, '2.4'], - ['allow_moderators_to_create_categories', 'moderators_create_categories', true, '2.4'], - ['disable_edit_notifications', 'disable_system_edit_notifications', true, '2.4'], - ['enable_category_group_review', 'enable_category_group_moderation', true, '2.7'], - ['newuser_max_images', 'newuser_max_embedded_media', true, '2.7'], - ['min_trust_to_post_images', 'min_trust_to_post_embedded_media', true, '2.7'], - ['moderators_create_categories', 'moderators_manage_categories_and_groups', '2.7'], - - ['enable_sso', 'enable_discourse_connect', true, '2.8'], - ['sso_allows_all_return_paths', 'discourse_connect_allows_all_return_paths', true, '2.8'], - ['enable_sso_provider', 'enable_discourse_connect_provider', true, '2.8'], - ['verbose_sso_logging', 'verbose_discourse_connect_logging', true, '2.8'], - ['sso_url', 'discourse_connect_url', true, '2.8'], - ['sso_secret', 'discourse_connect_secret', true, '2.8'], - ['sso_provider_secrets', 'discourse_connect_provider_secrets', true, '2.8'], - ['sso_overrides_groups', 'discourse_connect_overrides_groups', true, '2.8'], - ['sso_overrides_bio', 'discourse_connect_overrides_bio', true, '2.8'], - ['sso_overrides_email', 'auth_overrides_email', true, '2.8'], - ['sso_overrides_username', 'auth_overrides_username', true, '2.8'], - ['sso_overrides_name', 'auth_overrides_name', true, '2.8'], - ['sso_overrides_avatar', 'discourse_connect_overrides_avatar', true, '2.8'], - ['sso_overrides_profile_background', 'discourse_connect_overrides_profile_background', true, '2.8'], - ['sso_overrides_location', 'discourse_connect_overrides_location', true, '2.8'], - ['sso_overrides_website', 'discourse_connect_overrides_website', true, '2.8'], - ['sso_overrides_card_background', 'discourse_connect_overrides_card_background', true, '2.8'], - ['external_auth_skip_create_confirm', 'auth_skip_create_confirm', true, '2.8'], - ['external_auth_immediately', 'auth_immediately', true, '2.8'], + # [, , , ] + ['search_tokenize_chinese_japanese_korean', 'search_tokenize_chinese', true, '2.9'], ] def setup_deprecated_methods diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb index 428aadfe4c..5c37f26c89 100644 --- a/lib/site_settings/type_supervisor.rb +++ b/lib/site_settings/type_supervisor.rb @@ -269,6 +269,8 @@ class SiteSettings::TypeSupervisor RegexSettingValidator when self.class.types[:string], self.class.types[:list], self.class.types[:enum] StringSettingValidator + when self.class.types[:host_list] + HostListSettingValidator else nil end end diff --git a/lib/tasks/admin.rake b/lib/tasks/admin.rake index 104b042916..37ef651f2b 100644 --- a/lib/tasks/admin.rake +++ b/lib/tasks/admin.rake @@ -28,7 +28,7 @@ task "admin:invite", [:email] => [:environment] do |_, args| puts "Sending email!" email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) - Jobs.enqueue(:user_email, type: :account_created, user_id: user.id, email_token: email_token.token) + Jobs.enqueue(:user_email, type: "account_created", user_id: user.id, email_token: email_token.token) end desc "Creates a forum administrator" diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index f57971cee9..10e19be30e 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -35,11 +35,10 @@ task 'assets:precompile:before' do require 'sprockets' require 'digest/sha1' - if ENV['EMBER_CLI_PROD_ASSETS'] + if ENV['EMBER_CLI_PROD_ASSETS'] != "0" # Remove the assets that Ember CLI will handle for us Rails.configuration.assets.precompile.reject! do |asset| - asset.is_a?(String) && - (%w(application.js admin.js ember_jquery.js pretty-text-bundle.js start-discourse.js vendor.js).include?(asset)) + asset.is_a?(String) && is_ember_cli_asset?(asset) end end end @@ -82,6 +81,11 @@ task 'assets:flush_sw' => 'environment' do end end +def is_ember_cli_asset?(name) + return false if ENV['EMBER_CLI_PROD_ASSETS'] == '0' + %w(application.js admin.js ember_jquery.js pretty-text-bundle.js start-discourse.js vendor.js).include?(name) +end + def assets_path "#{Rails.root}/public/assets" end @@ -105,11 +109,11 @@ def compress_node(from, to) assets = cdn_relative_path("/assets") assets_additional_path = (d = File.dirname(from)) == "." ? "" : "/#{d}" source_map_root = assets + assets_additional_path - source_map_url = cdn_path "/assets/#{to}.map" + source_map_url = "#{File.basename(to)}.map" base_source_map = assets_path + assets_additional_path cmd = <<~EOS - terser '#{assets_path}/#{from}' -m -c -o '#{to_path}' --source-map "base='#{base_source_map}',root='#{source_map_root}',url='#{source_map_url}'" + terser '#{assets_path}/#{from}' -m -c -o '#{to_path}' --source-map "base='#{base_source_map}',root='#{source_map_root}',url='#{source_map_url}',includeSources=true" EOS STDERR.puts cmd @@ -162,6 +166,7 @@ end def max_compress?(path, locales) return false if Rails.configuration.assets.skip_minification.include? path + return false if is_ember_cli_asset?(path) return true unless path.include? "locales/" path_locale = path.delete_prefix("locales/").delete_suffix(".js") @@ -312,7 +317,7 @@ end task 'assets:precompile' => 'assets:precompile:before' do - copy_ember_cli_assets if ENV['EMBER_CLI_PROD_ASSETS'] + copy_ember_cli_assets if ENV['EMBER_CLI_PROD_ASSETS'] != '0' refresh_days = GlobalSetting.refresh_maxmind_db_during_precompile_days diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index 70570efe0b..b7751a4962 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -89,13 +89,6 @@ def dependencies }, { source: 'pikaday/pikaday.js', public: true - }, { - source: 'spectrum-colorpicker/spectrum.js', - uglify: true, - public: true - }, { - source: 'spectrum-colorpicker/spectrum.css', - public: true }, { source: 'handlebars/dist/handlebars.js' }, { diff --git a/lib/tasks/s3.rake b/lib/tasks/s3.rake index 2c54f9f6a4..b8aa43eee4 100644 --- a/lib/tasks/s3.rake +++ b/lib/tasks/s3.rake @@ -59,9 +59,9 @@ def assets fullpath = (Rails.root + "public/assets/#{path}").to_s # Ignore files we can't find the mime type of, like yarn.lock - if mime = MiniMime.lookup_by_filename(fullpath) - content_type = mime.content_type - + content_type = MiniMime.lookup_by_filename(fullpath)&.content_type + content_type ||= "application/json" if fullpath.end_with?(".map") + if content_type asset_path = "assets/#{path}" results << [fullpath, asset_path, content_type] diff --git a/lib/tiny_japanese_segmenter.rb b/lib/tiny_japanese_segmenter.rb new file mode 100644 index 0000000000..5da3175a36 --- /dev/null +++ b/lib/tiny_japanese_segmenter.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +# Ruby port of http://chasen.org/~taku/software/TinySegmenter/tiny_segmenter-0.2.js +# This is esstentially a trained machine learning model used to segment words in Japanese. +# Discourse core uses it for "best effort" segmentation of Japanese text for search. +class TinyJapaneseSegmenter + CHARTYPE = { + "[一二三四五六七八九十百千万億兆]" => "M", + "[一-龠々〆ヵヶ]" => "H", + "[ぁ-ん]" => "I", + "[ァ-ヴーア-ン゙ー]" => "K", + "[a-zA-Za-zA-Z]" => "A", + "[0-90-9]" => "N" + }.map do |pattern, value| + [Regexp.compile(pattern), value] + end + + BIAS = -322 + BC1 = { "HH" => 6, "II" => 2461, "KH" => 406, "OH" => -1378 } + BC2 = { "AA" => -3267, "AI" => 2744, "AN" => -878, "HH" => -4070, "HM" => -1711, "HN" => 4012, "HO" => 3761, "IA" => 1327, "IH" => -1184, "II" => -1332, "IK" => 1721, "IO" => 5492, "KI" => 3831, "KK" => -8741, "MH" => -3132, "MK" => 3334, "OO" => -2920 } + BC3 = { "HH" => 996, "HI" => 626, "HK" => -721, "HN" => -1307, "HO" => -836, "IH" => -301, "KK" => 2762, "MK" => 1079, "MM" => 4034, "OA" => -1652, "OH" => 266 } + BP1 = { "BB" => 295, "OB" => 304, "OO" => -125, "UB" => 352 } + BP2 = { "BO" => 60, "OO" => -1762 } + BQ1 = { "BHH" => 1150, "BHM" => 1521, "BII" => -1158, "BIM" => 886, "BMH" => 1208, "BNH" => 449, "BOH" => -91, "BOO" => -2597, "OHI" => 451, "OIH" => -296, "OKA" => 1851, "OKH" => -1020, "OKK" => 904, "OOO" => 2965 } + BQ2 = { "BHH" => 118, "BHI" => -1159, "BHM" => 466, "BIH" => -919, "BKK" => -1720, "BKO" => 864, "OHH" => -1139, "OHM" => -181, "OIH" => 153, "UHI" => -1146 } + BQ3 = { "BHH" => -792, "BHI" => 2664, "BII" => -299, "BKI" => 419, "BMH" => 937, "BMM" => 8335, "BNN" => 998, "BOH" => 775, "OHH" => 2174, "OHM" => 439, "OII" => 280, "OKH" => 1798, "OKI" => -793, "OKO" => -2242, "OMH" => -2402, "OOO" => 11699 } + BQ4 = { "BHH" => -3895, "BIH" => 3761, "BII" => -4654, "BIK" => 1348, "BKK" => -1806, "BMI" => -3385, "BOO" => -12396, "OAH" => 926, "OHH" => 266, "OHK" => -2036, "ONN" => -973 } + BW1 = { ",と" => 660, ",同" => 727, "B1あ" => 1404, "B1同" => 542, "、と" => 660, "、同" => 727, "」と" => 1682, "あっ" => 1505, "いう" => 1743, "いっ" => -2055, "いる" => 672, "うし" => -4817, "うん" => 665, "から" => 3472, "がら" => 600, "こう" => -790, "こと" => 2083, "こん" => -1262, "さら" => -4143, "さん" => 4573, "した" => 2641, "して" => 1104, "すで" => -3399, "そこ" => 1977, "それ" => -871, "たち" => 1122, "ため" => 601, "った" => 3463, "つい" => -802, "てい" => 805, "てき" => 1249, "でき" => 1127, "です" => 3445, "では" => 844, "とい" => -4915, "とみ" => 1922, "どこ" => 3887, "ない" => 5713, "なっ" => 3015, "など" => 7379, "なん" => -1113, "にし" => 2468, "には" => 1498, "にも" => 1671, "に対" => -912, "の一" => -501, "の中" => 741, "ませ" => 2448, "まで" => 1711, "まま" => 2600, "まる" => -2155, "やむ" => -1947, "よっ" => -2565, "れた" => 2369, "れで" => -913, "をし" => 1860, "を見" => 731, "亡く" => -1886, "京都" => 2558, "取り" => -2784, "大き" => -2604, "大阪" => 1497, "平方" => -2314, "引き" => -1336, "日本" => -195, "本当" => -2423, "毎日" => -2113, "目指" => -724, "B1あ" => 1404, "B1同" => 542, "」と" => 1682 } + BW2 = { ".." => -11822, "11" => -669, "――" => -5730, "−−" => -13175, "いう" => -1609, "うか" => 2490, "かし" => -1350, "かも" => -602, "から" => -7194, "かれ" => 4612, "がい" => 853, "がら" => -3198, "きた" => 1941, "くな" => -1597, "こと" => -8392, "この" => -4193, "させ" => 4533, "され" => 13168, "さん" => -3977, "しい" => -1819, "しか" => -545, "した" => 5078, "して" => 972, "しな" => 939, "その" => -3744, "たい" => -1253, "たた" => -662, "ただ" => -3857, "たち" => -786, "たと" => 1224, "たは" => -939, "った" => 4589, "って" => 1647, "っと" => -2094, "てい" => 6144, "てき" => 3640, "てく" => 2551, "ては" => -3110, "ても" => -3065, "でい" => 2666, "でき" => -1528, "でし" => -3828, "です" => -4761, "でも" => -4203, "とい" => 1890, "とこ" => -1746, "とと" => -2279, "との" => 720, "とみ" => 5168, "とも" => -3941, "ない" => -2488, "なが" => -1313, "など" => -6509, "なの" => 2614, "なん" => 3099, "にお" => -1615, "にし" => 2748, "にな" => 2454, "によ" => -7236, "に対" => -14943, "に従" => -4688, "に関" => -11388, "のか" => 2093, "ので" => -7059, "のに" => -6041, "のの" => -6125, "はい" => 1073, "はが" => -1033, "はず" => -2532, "ばれ" => 1813, "まし" => -1316, "まで" => -6621, "まれ" => 5409, "めて" => -3153, "もい" => 2230, "もの" => -10713, "らか" => -944, "らし" => -1611, "らに" => -1897, "りし" => 651, "りま" => 1620, "れた" => 4270, "れて" => 849, "れば" => 4114, "ろう" => 6067, "われ" => 7901, "を通" => -11877, "んだ" => 728, "んな" => -4115, "一人" => 602, "一方" => -1375, "一日" => 970, "一部" => -1051, "上が" => -4479, "会社" => -1116, "出て" => 2163, "分の" => -7758, "同党" => 970, "同日" => -913, "大阪" => -2471, "委員" => -1250, "少な" => -1050, "年度" => -8669, "年間" => -1626, "府県" => -2363, "手権" => -1982, "新聞" => -4066, "日新" => -722, "日本" => -7068, "日米" => 3372, "曜日" => -601, "朝鮮" => -2355, "本人" => -2697, "東京" => -1543, "然と" => -1384, "社会" => -1276, "立て" => -990, "第に" => -1612, "米国" => -4268, "11" => -669 } + BW3 = { "あた" => -2194, "あり" => 719, "ある" => 3846, "い." => -1185, "い。" => -1185, "いい" => 5308, "いえ" => 2079, "いく" => 3029, "いた" => 2056, "いっ" => 1883, "いる" => 5600, "いわ" => 1527, "うち" => 1117, "うと" => 4798, "えと" => 1454, "か." => 2857, "か。" => 2857, "かけ" => -743, "かっ" => -4098, "かに" => -669, "から" => 6520, "かり" => -2670, "が," => 1816, "が、" => 1816, "がき" => -4855, "がけ" => -1127, "がっ" => -913, "がら" => -4977, "がり" => -2064, "きた" => 1645, "けど" => 1374, "こと" => 7397, "この" => 1542, "ころ" => -2757, "さい" => -714, "さを" => 976, "し," => 1557, "し、" => 1557, "しい" => -3714, "した" => 3562, "して" => 1449, "しな" => 2608, "しま" => 1200, "す." => -1310, "す。" => -1310, "する" => 6521, "ず," => 3426, "ず、" => 3426, "ずに" => 841, "そう" => 428, "た." => 8875, "た。" => 8875, "たい" => -594, "たの" => 812, "たり" => -1183, "たる" => -853, "だ." => 4098, "だ。" => 4098, "だっ" => 1004, "った" => -4748, "って" => 300, "てい" => 6240, "てお" => 855, "ても" => 302, "です" => 1437, "でに" => -1482, "では" => 2295, "とう" => -1387, "とし" => 2266, "との" => 541, "とも" => -3543, "どう" => 4664, "ない" => 1796, "なく" => -903, "など" => 2135, "に," => -1021, "に、" => -1021, "にし" => 1771, "にな" => 1906, "には" => 2644, "の," => -724, "の、" => -724, "の子" => -1000, "は," => 1337, "は、" => 1337, "べき" => 2181, "まし" => 1113, "ます" => 6943, "まっ" => -1549, "まで" => 6154, "まれ" => -793, "らし" => 1479, "られ" => 6820, "るる" => 3818, "れ," => 854, "れ、" => 854, "れた" => 1850, "れて" => 1375, "れば" => -3246, "れる" => 1091, "われ" => -605, "んだ" => 606, "んで" => 798, "カ月" => 990, "会議" => 860, "入り" => 1232, "大会" => 2217, "始め" => 1681, "市" => 965, "新聞" => -5055, "日," => 974, "日、" => 974, "社会" => 2024, "カ月" => 990 } + TC1 = { "AAA" => 1093, "HHH" => 1029, "HHM" => 580, "HII" => 998, "HOH" => -390, "HOM" => -331, "IHI" => 1169, "IOH" => -142, "IOI" => -1015, "IOM" => 467, "MMH" => 187, "OOI" => -1832 } + TC2 = { "HHO" => 2088, "HII" => -1023, "HMM" => -1154, "IHI" => -1965, "KKH" => 703, "OII" => -2649 } + TC3 = { "AAA" => -294, "HHH" => 346, "HHI" => -341, "HII" => -1088, "HIK" => 731, "HOH" => -1486, "IHH" => 128, "IHI" => -3041, "IHO" => -1935, "IIH" => -825, "IIM" => -1035, "IOI" => -542, "KHH" => -1216, "KKA" => 491, "KKH" => -1217, "KOK" => -1009, "MHH" => -2694, "MHM" => -457, "MHO" => 123, "MMH" => -471, "NNH" => -1689, "NNO" => 662, "OHO" => -3393 } + TC4 = { "HHH" => -203, "HHI" => 1344, "HHK" => 365, "HHM" => -122, "HHN" => 182, "HHO" => 669, "HIH" => 804, "HII" => 679, "HOH" => 446, "IHH" => 695, "IHO" => -2324, "IIH" => 321, "III" => 1497, "IIO" => 656, "IOO" => 54, "KAK" => 4845, "KKA" => 3386, "KKK" => 3065, "MHH" => -405, "MHI" => 201, "MMH" => -241, "MMM" => 661, "MOM" => 841 } + TQ1 = { "BHHH" => -227, "BHHI" => 316, "BHIH" => -132, "BIHH" => 60, "BIII" => 1595, "BNHH" => -744, "BOHH" => 225, "BOOO" => -908, "OAKK" => 482, "OHHH" => 281, "OHIH" => 249, "OIHI" => 200, "OIIH" => -68 } + TQ2 = { "BIHH" => -1401, "BIII" => -1033, "BKAK" => -543, "BOOO" => -5591 } + TQ3 = { "BHHH" => 478, "BHHM" => -1073, "BHIH" => 222, "BHII" => -504, "BIIH" => -116, "BIII" => -105, "BMHI" => -863, "BMHM" => -464, "BOMH" => 620, "OHHH" => 346, "OHHI" => 1729, "OHII" => 997, "OHMH" => 481, "OIHH" => 623, "OIIH" => 1344, "OKAK" => 2792, "OKHH" => 587, "OKKA" => 679, "OOHH" => 110, "OOII" => -685 } + TQ4 = { "BHHH" => -721, "BHHM" => -3604, "BHII" => -966, "BIIH" => -607, "BIII" => -2181, "OAAA" => -2763, "OAKK" => 180, "OHHH" => -294, "OHHI" => 2446, "OHHO" => 480, "OHIH" => -1573, "OIHH" => 1935, "OIHI" => -493, "OIIH" => 626, "OIII" => -4007, "OKAK" => -8156 } + TW1 = { "につい" => -4681, "東京都" => 2026 } + TW2 = { "ある程" => -2049, "いった" => -1256, "ころが" => -2434, "しょう" => 3873, "その後" => -4430, "だって" => -1049, "ていた" => 1833, "として" => -4657, "ともに" => -4517, "もので" => 1882, "一気に" => -792, "初めて" => -1512, "同時に" => -8097, "大きな" => -1255, "対して" => -2721, "社会党" => -3216 } + TW3 = { "いただ" => -1734, "してい" => 1314, "として" => -4314, "につい" => -5483, "にとっ" => -5989, "に当た" => -6247, "ので," => -727, "ので、" => -727, "のもの" => -600, "れから" => -3752, "十二月" => -2287 } + TW4 = { "いう." => 8576, "いう。" => 8576, "からな" => -2348, "してい" => 2958, "たが," => 1516, "たが、" => 1516, "ている" => 1538, "という" => 1349, "ました" => 5543, "ません" => 1097, "ようと" => -4258, "よると" => 5865 } + UC1 = { "A" => 484, "K" => 93, "M" => 645, "O" => -505 } + UC2 = { "A" => 819, "H" => 1059, "I" => 409, "M" => 3987, "N" => 5775, "O" => 646 } + UC3 = { "A" => -1370, "I" => 2311 } + UC4 = { "A" => -2643, "H" => 1809, "I" => -1032, "K" => -3450, "M" => 3565, "N" => 3876, "O" => 6646 } + UC5 = { "H" => 313, "I" => -1238, "K" => -799, "M" => 539, "O" => -831 } + UC6 = { "H" => -506, "I" => -253, "K" => 87, "M" => 247, "O" => -387 } + UP1 = { "O" => -214 } + UP2 = { "B" => 69, "O" => 935 } + UP3 = { "B" => 189 } + UQ1 = { "BH" => 21, "BI" => -12, "BK" => -99, "BN" => 142, "BO" => -56, "OH" => -95, "OI" => 477, "OK" => 410, "OO" => -2422 } + UQ2 = { "BH" => 216, "BI" => 113, "OK" => 1759 } + UQ3 = { "BA" => -479, "BH" => 42, "BI" => 1913, "BK" => -7198, "BM" => 3160, "BN" => 6427, "BO" => 14761, "OI" => -827, "ON" => -3212 } + UW1 = { "," => 156, "、" => 156, "「" => -463, "あ" => -941, "う" => -127, "が" => -553, "き" => 121, "こ" => 505, "で" => -201, "と" => -547, "ど" => -123, "に" => -789, "の" => -185, "は" => -847, "も" => -466, "や" => -470, "よ" => 182, "ら" => -292, "り" => 208, "れ" => 169, "を" => -446, "ん" => -137, "・" => -135, "主" => -402, "京" => -268, "区" => -912, "午" => 871, "国" => -460, "大" => 561, "委" => 729, "市" => -411, "日" => -141, "理" => 361, "生" => -408, "県" => -386, "都" => -718, "「" => -463, "・" => -135 } + UW2 = { "," => -829, "、" => -829, "〇" => 892, "「" => -645, "」" => 3145, "あ" => -538, "い" => 505, "う" => 134, "お" => -502, "か" => 1454, "が" => -856, "く" => -412, "こ" => 1141, "さ" => 878, "ざ" => 540, "し" => 1529, "す" => -675, "せ" => 300, "そ" => -1011, "た" => 188, "だ" => 1837, "つ" => -949, "て" => -291, "で" => -268, "と" => -981, "ど" => 1273, "な" => 1063, "に" => -1764, "の" => 130, "は" => -409, "ひ" => -1273, "べ" => 1261, "ま" => 600, "も" => -1263, "や" => -402, "よ" => 1639, "り" => -579, "る" => -694, "れ" => 571, "を" => -2516, "ん" => 2095, "ア" => -587, "カ" => 306, "キ" => 568, "ッ" => 831, "三" => -758, "不" => -2150, "世" => -302, "中" => -968, "主" => -861, "事" => 492, "人" => -123, "会" => 978, "保" => 362, "入" => 548, "初" => -3025, "副" => -1566, "北" => -3414, "区" => -422, "大" => -1769, "天" => -865, "太" => -483, "子" => -1519, "学" => 760, "実" => 1023, "小" => -2009, "市" => -813, "年" => -1060, "強" => 1067, "手" => -1519, "揺" => -1033, "政" => 1522, "文" => -1355, "新" => -1682, "日" => -1815, "明" => -1462, "最" => -630, "朝" => -1843, "本" => -1650, "東" => -931, "果" => -665, "次" => -2378, "民" => -180, "気" => -1740, "理" => 752, "発" => 529, "目" => -1584, "相" => -242, "県" => -1165, "立" => -763, "第" => 810, "米" => 509, "自" => -1353, "行" => 838, "西" => -744, "見" => -3874, "調" => 1010, "議" => 1198, "込" => 3041, "開" => 1758, "間" => -1257, "「" => -645, "」" => 3145, "ッ" => 831, "ア" => -587, "カ" => 306, "キ" => 568 } + UW3 = { "," => 4889, "1" => -800, "−" => -1723, "、" => 4889, "々" => -2311, "〇" => 5827, "」" => 2670, "〓" => -3573, "あ" => -2696, "い" => 1006, "う" => 2342, "え" => 1983, "お" => -4864, "か" => -1163, "が" => 3271, "く" => 1004, "け" => 388, "げ" => 401, "こ" => -3552, "ご" => -3116, "さ" => -1058, "し" => -395, "す" => 584, "せ" => 3685, "そ" => -5228, "た" => 842, "ち" => -521, "っ" => -1444, "つ" => -1081, "て" => 6167, "で" => 2318, "と" => 1691, "ど" => -899, "な" => -2788, "に" => 2745, "の" => 4056, "は" => 4555, "ひ" => -2171, "ふ" => -1798, "へ" => 1199, "ほ" => -5516, "ま" => -4384, "み" => -120, "め" => 1205, "も" => 2323, "や" => -788, "よ" => -202, "ら" => 727, "り" => 649, "る" => 5905, "れ" => 2773, "わ" => -1207, "を" => 6620, "ん" => -518, "ア" => 551, "グ" => 1319, "ス" => 874, "ッ" => -1350, "ト" => 521, "ム" => 1109, "ル" => 1591, "ロ" => 2201, "ン" => 278, "・" => -3794, "一" => -1619, "下" => -1759, "世" => -2087, "両" => 3815, "中" => 653, "主" => -758, "予" => -1193, "二" => 974, "人" => 2742, "今" => 792, "他" => 1889, "以" => -1368, "低" => 811, "何" => 4265, "作" => -361, "保" => -2439, "元" => 4858, "党" => 3593, "全" => 1574, "公" => -3030, "六" => 755, "共" => -1880, "円" => 5807, "再" => 3095, "分" => 457, "初" => 2475, "別" => 1129, "前" => 2286, "副" => 4437, "力" => 365, "動" => -949, "務" => -1872, "化" => 1327, "北" => -1038, "区" => 4646, "千" => -2309, "午" => -783, "協" => -1006, "口" => 483, "右" => 1233, "各" => 3588, "合" => -241, "同" => 3906, "和" => -837, "員" => 4513, "国" => 642, "型" => 1389, "場" => 1219, "外" => -241, "妻" => 2016, "学" => -1356, "安" => -423, "実" => -1008, "家" => 1078, "小" => -513, "少" => -3102, "州" => 1155, "市" => 3197, "平" => -1804, "年" => 2416, "広" => -1030, "府" => 1605, "度" => 1452, "建" => -2352, "当" => -3885, "得" => 1905, "思" => -1291, "性" => 1822, "戸" => -488, "指" => -3973, "政" => -2013, "教" => -1479, "数" => 3222, "文" => -1489, "新" => 1764, "日" => 2099, "旧" => 5792, "昨" => -661, "時" => -1248, "曜" => -951, "最" => -937, "月" => 4125, "期" => 360, "李" => 3094, "村" => 364, "東" => -805, "核" => 5156, "森" => 2438, "業" => 484, "氏" => 2613, "民" => -1694, "決" => -1073, "法" => 1868, "海" => -495, "無" => 979, "物" => 461, "特" => -3850, "生" => -273, "用" => 914, "町" => 1215, "的" => 7313, "直" => -1835, "省" => 792, "県" => 6293, "知" => -1528, "私" => 4231, "税" => 401, "立" => -960, "第" => 1201, "米" => 7767, "系" => 3066, "約" => 3663, "級" => 1384, "統" => -4229, "総" => 1163, "線" => 1255, "者" => 6457, "能" => 725, "自" => -2869, "英" => 785, "見" => 1044, "調" => -562, "財" => -733, "費" => 1777, "車" => 1835, "軍" => 1375, "込" => -1504, "通" => -1136, "選" => -681, "郎" => 1026, "郡" => 4404, "部" => 1200, "金" => 2163, "長" => 421, "開" => -1432, "間" => 1302, "関" => -1282, "雨" => 2009, "電" => -1045, "非" => 2066, "駅" => 1620, "1" => -800, "」" => 2670, "・" => -3794, "ッ" => -1350, "ア" => 551, "グ" => 1319, "ス" => 874, "ト" => 521, "ム" => 1109, "ル" => 1591, "ロ" => 2201, "ン" => 278 } + UW4 = { "," => 3930, "." => 3508, "―" => -4841, "、" => 3930, "。" => 3508, "〇" => 4999, "「" => 1895, "」" => 3798, "〓" => -5156, "あ" => 4752, "い" => -3435, "う" => -640, "え" => -2514, "お" => 2405, "か" => 530, "が" => 6006, "き" => -4482, "ぎ" => -3821, "く" => -3788, "け" => -4376, "げ" => -4734, "こ" => 2255, "ご" => 1979, "さ" => 2864, "し" => -843, "じ" => -2506, "す" => -731, "ず" => 1251, "せ" => 181, "そ" => 4091, "た" => 5034, "だ" => 5408, "ち" => -3654, "っ" => -5882, "つ" => -1659, "て" => 3994, "で" => 7410, "と" => 4547, "な" => 5433, "に" => 6499, "ぬ" => 1853, "ね" => 1413, "の" => 7396, "は" => 8578, "ば" => 1940, "ひ" => 4249, "び" => -4134, "ふ" => 1345, "へ" => 6665, "べ" => -744, "ほ" => 1464, "ま" => 1051, "み" => -2082, "む" => -882, "め" => -5046, "も" => 4169, "ゃ" => -2666, "や" => 2795, "ょ" => -1544, "よ" => 3351, "ら" => -2922, "り" => -9726, "る" => -14896, "れ" => -2613, "ろ" => -4570, "わ" => -1783, "を" => 13150, "ん" => -2352, "カ" => 2145, "コ" => 1789, "セ" => 1287, "ッ" => -724, "ト" => -403, "メ" => -1635, "ラ" => -881, "リ" => -541, "ル" => -856, "ン" => -3637, "・" => -4371, "ー" => -11870, "一" => -2069, "中" => 2210, "予" => 782, "事" => -190, "井" => -1768, "人" => 1036, "以" => 544, "会" => 950, "体" => -1286, "作" => 530, "側" => 4292, "先" => 601, "党" => -2006, "共" => -1212, "内" => 584, "円" => 788, "初" => 1347, "前" => 1623, "副" => 3879, "力" => -302, "動" => -740, "務" => -2715, "化" => 776, "区" => 4517, "協" => 1013, "参" => 1555, "合" => -1834, "和" => -681, "員" => -910, "器" => -851, "回" => 1500, "国" => -619, "園" => -1200, "地" => 866, "場" => -1410, "塁" => -2094, "士" => -1413, "多" => 1067, "大" => 571, "子" => -4802, "学" => -1397, "定" => -1057, "寺" => -809, "小" => 1910, "屋" => -1328, "山" => -1500, "島" => -2056, "川" => -2667, "市" => 2771, "年" => 374, "庁" => -4556, "後" => 456, "性" => 553, "感" => 916, "所" => -1566, "支" => 856, "改" => 787, "政" => 2182, "教" => 704, "文" => 522, "方" => -856, "日" => 1798, "時" => 1829, "最" => 845, "月" => -9066, "木" => -485, "来" => -442, "校" => -360, "業" => -1043, "氏" => 5388, "民" => -2716, "気" => -910, "沢" => -939, "済" => -543, "物" => -735, "率" => 672, "球" => -1267, "生" => -1286, "産" => -1101, "田" => -2900, "町" => 1826, "的" => 2586, "目" => 922, "省" => -3485, "県" => 2997, "空" => -867, "立" => -2112, "第" => 788, "米" => 2937, "系" => 786, "約" => 2171, "経" => 1146, "統" => -1169, "総" => 940, "線" => -994, "署" => 749, "者" => 2145, "能" => -730, "般" => -852, "行" => -792, "規" => 792, "警" => -1184, "議" => -244, "谷" => -1000, "賞" => 730, "車" => -1481, "軍" => 1158, "輪" => -1433, "込" => -3370, "近" => 929, "道" => -1291, "選" => 2596, "郎" => -4866, "都" => 1192, "野" => -1100, "銀" => -2213, "長" => 357, "間" => -2344, "院" => -2297, "際" => -2604, "電" => -878, "領" => -1659, "題" => -792, "館" => -1984, "首" => 1749, "高" => 2120, "「" => 1895, "」" => 3798, "・" => -4371, "ッ" => -724, "ー" => -11870, "カ" => 2145, "コ" => 1789, "セ" => 1287, "ト" => -403, "メ" => -1635, "ラ" => -881, "リ" => -541, "ル" => -856, "ン" => -3637 } + UW5 = { "," => 465, "." => -299, "1" => -514, "E2" => -32768, "]" => -2762, "、" => 465, "。" => -299, "「" => 363, "あ" => 1655, "い" => 331, "う" => -503, "え" => 1199, "お" => 527, "か" => 647, "が" => -421, "き" => 1624, "ぎ" => 1971, "く" => 312, "げ" => -983, "さ" => -1537, "し" => -1371, "す" => -852, "だ" => -1186, "ち" => 1093, "っ" => 52, "つ" => 921, "て" => -18, "で" => -850, "と" => -127, "ど" => 1682, "な" => -787, "に" => -1224, "の" => -635, "は" => -578, "べ" => 1001, "み" => 502, "め" => 865, "ゃ" => 3350, "ょ" => 854, "り" => -208, "る" => 429, "れ" => 504, "わ" => 419, "を" => -1264, "ん" => 327, "イ" => 241, "ル" => 451, "ン" => -343, "中" => -871, "京" => 722, "会" => -1153, "党" => -654, "務" => 3519, "区" => -901, "告" => 848, "員" => 2104, "大" => -1296, "学" => -548, "定" => 1785, "嵐" => -1304, "市" => -2991, "席" => 921, "年" => 1763, "思" => 872, "所" => -814, "挙" => 1618, "新" => -1682, "日" => 218, "月" => -4353, "査" => 932, "格" => 1356, "機" => -1508, "氏" => -1347, "田" => 240, "町" => -3912, "的" => -3149, "相" => 1319, "省" => -1052, "県" => -4003, "研" => -997, "社" => -278, "空" => -813, "統" => 1955, "者" => -2233, "表" => 663, "語" => -1073, "議" => 1219, "選" => -1018, "郎" => -368, "長" => 786, "間" => 1191, "題" => 2368, "館" => -689, "1" => -514, "E2" => -32768, "「" => 363, "イ" => 241, "ル" => 451, "ン" => -343 } + UW6 = { "," => 227, "." => 808, "1" => -270, "E1" => 306, "、" => 227, "。" => 808, "あ" => -307, "う" => 189, "か" => 241, "が" => -73, "く" => -121, "こ" => -200, "じ" => 1782, "す" => 383, "た" => -428, "っ" => 573, "て" => -1014, "で" => 101, "と" => -105, "な" => -253, "に" => -149, "の" => -417, "は" => -236, "も" => -206, "り" => 187, "る" => -135, "を" => 195, "ル" => -673, "ン" => -496, "一" => -277, "中" => 201, "件" => -800, "会" => 624, "前" => 302, "区" => 1792, "員" => -1212, "委" => 798, "学" => -960, "市" => 887, "広" => -695, "後" => 535, "業" => -697, "相" => 753, "社" => -507, "福" => 974, "空" => -822, "者" => 1811, "連" => 463, "郎" => 1082, "1" => -270, "E1" => 306, "ル" => -673, "ン" => -496 } + + class << self + def segment(text) + return [] if text.nil? || text.strip.length == 0 + + result = [] + + segments = ["B3", "B2", "B1"] + ctypes = ["O", "O", "O"] + + text.chars.each do |char| + segments << char + ctypes << ctype(char) + end + + segments.concat(["E1", "E2", "E3"]) + ctypes.concat(["O", "O", "O"]) + + word = segments[3] + p1 = "U" + p2 = "U" + p3 = "U" + + 4.upto(segments.size - 4) do |i| + score = BIAS + w1 = segments[i - 3] + w2 = segments[i - 2] + w3 = segments[i - 1] + w4 = segments[i] + w5 = segments[i + 1] + w6 = segments[i + 2] + c1 = ctypes[i - 3] + c2 = ctypes[i - 2] + c3 = ctypes[i - 1] + c4 = ctypes[i] + c5 = ctypes[i + 1] + c6 = ctypes[i + 2] + score += UP1[p1].to_i + score += UP2[p2].to_i + score += UP3[p3].to_i + score += BP1[p1 + p2].to_i + score += BP2[p2 + p3].to_i + score += UW1[w1].to_i + score += UW2[w2].to_i + score += UW3[w3].to_i + score += UW4[w4].to_i + score += UW5[w5].to_i + score += UW6[w6].to_i + score += BW1[w2 + w3].to_i + score += BW2[w3 + w4].to_i + score += BW3[w4 + w5].to_i + score += TW1[w1 + w2 + w3].to_i + score += TW2[w2 + w3 + w4].to_i + score += TW3[w3 + w4 + w5].to_i + score += TW4[w4 + w5 + w6].to_i + score += UC1[c1].to_i + score += UC2[c2].to_i + score += UC3[c3].to_i + score += UC4[c4].to_i + score += UC5[c5].to_i + score += UC6[c6].to_i + score += BC1[c2 + c3].to_i + score += BC2[c3 + c4].to_i + score += BC3[c4 + c5].to_i + score += TC1[c1 + c2 + c3].to_i + score += TC2[c2 + c3 + c4].to_i + score += TC3[c3 + c4 + c5].to_i + score += TC4[c4 + c5 + c6].to_i + # score += TC5[c4 + c5 + c6].to_i + score += UQ1[p1 + c1].to_i + score += UQ2[p2 + c2].to_i + score += UQ3[p3 + c3].to_i + score += BQ1[p2 + c2 + c3].to_i + score += BQ2[p2 + c3 + c4].to_i + score += BQ3[p3 + c2 + c3].to_i + score += BQ4[p3 + c3 + c4].to_i + score += TQ1[p2 + c1 + c2 + c3].to_i + score += TQ2[p2 + c2 + c3 + c4].to_i + score += TQ3[p3 + c1 + c2 + c3].to_i + score += TQ4[p3 + c2 + c3 + c4].to_i + + p = "O" + + if score > 0 + result.push(word) + word = "" + p = "B" + end + + p1 = p2 + p2 = p3 + p3 = p + word += segments[i] + end + + result.push(word) + + result + end + + private + + def ctype(text) + CHARTYPE.each do |regexp, value| + if text.match(regexp) + return value + end + end + + "O" + end + end +end diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb index 1211c84fe7..9119ec9551 100644 --- a/lib/topic_creator.rb +++ b/lib/topic_creator.rb @@ -111,7 +111,7 @@ class TopicCreator visible: @opts[:visible] } - [:subtype, :archetype, :meta_data, :import_mode].each do |key| + [:subtype, :archetype, :meta_data, :import_mode, :advance_draft].each do |key| topic_params[key] = @opts[key] if @opts[key].present? end @@ -137,6 +137,7 @@ class TopicCreator topic_params[:created_at] = convert_time(@opts[:created_at]) if @opts[:created_at].present? topic_params[:pinned_at] = convert_time(@opts[:pinned_at]) if @opts[:pinned_at].present? topic_params[:pinned_globally] = @opts[:pinned_globally] if @opts[:pinned_globally].present? + topic_params[:external_id] = @opts[:external_id] if @opts[:external_id].present? topic_params[:featured_link] = @opts[:featured_link] topic_params diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 3175a6b1cf..324cfed547 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -75,6 +75,7 @@ class TopicQuery guardian no_definitions destination_category_id + include_all_pms include_pms) end @@ -709,8 +710,12 @@ class TopicQuery all_listable_topics = @guardian.filter_allowed_categories(Topic.unscoped.listable_topics) - if options[:include_pms] - all_pm_topics = Topic.unscoped.private_messages_for_user(@user) + if options[:include_pms] || options[:include_all_pms] + all_pm_topics = if options[:include_all_pms] && @guardian.is_admin? + Topic.unscoped.private_messages + else + Topic.unscoped.private_messages_for_user(@user) + end result = result.merge(all_listable_topics.or(all_pm_topics)) else result = result.merge(all_listable_topics) diff --git a/lib/twitter_api.rb b/lib/twitter_api.rb index 5664bb2a26..85155dd2e3 100644 --- a/lib/twitter_api.rb +++ b/lib/twitter_api.rb @@ -95,28 +95,17 @@ class TwitterApi protected def link_handles_in(text) - text.scan(/(?:^|\s)@(\w+)/).flatten.uniq.each do |handle| - text.gsub!(/(?:^|\s)@#{handle}/, [ - " ", - "@#{handle}", - "" - ].join) - end - - text.strip + text.gsub(/(?:^|\s)@\w+/) do |match| + handle = match.strip[1..] + "@#{handle}" + end.strip end def link_hashtags_in(text) - text.scan(/(?:^|\s)#(\w+)/).flatten.uniq.each do |hashtag| - text.gsub!(/(?:^|\s)##{hashtag}/, [ - " ", - "##{hashtag}", - "" - ].join) - end - - text.strip + text.gsub(/(?:^|\s)#\w+/) do |match| + hashtag = match.strip[1..] + "##{hashtag}" + end.strip end def user_timeline_uri_for(screen_name) diff --git a/lib/validators/host_list_setting_validator.rb b/lib/validators/host_list_setting_validator.rb new file mode 100644 index 0000000000..93d51aa2b0 --- /dev/null +++ b/lib/validators/host_list_setting_validator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class HostListSettingValidator + def initialize(opts = {}) + @opts = opts + end + + def valid_value?(val) + val.exclude?("*") && val.exclude?("?") + end + + def error_message + I18n.t('site_settings.errors.invalid_domain_hostname') + end +end diff --git a/lib/validators/search_tokenize_chinese_validator.rb b/lib/validators/search_tokenize_chinese_validator.rb new file mode 100644 index 0000000000..0cda74ea10 --- /dev/null +++ b/lib/validators/search_tokenize_chinese_validator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class SearchTokenizeChineseValidator + def initialize(opts = {}) + end + + def valid_value?(value) + !SiteSetting.search_tokenize_japanese + end + + def error_message + I18n.t("site_settings.errors.search_tokenize_japanese_enabled") + end +end diff --git a/lib/validators/search_tokenize_japanese_validator.rb b/lib/validators/search_tokenize_japanese_validator.rb new file mode 100644 index 0000000000..7ad07a9c88 --- /dev/null +++ b/lib/validators/search_tokenize_japanese_validator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class SearchTokenizeJapaneseValidator + def initialize(opts = {}) + end + + def valid_value?(value) + !SiteSetting.search_tokenize_chinese + end + + def error_message + I18n.t("site_settings.errors.search_tokenize_chinese_enabled") + end +end diff --git a/lib/version.rb b/lib/version.rb index b8674f62cb..d9e7ad4b35 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -10,7 +10,7 @@ module Discourse MAJOR = 2 MINOR = 9 TINY = 0 - PRE = 'beta1' + PRE = 'beta2' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/lib/webauthn.rb b/lib/webauthn.rb index 4734b660ca..089913f31c 100644 --- a/lib/webauthn.rb +++ b/lib/webauthn.rb @@ -10,7 +10,7 @@ module Webauthn # -7 - ES256 # -257 - RS256 (Windows Hello supported alg.) - SUPPORTED_ALGORITHMS = [-7, -257].freeze + SUPPORTED_ALGORITHMS = COSE::Algorithm.registered_algorithm_ids.freeze VALID_ATTESTATION_FORMATS = ['none', 'packed', 'fido-u2f'].freeze class SecurityKeyError < StandardError; end diff --git a/package.json b/package.json index 366bd58e0e..d4a6629fa6 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "license": "GPL-2.0-only", "dependencies": { "@discourse/itsatrap": "^2.0.10", - "@fortawesome/fontawesome-free": "5.11.2", + "@fortawesome/fontawesome-free": "5.15.4", + "@discourse/moment-timezone-names-translations": "^1.0.0", "@highlightjs/cdn-assets": "^10.7.0", "@json-editor/json-editor": "^2.6.1", "@popperjs/core": "v2.10.2", @@ -18,7 +19,6 @@ "@uppy/utils": "^4.0.3", "@uppy/xhr-upload": "^2.0.4", "ace-builds": "1.4.13", - "blueimp-file-upload": "10.13.0", "bootbox": "3.2.0", "bootstrap": "v3.4.1", "chart.js": "3.5.1", @@ -31,9 +31,7 @@ "markdown-it": "10.0.0", "moment": "2.29.1", "moment-timezone": "0.5.31", - "moment-timezone-names-translations": "https://github.com/discourse/moment-timezone-names-translations", "pikaday": "1.8.0", - "spectrum-colorpicker": "1.8.0", "workbox-cacheable-response": "^4.3.1", "workbox-core": "^4.3.1", "workbox-expiration": "^4.3.1", @@ -45,7 +43,7 @@ "@arkweid/lefthook": "^0.7.2", "@mixer/parallel-prettier": "^2.0.1", "browserify": "^17.0.0", - "chrome-launcher": "^0.12.0", + "chrome-launcher": "^0.14.2", "chrome-remote-interface": "^0.25", "lodash-cli": "https://github.com/lodash-archive/lodash-cli.git", "pretender": "^3.4.7", diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js index 46dee48eee..c2a2f5f6a4 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js @@ -205,7 +205,7 @@ export default Component.extend({ @computed("currentUserTimezone") formatedCurrentUserTimezone(timezone) { - return timezone.replace("_", " ").replace("Etc/", "").split("/"); + return timezone.replace("_", " ").replace("Etc/", "").replace("/", ", "); }, @computed("formats") @@ -398,8 +398,10 @@ export default Component.extend({ return new Promise((resolve) => { loadScript("/javascripts/pikaday.js").then(() => { const options = { - field: this.$(`.fake-input`)[0], - container: this.$(`#picker-container-${this.elementId}`)[0], + field: this.element.querySelector(".fake-input"), + container: this.element.querySelector( + `#picker-container-${this.elementId}` + ), bound: false, format: "YYYY-MM-DD", reposition: false, diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs index 196fb791c9..847a3b31cd 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs @@ -7,7 +7,8 @@ {{#if isValid}} {{#if timezoneIsDifferentFromUserTimezone}}
- {{formatedCurrentUserTimezone}} {{currentPreview}} + {{i18n "discourse_local_dates.create.form.current_timezone"}} + {{formatedCurrentUserTimezone}}{{currentPreview}}
{{/if}} {{else}} diff --git a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss index 9f2543d22c..ee45d9826e 100644 --- a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss +++ b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss @@ -262,6 +262,7 @@ b { margin-right: 0.5em; + margin-left: 0.5em; } b + p { diff --git a/plugins/discourse-local-dates/config/locales/client.de.yml b/plugins/discourse-local-dates/config/locales/client.de.yml index 056ade23da..6d8741e350 100644 --- a/plugins/discourse-local-dates/config/locales/client.de.yml +++ b/plugins/discourse-local-dates/config/locales/client.de.yml @@ -31,6 +31,7 @@ de: format_title: Datumsformat timezone: Zeitzone until: Bis … + current_timezone: "Aktuelle Zeitzone:" recurring: every_day: "Jeden Tag" every_week: "Jede Woche" diff --git a/plugins/discourse-local-dates/config/locales/client.en.yml b/plugins/discourse-local-dates/config/locales/client.en.yml index a968b96f87..a18bddd976 100644 --- a/plugins/discourse-local-dates/config/locales/client.en.yml +++ b/plugins/discourse-local-dates/config/locales/client.en.yml @@ -25,6 +25,7 @@ en: format_title: Date format timezone: Timezone until: Until... + current_timezone: "Current timezone:" recurring: every_day: "Every day" every_week: "Every week" diff --git a/plugins/discourse-local-dates/config/locales/client.fa_IR.yml b/plugins/discourse-local-dates/config/locales/client.fa_IR.yml index 58afdba006..ff44b6205b 100644 --- a/plugins/discourse-local-dates/config/locales/client.fa_IR.yml +++ b/plugins/discourse-local-dates/config/locales/client.fa_IR.yml @@ -28,6 +28,7 @@ fa_IR: format_title: فرمت تاریخ timezone: منطقه‌ی زمانی until: تا وقتی که... + current_timezone: "منطقه‌ زمانی فعلی:" recurring: every_day: "هر روز" every_week: "هر هفته" diff --git a/plugins/discourse-local-dates/config/locales/client.he.yml b/plugins/discourse-local-dates/config/locales/client.he.yml index e87699d57a..2d7babd2eb 100644 --- a/plugins/discourse-local-dates/config/locales/client.he.yml +++ b/plugins/discourse-local-dates/config/locales/client.he.yml @@ -31,6 +31,7 @@ he: format_title: מבנה תאריך timezone: אזור זמן until: עד… + current_timezone: "אזור זמן נוכחי:" recurring: every_day: "כל יום" every_week: "כל שבוע" diff --git a/plugins/discourse-local-dates/config/locales/client.hu.yml b/plugins/discourse-local-dates/config/locales/client.hu.yml index 373cfbea76..7e6c9ae9b6 100644 --- a/plugins/discourse-local-dates/config/locales/client.hu.yml +++ b/plugins/discourse-local-dates/config/locales/client.hu.yml @@ -31,6 +31,7 @@ hu: format_title: Dátumformátum timezone: Időzóna until: Eddig… + current_timezone: "Jelenlegi időzóna:" recurring: every_day: "Naponta" every_week: "Hetente" diff --git a/plugins/discourse-local-dates/config/locales/client.pl_PL.yml b/plugins/discourse-local-dates/config/locales/client.pl_PL.yml index 45c4d5d196..0e267b9cd6 100644 --- a/plugins/discourse-local-dates/config/locales/client.pl_PL.yml +++ b/plugins/discourse-local-dates/config/locales/client.pl_PL.yml @@ -31,6 +31,7 @@ pl_PL: format_title: Format daty timezone: Strefa czasowa until: Do… + current_timezone: "Aktualna strefa czasowa:" recurring: every_day: "Codziennie" every_week: "Co tydzień" diff --git a/plugins/discourse-local-dates/config/locales/client.ru.yml b/plugins/discourse-local-dates/config/locales/client.ru.yml index c31d523a26..17beb2886b 100644 --- a/plugins/discourse-local-dates/config/locales/client.ru.yml +++ b/plugins/discourse-local-dates/config/locales/client.ru.yml @@ -31,6 +31,7 @@ ru: format_title: Формат даты timezone: Часовой пояс until: По... + current_timezone: "Текущий часовой пояс:" recurring: every_day: "Каждый день" every_week: "Каждую неделю" diff --git a/plugins/discourse-local-dates/config/locales/client.sv.yml b/plugins/discourse-local-dates/config/locales/client.sv.yml index 12de3f19c7..20f84a771b 100644 --- a/plugins/discourse-local-dates/config/locales/client.sv.yml +++ b/plugins/discourse-local-dates/config/locales/client.sv.yml @@ -31,6 +31,7 @@ sv: format_title: Datumformat timezone: Tidszon until: Tills... + current_timezone: "Aktuell tidszon:" recurring: every_day: "Varje dag" every_week: "Varje vecka" diff --git a/plugins/discourse-local-dates/config/locales/client.tr_TR.yml b/plugins/discourse-local-dates/config/locales/client.tr_TR.yml index a03042cc0f..42800c7511 100644 --- a/plugins/discourse-local-dates/config/locales/client.tr_TR.yml +++ b/plugins/discourse-local-dates/config/locales/client.tr_TR.yml @@ -31,6 +31,7 @@ tr_TR: format_title: Tarih formatı timezone: Saat dilimi until: A kadar... + current_timezone: "Geçerli saat dilimi:" recurring: every_day: "Her gün" every_week: "Her hafta" diff --git a/plugins/discourse-narrative-bot/config/locales/client.nb_NO.yml b/plugins/discourse-narrative-bot/config/locales/client.nb_NO.yml index aa33db00e2..043b4b28b7 100644 --- a/plugins/discourse-narrative-bot/config/locales/client.nb_NO.yml +++ b/plugins/discourse-narrative-bot/config/locales/client.nb_NO.yml @@ -8,5 +8,5 @@ nb_NO: js: discourse_narrative_bot: welcome_post_type: - new_user_track: "Start den nye brukerveiledningen for alle nye brukere" - welcome_message: "Send alle nye brukere en velkomstmelding med en rask startguide for bruk av forumet." + new_user_track: "Start den nye brukerveiledningen for alle nye brukere?" + welcome_message: "Send alle nye brukere en velkomstmelding med en rask startguide for bruk av forumet?" diff --git a/plugins/discourse-narrative-bot/config/locales/server.da.yml b/plugins/discourse-narrative-bot/config/locales/server.da.yml index 3354a3fa4b..6504bfd8e2 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.da.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.da.yml @@ -84,6 +84,15 @@ da: "10": quote: "Uanset hvad menneskets sind kan begribe og tro på, kan det opnå." author: "Napoleon Hill" + "11": + quote: "Fred derhjemme, fred i verden." + author: "Mustafa Kemal Atatürk" + "12": + quote: "Uddannelse er ikke en måde at slippe ud af fattigdom, det er en måde at bekæmpe den." + author: "Julius Nyerere" + "13": + quote: "En rejse på tusind km. skal begynde med et enkelt trin." + author: "Lao Tzu" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.de.yml b/plugins/discourse-narrative-bot/config/locales/server.de.yml index 22392e160f..ae0a667619 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.de.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.de.yml @@ -84,6 +84,15 @@ de: "10": quote: "Was der Verstand eines Menschen begreifen und glauben kann, kann er erreichen." author: "Napoleon Hill" + "11": + quote: "Frieden zu Hause, Frieden in der Welt." + author: "Mustafa Kemal Atatürk" + "12": + quote: "Bildung ist kein Weg, der Armut zu entkommen, sondern ein Weg, sie zu bekämpfen." + author: "Julius Nyerere" + "13": + quote: "Eine Reise von tausend Meilen muss mit einem einzigen Schritt beginnen." + author: "Lao Tzu" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.en.yml b/plugins/discourse-narrative-bot/config/locales/server.en.yml index 9a5714d97f..cf69370c2a 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.en.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.en.yml @@ -84,6 +84,15 @@ en: "10": quote: "Whatever the mind of man can conceive and believe, it can achieve." author: "Napoleon Hill" + "11": + quote: "Peace at home, peace in the world." + author: "Mustafa Kemal Atatürk" + "12": + quote: "Education is not a way of escaping poverty, it is a way of fighting it." + author: "Julius Nyerere" + "13": + quote: "A journey of a thousand miles must begin with a single step." + author: "Lao Tzu" results: |- > :left_speech_bubble: _%{quote}_ — %{author} diff --git a/plugins/discourse-narrative-bot/config/locales/server.he.yml b/plugins/discourse-narrative-bot/config/locales/server.he.yml index 1984cb4543..034102b59f 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.he.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.he.yml @@ -86,6 +86,15 @@ he: "10": quote: "מה ששכל האדם מסוגל להגות ולהאמין, ניתן להשגה." author: "נפוליאון היל" + "11": + quote: "שלום בבית, שלום בעולם." + author: "מוסטפא כמאל אטאטורק" + "12": + quote: "חינוך זאת אינה הדרך להימלט מעוני, זו הדרך להילחם בו." + author: "ג׳וליוס נייררה" + "13": + quote: "מסע של אלף קילומטרים חייב להתחיל בצעד אחד." + author: "לאו דזה" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.hu.yml b/plugins/discourse-narrative-bot/config/locales/server.hu.yml index d8e5e1af84..d7ae2056a3 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.hu.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.hu.yml @@ -84,6 +84,15 @@ hu: "10": quote: "Amit az emberi elme el tud képzelni és hinni, elérheti." author: "Napoleon Hill" + "11": + quote: "Béke otthon, béke a világban." + author: "Mustafa Kemal Atatürk" + "12": + quote: "Az oktatás nem a szegénység előli menekülés, hanem az ellene folytatott küzdelem módja." + author: "Julius Nyerere" + "13": + quote: "Az ezer mérföldes utazásnak is egyetlen lépéssel kell kezdődnie." + author: "Lao-ce" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.it.yml b/plugins/discourse-narrative-bot/config/locales/server.it.yml index a2c2f76e3b..75d39d4083 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.it.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.it.yml @@ -84,6 +84,15 @@ it: "10": quote: "Tutto ciò che la mente umana può concepire e credere, può essere realizzato." author: "Napoleon Hill" + "11": + quote: "Pace in casa, pace nel mondo." + author: "Mustafa Kemal Atatürk" + "12": + quote: "L'istruzione non è un modo per sfuggire alla povertà, è un modo per combatterla." + author: "Julius Nyerere" + "13": + quote: "Un viaggio di mille miglia deve iniziare con un solo passo." + author: "Lao Tzu" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml b/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml index f81b54c34e..77169ef40c 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.nb_NO.yml @@ -6,9 +6,9 @@ nb_NO: site_settings: - discourse_narrative_bot_enabled: "Aktiver Discourse Narrativ Bot (discobot)" - disable_discourse_narrative_bot_welcome_post: "Deaktiver velkomstinnlegget fra Discourse Fortellerbot" - discourse_narrative_bot_ignored_usernames: "Brukernavn som Discourse Fortellerbot skal ignorere" + discourse_narrative_bot_enabled: "Aktiver Discourse Narrative Bot (discobot)" + disable_discourse_narrative_bot_welcome_post: "Deaktiver velkomst innlegg fra Discourse Fortellerbot" + discourse_narrative_bot_ignored_usernames: "Brukernavn som Discourse Narrative bot skal ignorere" discourse_narrative_bot_disable_public_replies: "Deaktiver åpne svar fra Discourse Fortellerbot" discourse_narrative_bot_welcome_post_type: "Type velkomstinnlegg som Discourse Fortellerbot skal sende ut" discourse_narrative_bot_welcome_post_delay: "Vent (n) sekunder før Discourse Fortellerbot sender ut velkomstinnlegg." diff --git a/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml b/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml index b4224af208..37a1448c3c 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.pl_PL.yml @@ -86,6 +86,12 @@ pl_PL: "10": quote: "Każda rzecz, którą umysł ludzki może pojąć i w nią uwierzyć, może on osiągnąć." author: "Napoleon Hill" + "11": + quote: "Pokój w domu, pokój na świecie." + "12": + quote: "Edukacja nie jest sposobem na wyrwanie się z ubóstwa, to jest sposób na walkę z nim." + "13": + quote: "Podróż o długości tysiąca mil musi rozpocząć się od jednego kroku." results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.ru.yml b/plugins/discourse-narrative-bot/config/locales/server.ru.yml index 753932dc55..88724296ca 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ru.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ru.yml @@ -86,6 +86,15 @@ ru: "10": quote: "Что разум человека способен познать и вообразить, того он способен достичь." author: "Наполеон Хилл" + "11": + quote: "Мир в доме - мир на Земле." + author: "Мустафа Кемаль Ататюрк" + "12": + quote: "Образование – это не способ избежать бедности, это способ борьбы с ней." + author: "Джулиус Ньерере" + "13": + quote: "Путешествие в тысячу миль должно начинаться с одного шага." + author: "Лао-цзы" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.sv.yml b/plugins/discourse-narrative-bot/config/locales/server.sv.yml index 1e0e66dd53..50c1fe8695 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sv.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sv.yml @@ -84,6 +84,15 @@ sv: "10": quote: "Oavsett vad människans sinne kan tänka och tro, kan det uppnås." author: "Napoleon Hill" + "11": + quote: "Fred hemma, fred i världen." + author: "Mustafa Kemal Atatürk" + "12": + quote: "Utbildning är inte ett sätt att undkomma fattigdomen, det är ett sätt att bekämpa den." + author: "Julius Nyerere" + "13": + quote: "En resa på tusen mil måste börja med ett enda steg." + author: "Lao Tzu" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml index 11ecc65e57..7dfc10db63 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml @@ -84,6 +84,15 @@ tr_TR: "10": quote: "İnsan zihni ne olursa olsun düşünebilir ve inanabilir, başarabilir." author: "Napoleon Hill" + "11": + quote: "Yurtta sulh cihanda sulh." + author: "Mustafa Kemal Atatürk" + "12": + quote: "Eğitim, yoksulluktan kurtulmanın bir yolu değil, onunla savaşmanın bir yoludur." + author: "Julius Nyerere" + "13": + quote: "Bin millik bir yolculuk tek bir adımla başlar." + author: "Lao Tzu" results: |- > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: diff --git a/plugins/discourse-narrative-bot/plugin.rb b/plugins/discourse-narrative-bot/plugin.rb index 2748dc9bc5..2eba865b37 100644 --- a/plugins/discourse-narrative-bot/plugin.rb +++ b/plugins/discourse-narrative-bot/plugin.rb @@ -223,7 +223,7 @@ after_initialize do Jobs.enqueue(:bot_input, user_id: user.id, post_id: post.id, - input: :reply + input: "reply" ) end end @@ -233,7 +233,7 @@ after_initialize do Jobs.enqueue(:bot_input, user_id: post.user.id, post_id: post.id, - input: :edit + input: "edit" ) end end @@ -244,7 +244,7 @@ after_initialize do user_id: user.id, post_id: post.id, topic_id: post.topic_id, - input: :delete + input: "delete" ) end end @@ -254,7 +254,7 @@ after_initialize do Jobs.enqueue(:bot_input, user_id: user.id, post_id: post.id, - input: :recover + input: "recover" ) end end @@ -264,11 +264,11 @@ after_initialize do input = case self.post_action_type_id when *PostActionType.flag_types.values - self.post_action_type_id == PostActionType.types[:inappropriate] ? :flag : :reply + self.post_action_type_id == PostActionType.types[:inappropriate] ? "flag" : "reply" when PostActionType.types[:like] - :like + "like" when PostActionType.types[:bookmark] - :bookmark + "bookmark" end if input @@ -283,7 +283,7 @@ after_initialize do self.add_model_callback(Bookmark, :after_commit, on: :create) do if self.post && self.user.enqueue_narrative_bot_job? - Jobs.enqueue(:bot_input, user_id: self.user_id, post_id: self.post_id, input: :bookmark) + Jobs.enqueue(:bot_input, user_id: self.user_id, post_id: self.post_id, input: "bookmark") end end @@ -294,7 +294,7 @@ after_initialize do Jobs.enqueue(:bot_input, user_id: user_id, topic_id: topic_id, - input: :topic_notification_level_changed + input: "topic_notification_level_changed" ) end end diff --git a/plugins/poll/config/locales/server.es.yml b/plugins/poll/config/locales/server.es.yml index 3956cf9107..7c20266e1e 100644 --- a/plugins/poll/config/locales/server.es.yml +++ b/plugins/poll/config/locales/server.es.yml @@ -44,6 +44,9 @@ es: max_vote_per_user: one: Votar %{count} vez en esta encuesta. other: Solo puedes votar %{count} veces en esta encuesta. + min_vote_per_user: + one: En esta encuesta hace falta %{count} voto. + other: En esta encuesta hacen falta %{count} votos. topic_must_be_open_to_toggle_status: "El tema debe estar abierto para cambiar el estado." only_staff_or_op_can_toggle_status: "Solo un miembro del equipo o la persona que realizó la publicación original pueden cambiar el estado de una encuesta." insufficient_rights_to_create: "No tienes permitido crear encuestas." diff --git a/plugins/poll/config/locales/server.pl_PL.yml b/plugins/poll/config/locales/server.pl_PL.yml index 555de3b547..317935f0cf 100644 --- a/plugins/poll/config/locales/server.pl_PL.yml +++ b/plugins/poll/config/locales/server.pl_PL.yml @@ -44,6 +44,17 @@ pl_PL: user_cant_post_in_topic: "Nie możesz głosować, bo nie możesz odpowiadać w tym temacie." topic_must_be_open_to_vote: "Tematy musi być otwarta aby głosować." poll_must_be_open_to_vote: "Ankieta musi być otwarta aby głosować." + one_vote_per_user: "W tej ankiecie można oddać tylko 1 głos." + max_vote_per_user: + one: Tylko %{count} głos jest dozwolony w tej ankiecie. + few: Tylko %{count} głosy są dozwolone w tej ankiecie. + many: Tylko %{count} głosów jest dozwolonych w tej ankiecie. + other: W tej ankiecie można oddać maksymalnie %{count} głosów. + min_vote_per_user: + one: Do tej ankiety wymagany jest co najmniej %{count} głos. + few: Do tej ankiety wymagane są co najmniej %{count} głosy. + many: Do tej ankiety wymagane jest co najmniej %{count} głosów. + other: Do tej ankiety wymagane jest co najmniej %{count} głosów. topic_must_be_open_to_toggle_status: "Ankieta musi być otwarta aby zmienić status ankiety." only_staff_or_op_can_toggle_status: "Status ankiety może być zmieniony przez autora wpisu lub członka załogi serwisu." insufficient_rights_to_create: "Nie masz zezwolenia na tworzenie ankiet." diff --git a/plugins/poll/spec/lib/pretty_text_spec.rb b/plugins/poll/spec/lib/pretty_text_spec.rb index 5770f804f3..0f6f870fe7 100644 --- a/plugins/poll/spec/lib/pretty_text_spec.rb +++ b/plugins/poll/spec/lib/pretty_text_spec.rb @@ -100,7 +100,7 @@ describe PrettyText do
    -
  1. test 1 :slight_smile: test +
  2. test 1 :slight_smile: test
  3. test 2
@@ -165,7 +165,7 @@ describe PrettyText do MD expect(cooked).to include(<<~HTML) -
What’s your favorite berry? :wink: https://google.com/ +
What’s your favorite berry? :wink: https://google.com/
HTML end @@ -185,7 +185,7 @@ describe PrettyText do MD expect(cooked).to include(<<~HTML) -
What’s your favorite berry? :wink: https://google.com/ +
What’s your favorite berry? :wink: https://google.com/
HTML diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/05-input-fields.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/05-input-fields.hbs index b9f9528515..d3b743b793 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/05-input-fields.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/05-input-fields.hbs @@ -62,6 +62,12 @@
{{/styleguide-example}} +{{#styleguide-example title="full-width inline-form with search type input"}} +
+ {{input placeholder="Search type input" type="search"}} +
+{{/styleguide-example}} + {{#styleguide-example title="category-notifications-button and regular button"}}
{{category-notifications-button category=dummy.categories.[0] value=1 onChange=(action "dummy")}} diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/date-time-inputs.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/date-time-inputs.hbs index cbea914449..51cbad64f6 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/date-time-inputs.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/atoms/date-time-inputs.hbs @@ -22,7 +22,6 @@ {{future-date-input displayLabelIcon="far-clock" includeDateTime=true - includeMidFuture=true clearable=true }} {{/styleguide-example}} diff --git a/plugins/styleguide/config/locales/client.es.yml b/plugins/styleguide/config/locales/client.es.yml index 3362a941d9..4c62894985 100644 --- a/plugins/styleguide/config/locales/client.es.yml +++ b/plugins/styleguide/config/locales/client.es.yml @@ -85,3 +85,5 @@ es: title: "Iconos de encabezado" spinners: title: "Indicadores de carga" + empty_state: + title: "En caso de vacío" diff --git a/plugins/styleguide/config/locales/client.pl_PL.yml b/plugins/styleguide/config/locales/client.pl_PL.yml index c02380b92d..ed95894c1a 100644 --- a/plugins/styleguide/config/locales/client.pl_PL.yml +++ b/plugins/styleguide/config/locales/client.pl_PL.yml @@ -85,3 +85,5 @@ pl_PL: title: "Ikony nagłówka" spinners: title: "Ładowanie" + empty_state: + title: "Pusty stan" diff --git a/plugins/styleguide/config/locales/client.uk.yml b/plugins/styleguide/config/locales/client.uk.yml index 30f4c470bd..cc0ceab117 100644 --- a/plugins/styleguide/config/locales/client.uk.yml +++ b/plugins/styleguide/config/locales/client.uk.yml @@ -85,3 +85,5 @@ uk: title: "Значки заголовків" spinners: title: "Спінери" + empty_state: + title: "Порожній стан" diff --git a/public/403.nb_NO.html b/public/403.nb_NO.html index de9bc32b8d..7a428d6972 100644 --- a/public/403.nb_NO.html +++ b/public/403.nb_NO.html @@ -20,7 +20,7 @@

403

-

Du har ikke tilgang til å se den ressursen!

+

Du har ikke tilgang til å se den siden

Dette vil bli erstattet av en egendefinert Discourse 403-side.

diff --git a/public/javascripts/spectrum-colorpicker/1.8.0/spectrum.css b/public/javascripts/spectrum-colorpicker/1.8.0/spectrum.css deleted file mode 100644 index a8ad9e4f82..0000000000 --- a/public/javascripts/spectrum-colorpicker/1.8.0/spectrum.css +++ /dev/null @@ -1,507 +0,0 @@ -/*** -Spectrum Colorpicker v1.8.0 -https://github.com/bgrins/spectrum -Author: Brian Grinstead -License: MIT -***/ - -.sp-container { - position:absolute; - top:0; - left:0; - display:inline-block; - *display: inline; - *zoom: 1; - /* https://github.com/bgrins/spectrum/issues/40 */ - z-index: 9999994; - overflow: hidden; -} -.sp-container.sp-flat { - position: relative; -} - -/* Fix for * { box-sizing: border-box; } */ -.sp-container, -.sp-container * { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */ -.sp-top { - position:relative; - width: 100%; - display:inline-block; -} -.sp-top-inner { - position:absolute; - top:0; - left:0; - bottom:0; - right:0; -} -.sp-color { - position: absolute; - top:0; - left:0; - bottom:0; - right:20%; -} -.sp-hue { - position: absolute; - top:0; - right:0; - bottom:0; - left:84%; - height: 100%; -} - -.sp-clear-enabled .sp-hue { - top:33px; - height: 77.5%; -} - -.sp-fill { - padding-top: 80%; -} -.sp-sat, .sp-val { - position: absolute; - top:0; - left:0; - right:0; - bottom:0; -} - -.sp-alpha-enabled .sp-top { - margin-bottom: 18px; -} -.sp-alpha-enabled .sp-alpha { - display: block; -} -.sp-alpha-handle { - position:absolute; - top:-4px; - bottom: -4px; - width: 6px; - left: 50%; - cursor: pointer; - border: 1px solid black; - background: white; - opacity: .8; -} -.sp-alpha { - display: none; - position: absolute; - bottom: -14px; - right: 0; - left: 0; - height: 8px; -} -.sp-alpha-inner { - border: solid 1px #333; -} - -.sp-clear { - display: none; -} - -.sp-clear.sp-clear-display { - background-position: center; -} - -.sp-clear-enabled .sp-clear { - display: block; - position:absolute; - top:0px; - right:0; - bottom:0; - left:84%; - height: 28px; -} - -/* Don't allow text selection */ -.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button { - -webkit-user-select:none; - -moz-user-select: -moz-none; - -o-user-select:none; - user-select: none; -} - -.sp-container.sp-input-disabled .sp-input-container { - display: none; -} -.sp-container.sp-buttons-disabled .sp-button-container { - display: none; -} -.sp-container.sp-palette-buttons-disabled .sp-palette-button-container { - display: none; -} -.sp-palette-only .sp-picker-container { - display: none; -} -.sp-palette-disabled .sp-palette-container { - display: none; -} - -.sp-initial-disabled .sp-initial { - display: none; -} - - -/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */ -.sp-sat { - background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0))); - background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); - background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); - background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); - background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); - background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0)); - -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)"; - filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81'); -} -.sp-val { - background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0))); - background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); - background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); - background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); - background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); - background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0)); - -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)"; - filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000'); -} - -.sp-hue { - background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000)); - background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); -} - -/* IE filters do not support multiple color stops. - Generate 6 divs, line them up, and do two color gradients for each. - Yes, really. - */ -.sp-1 { - height:17%; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00'); -} -.sp-2 { - height:16%; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00'); -} -.sp-3 { - height:17%; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff'); -} -.sp-4 { - height:17%; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff'); -} -.sp-5 { - height:16%; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff'); -} -.sp-6 { - height:17%; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000'); -} - -.sp-hidden { - display: none !important; -} - -/* Clearfix hack */ -.sp-cf:before, .sp-cf:after { content: ""; display: table; } -.sp-cf:after { clear: both; } -.sp-cf { *zoom: 1; } - -/* Mobile devices, make hue slider bigger so it is easier to slide */ -@media (max-device-width: 480px) { - .sp-color { right: 40%; } - .sp-hue { left: 63%; } - .sp-fill { padding-top: 60%; } -} -.sp-dragger { - border-radius: 5px; - height: 5px; - width: 5px; - border: 1px solid #fff; - background: #000; - cursor: pointer; - position:absolute; - top:0; - left: 0; -} -.sp-slider { - position: absolute; - top:0; - cursor:pointer; - height: 3px; - left: -1px; - right: -1px; - border: 1px solid #000; - background: white; - opacity: .8; -} - -/* -Theme authors: -Here are the basic themeable display options (colors, fonts, global widths). -See http://bgrins.github.io/spectrum/themes/ for instructions. -*/ - -.sp-container { - border-radius: 0; - background-color: #ECECEC; - border: solid 1px #f0c49B; - padding: 0; -} -.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear { - font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; -} -.sp-top { - margin-bottom: 3px; -} -.sp-color, .sp-hue, .sp-clear { - border: solid 1px #666; -} - -/* Input */ -.sp-input-container { - float:right; - width: 100px; - margin-bottom: 4px; -} -.sp-initial-disabled .sp-input-container { - width: 100%; -} -.sp-input { - font-size: 12px !important; - border: 1px inset; - padding: 4px 5px; - margin: 0; - width: 100%; - background:transparent; - border-radius: 3px; - color: #222; -} -.sp-input:focus { - border: 1px solid orange; -} -.sp-input.sp-validation-error { - border: 1px solid red; - background: #fdd; -} -.sp-picker-container , .sp-palette-container { - float:left; - position: relative; - padding: 10px; - padding-bottom: 300px; - margin-bottom: -290px; -} -.sp-picker-container { - width: 172px; - border-left: solid 1px #fff; -} - -/* Palettes */ -.sp-palette-container { - border-right: solid 1px #ccc; -} - -.sp-palette-only .sp-palette-container { - border: 0; -} - -.sp-palette .sp-thumb-el { - display: block; - position:relative; - float:left; - width: 24px; - height: 15px; - margin: 3px; - cursor: pointer; - border:solid 2px transparent; -} -.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active { - border-color: orange; -} -.sp-thumb-el { - position:relative; -} - -/* Initial */ -.sp-initial { - float: left; - border: solid 1px #333; -} -.sp-initial span { - width: 30px; - height: 25px; - border:none; - display:block; - float:left; - margin:0; -} - -.sp-initial .sp-clear-display { - background-position: center; -} - -/* Buttons */ -.sp-palette-button-container, -.sp-button-container { - float: right; -} - -/* Replacer (the little preview div that shows up instead of the ) */ -.sp-replacer { - margin:0; - overflow:hidden; - cursor:pointer; - padding: 4px; - display:inline-block; - *zoom: 1; - *display: inline; - border: solid 1px #91765d; - background: #eee; - color: #333; - vertical-align: middle; -} -.sp-replacer:hover, .sp-replacer.sp-active { - border-color: #F0C49B; - color: #111; -} -.sp-replacer.sp-disabled { - cursor:default; - border-color: silver; - color: silver; -} -.sp-dd { - padding: 2px 0; - height: 16px; - line-height: 16px; - float:left; - font-size:10px; -} -.sp-preview { - position:relative; - width:25px; - height: 20px; - border: solid 1px #222; - margin-right: 5px; - float:left; - z-index: 0; -} - -.sp-palette { - *width: 220px; - max-width: 220px; -} -.sp-palette .sp-thumb-el { - width:16px; - height: 16px; - margin:2px 1px; - border: solid 1px #d0d0d0; -} - -.sp-container { - padding-bottom:0; -} - - -/* Buttons: http://hellohappy.org/css3-buttons/ */ -.sp-container button { - background-color: #eeeeee; - background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc); - background-image: -moz-linear-gradient(top, #eeeeee, #cccccc); - background-image: -ms-linear-gradient(top, #eeeeee, #cccccc); - background-image: -o-linear-gradient(top, #eeeeee, #cccccc); - background-image: linear-gradient(to bottom, #eeeeee, #cccccc); - border: 1px solid #ccc; - border-bottom: 1px solid #bbb; - border-radius: 3px; - color: #333; - font-size: 14px; - line-height: 1; - padding: 5px 4px; - text-align: center; - text-shadow: 0 1px 0 #eee; - vertical-align: middle; -} -.sp-container button:hover { - background-color: #dddddd; - background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); - background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); - background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); - background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); - background-image: linear-gradient(to bottom, #dddddd, #bbbbbb); - border: 1px solid #bbb; - border-bottom: 1px solid #999; - cursor: pointer; - text-shadow: 0 1px 0 #ddd; -} -.sp-container button:active { - border: 1px solid #aaa; - border-bottom: 1px solid #888; - -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; -} -.sp-cancel { - font-size: 11px; - color: #d93f3f !important; - margin:0; - padding:2px; - margin-right: 5px; - vertical-align: middle; - text-decoration:none; - -} -.sp-cancel:hover { - color: #d93f3f !important; - text-decoration: underline; -} - - -.sp-palette span:hover, .sp-palette span.sp-thumb-active { - border-color: #000; -} - -.sp-preview, .sp-alpha, .sp-thumb-el { - position:relative; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); -} -.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner { - display:block; - position:absolute; - top:0;left:0;bottom:0;right:0; -} - -.sp-palette .sp-thumb-inner { - background-position: 50% 50%; - background-repeat: no-repeat; -} - -.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=); -} - -.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=); -} - -.sp-clear-display { - background-repeat:no-repeat; - background-position: center; - background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==); -} diff --git a/public/javascripts/spectrum-colorpicker/1.8.0/spectrum.js b/public/javascripts/spectrum-colorpicker/1.8.0/spectrum.js deleted file mode 100644 index cf74825511..0000000000 --- a/public/javascripts/spectrum-colorpicker/1.8.0/spectrum.js +++ /dev/null @@ -1 +0,0 @@ -!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof exports&&"object"==typeof module?module.exports=t(require("jquery")):t(jQuery)}(function(jt,Et){"use strict";function Dt(t,e,r,n){for(var a=[],i=0;i')}else{var u="sp-clear-display";a.push(jt("
").append(jt('').attr("title",n.noColorSelectedText)).html())}}return"
"+a.join("")+"
"}function It(){for(var t=0;tMath.abs(e-a);nt=i?"x":"y"}}else nt=null;var o=!nt||"y"===nt;(!nt||"x"===nt)&&(Y=parseFloat(t/z)),o&&(G=parseFloat((B-e)/B)),Ot=!1,F.showAlpha||(Q=1),_()},s,l),Rt?(m(Rt),x(),Ft=F.preferredFormat||tinycolor(Rt).format,i(Rt)):x(),T&&d();var r=Xt?"mousedown.spectrum":"click.spectrum touchstart.spectrum";vt.delegate(".sp-thumb-el",r,t),mt.delegate(".sp-thumb-el:nth-child(1)",r,{ignore:!0},t)}function a(){if(q&&window.localStorage){try{var t=window.localStorage[q].split(",#");1et;)tt.shift();if(q&&window.localStorage)try{window.localStorage[q]=tt.join(";")}catch(r){}}}function o(){var t=[];if(F.showPalette)for(var e=0;el&&nc&&a","
","
","
"].join(""),Qt=function(){var t="";if(Xt)for(var e=1;e<=6;e++)t+="
";return["
","
","
","
","","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
",t,"
","
","
","
","
","","
","
","
","","","
","
","
"].join("")}(),i="spectrum.id";jt.fn.spectrum=function(r){if("string"!=typeof r)return this.spectrum("destroy").each(function(){var t=e(this,jt.extend({},r,jt(this).data()));jt(this).data(i,t.id)});var n=this,a=Array.prototype.slice.call(arguments,1);return this.each(function(){var t=Wt[jt(this).data(i)];if(t){var e=t[r];if(!e)throw new Error("Spectrum: no such method: '"+r+"'");"get"==r?n=t.get():"container"==r?n=t.container:"option"==r?n=t.option.apply(t,a):"destroy"==r?(t.destroy(),jt(this).removeData(i)):e.apply(t,a)}}),n},jt.fn.spectrum.load=!0,jt.fn.spectrum.loadOpts={},jt.fn.spectrum.draggable=Kt,jt.fn.spectrum.defaults=a,jt.fn.spectrum.inputTypeColorSupport=function $t(){if("undefined"==typeof $t._cachedResult){var t=jt("")[0];$t._cachedResult="color"===t.type&&""!==t.value}return $t._cachedResult},jt.spectrum={},jt.spectrum.localization={},jt.spectrum.palettes={},jt.fn.spectrum.processNativeColorInputs=function(){var t=jt("input[type=color]");t.length&&!$t()&&t.spectrum({preferredFormat:"hex6"})},function(){function n(t){var e={r:0,g:0,b:0},r=1,n=!1,a=!1;return"string"==typeof t&&(t=T(t)),"object"==typeof t&&(t.hasOwnProperty("r")&&t.hasOwnProperty("g")&&t.hasOwnProperty("b")?(e=i(t.r,t.g,t.b),n=!0,a="%"===String(t.r).substr(-1)?"prgb":"rgb"):t.hasOwnProperty("h")&&t.hasOwnProperty("s")&&t.hasOwnProperty("v")?(t.s=R(t.s),t.v=R(t.v),e=l(t.h,t.s,t.v),n=!0,a="hsv"):t.hasOwnProperty("h")&&t.hasOwnProperty("s")&&t.hasOwnProperty("l")&&(t.s=R(t.s),t.l=R(t.l),e=o(t.h,t.s,t.l),n=!0,a="hsl"),t.hasOwnProperty("a")&&(r=t.a)),r=x(r),{ok:n,format:t.format||a,r:D(255,I(e.r,0)),g:D(255,I(e.g,0)),b:D(255,I(e.b,0)),a:r}}function i(t,e,r){return{r:255*k(t,255),g:255*k(e,255),b:255*k(r,255)}}function a(t,e,r){t=k(t,255),e=k(e,255),r=k(r,255);var n,a,i=I(t,e,r),o=D(t,e,r),s=(i+o)/2;if(i==o)n=a=0;else{var l=i-o;switch(a=.5>1)+720)%360;--e;)n.h=(n.h+a)%360,i.push(B(n));return i}function w(t,e){e=e||6;for(var r=B(t).toHsv(),n=r.h,a=r.s,i=r.v,o=[],s=1/e;e--;)o.push(B({h:n,s:a,v:i})),i=(i+s)%1;return o}function _(t){var e={};for(var r in t)t.hasOwnProperty(r)&&(e[t[r]]=r);return e}function x(t){return t=parseFloat(t),(isNaN(t)||t<0||1) */ -.sp-replacer { - margin:0; - overflow:hidden; - cursor:pointer; - padding: 4px; - display:inline-block; - *zoom: 1; - *display: inline; - border: solid 1px #91765d; - background: #eee; - color: #333; - vertical-align: middle; -} -.sp-replacer:hover, .sp-replacer.sp-active { - border-color: #F0C49B; - color: #111; -} -.sp-replacer.sp-disabled { - cursor:default; - border-color: silver; - color: silver; -} -.sp-dd { - padding: 2px 0; - height: 16px; - line-height: 16px; - float:left; - font-size:10px; -} -.sp-preview { - position:relative; - width:25px; - height: 20px; - border: solid 1px #222; - margin-right: 5px; - float:left; - z-index: 0; -} - -.sp-palette { - *width: 220px; - max-width: 220px; -} -.sp-palette .sp-thumb-el { - width:16px; - height: 16px; - margin:2px 1px; - border: solid 1px #d0d0d0; -} - -.sp-container { - padding-bottom:0; -} - - -/* Buttons: http://hellohappy.org/css3-buttons/ */ -.sp-container button { - background-color: #eeeeee; - background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc); - background-image: -moz-linear-gradient(top, #eeeeee, #cccccc); - background-image: -ms-linear-gradient(top, #eeeeee, #cccccc); - background-image: -o-linear-gradient(top, #eeeeee, #cccccc); - background-image: linear-gradient(to bottom, #eeeeee, #cccccc); - border: 1px solid #ccc; - border-bottom: 1px solid #bbb; - border-radius: 3px; - color: #333; - font-size: 14px; - line-height: 1; - padding: 5px 4px; - text-align: center; - text-shadow: 0 1px 0 #eee; - vertical-align: middle; -} -.sp-container button:hover { - background-color: #dddddd; - background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); - background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); - background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); - background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); - background-image: linear-gradient(to bottom, #dddddd, #bbbbbb); - border: 1px solid #bbb; - border-bottom: 1px solid #999; - cursor: pointer; - text-shadow: 0 1px 0 #ddd; -} -.sp-container button:active { - border: 1px solid #aaa; - border-bottom: 1px solid #888; - -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; - box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; -} -.sp-cancel { - font-size: 11px; - color: #d93f3f !important; - margin:0; - padding:2px; - margin-right: 5px; - vertical-align: middle; - text-decoration:none; - -} -.sp-cancel:hover { - color: #d93f3f !important; - text-decoration: underline; -} - - -.sp-palette span:hover, .sp-palette span.sp-thumb-active { - border-color: #000; -} - -.sp-preview, .sp-alpha, .sp-thumb-el { - position:relative; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); -} -.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner { - display:block; - position:absolute; - top:0;left:0;bottom:0;right:0; -} - -.sp-palette .sp-thumb-inner { - background-position: 50% 50%; - background-repeat: no-repeat; -} - -.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=); -} - -.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=); -} - -.sp-clear-display { - background-repeat:no-repeat; - background-position: center; - background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==); -} diff --git a/public/javascripts/spectrum.js b/public/javascripts/spectrum.js deleted file mode 100644 index cf74825511..0000000000 --- a/public/javascripts/spectrum.js +++ /dev/null @@ -1 +0,0 @@ -!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof exports&&"object"==typeof module?module.exports=t(require("jquery")):t(jQuery)}(function(jt,Et){"use strict";function Dt(t,e,r,n){for(var a=[],i=0;i')}else{var u="sp-clear-display";a.push(jt("
").append(jt('').attr("title",n.noColorSelectedText)).html())}}return"
"+a.join("")+"
"}function It(){for(var t=0;tMath.abs(e-a);nt=i?"x":"y"}}else nt=null;var o=!nt||"y"===nt;(!nt||"x"===nt)&&(Y=parseFloat(t/z)),o&&(G=parseFloat((B-e)/B)),Ot=!1,F.showAlpha||(Q=1),_()},s,l),Rt?(m(Rt),x(),Ft=F.preferredFormat||tinycolor(Rt).format,i(Rt)):x(),T&&d();var r=Xt?"mousedown.spectrum":"click.spectrum touchstart.spectrum";vt.delegate(".sp-thumb-el",r,t),mt.delegate(".sp-thumb-el:nth-child(1)",r,{ignore:!0},t)}function a(){if(q&&window.localStorage){try{var t=window.localStorage[q].split(",#");1et;)tt.shift();if(q&&window.localStorage)try{window.localStorage[q]=tt.join(";")}catch(r){}}}function o(){var t=[];if(F.showPalette)for(var e=0;el&&nc&&a","
","
","
"].join(""),Qt=function(){var t="";if(Xt)for(var e=1;e<=6;e++)t+="
";return["
","
","
","
","","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
","
",t,"
","
","
","
","
","","
","
","
","","","
","
","
"].join("")}(),i="spectrum.id";jt.fn.spectrum=function(r){if("string"!=typeof r)return this.spectrum("destroy").each(function(){var t=e(this,jt.extend({},r,jt(this).data()));jt(this).data(i,t.id)});var n=this,a=Array.prototype.slice.call(arguments,1);return this.each(function(){var t=Wt[jt(this).data(i)];if(t){var e=t[r];if(!e)throw new Error("Spectrum: no such method: '"+r+"'");"get"==r?n=t.get():"container"==r?n=t.container:"option"==r?n=t.option.apply(t,a):"destroy"==r?(t.destroy(),jt(this).removeData(i)):e.apply(t,a)}}),n},jt.fn.spectrum.load=!0,jt.fn.spectrum.loadOpts={},jt.fn.spectrum.draggable=Kt,jt.fn.spectrum.defaults=a,jt.fn.spectrum.inputTypeColorSupport=function $t(){if("undefined"==typeof $t._cachedResult){var t=jt("")[0];$t._cachedResult="color"===t.type&&""!==t.value}return $t._cachedResult},jt.spectrum={},jt.spectrum.localization={},jt.spectrum.palettes={},jt.fn.spectrum.processNativeColorInputs=function(){var t=jt("input[type=color]");t.length&&!$t()&&t.spectrum({preferredFormat:"hex6"})},function(){function n(t){var e={r:0,g:0,b:0},r=1,n=!1,a=!1;return"string"==typeof t&&(t=T(t)),"object"==typeof t&&(t.hasOwnProperty("r")&&t.hasOwnProperty("g")&&t.hasOwnProperty("b")?(e=i(t.r,t.g,t.b),n=!0,a="%"===String(t.r).substr(-1)?"prgb":"rgb"):t.hasOwnProperty("h")&&t.hasOwnProperty("s")&&t.hasOwnProperty("v")?(t.s=R(t.s),t.v=R(t.v),e=l(t.h,t.s,t.v),n=!0,a="hsv"):t.hasOwnProperty("h")&&t.hasOwnProperty("s")&&t.hasOwnProperty("l")&&(t.s=R(t.s),t.l=R(t.l),e=o(t.h,t.s,t.l),n=!0,a="hsl"),t.hasOwnProperty("a")&&(r=t.a)),r=x(r),{ok:n,format:t.format||a,r:D(255,I(e.r,0)),g:D(255,I(e.g,0)),b:D(255,I(e.b,0)),a:r}}function i(t,e,r){return{r:255*k(t,255),g:255*k(e,255),b:255*k(r,255)}}function a(t,e,r){t=k(t,255),e=k(e,255),r=k(r,255);var n,a,i=I(t,e,r),o=D(t,e,r),s=(i+o)/2;if(i==o)n=a=0;else{var l=i-o;switch(a=.5>1)+720)%360;--e;)n.h=(n.h+a)%360,i.push(B(n));return i}function w(t,e){e=e||6;for(var r=B(t).toHsv(),n=r.h,a=r.s,i=r.v,o=[],s=1/e;e--;)o.push(B({h:n,s:a,v:i})),i=(i+s)%1;return o}function _(t){var e={};for(var r in t)t.hasOwnProperty(r)&&(e[t[r]]=r);return e}function x(t){return t=parseFloat(t),(isNaN(t)||t<0||1:) - text.gsub!(/.*?/) do + text.gsub!(/.*?/) do emoji($1) end end diff --git a/script/import_scripts/phpbb3/support/text_processor.rb b/script/import_scripts/phpbb3/support/text_processor.rb index f49765d648..622f0a931c 100644 --- a/script/import_scripts/phpbb3/support/text_processor.rb +++ b/script/import_scripts/phpbb3/support/text_processor.rb @@ -78,10 +78,10 @@ module ImportScripts::PhpBB3 # Many phpbb bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - text.gsub!(/:(?:\w{8})\]/, ']') + text.gsub!(/:(?:\w{5,8})\]/, ']') # remove color tags - text.gsub!(/\[\/?color(=#[a-z0-9]*)?\]/i, "") + text.gsub!(/\[\/?color(=#?[a-z0-9]*)?\]/i, "") end def bbcode_to_md(text) diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 77f9cb4b3a..9de2d5b628 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -1246,7 +1246,7 @@ describe CookedPostProcessor do Google

text.txt (20 Bytes)
- :smile:

+ :smile:

HTML end @@ -1261,7 +1261,7 @@ describe CookedPostProcessor do Google

text.txt (20 Bytes)
- :smile:

+ :smile:

HTML end @@ -1274,7 +1274,7 @@ describe CookedPostProcessor do Google

text.txt (20 Bytes)
- :smile:

+ :smile:

HTML end @@ -1288,7 +1288,7 @@ describe CookedPostProcessor do Google

text.txt (20 Bytes)
- :smile:

+ :smile:

HTML end @@ -1302,7 +1302,7 @@ describe CookedPostProcessor do Google

text.txt (20 Bytes)
- :smile:

+ :smile:

HTML end @@ -1336,7 +1336,7 @@ describe CookedPostProcessor do cpp.optimize_urls expect(cpp.html).to match_html <<~HTML -

This post has a local emoji :+1: and an external upload

+

This post has a local emoji :+1: and an external upload

smallest.png

HTML end @@ -1354,11 +1354,31 @@ describe CookedPostProcessor do cpp.optimize_urls expect(cpp.html).to match_html <<~HTML -

This post has a local emoji :+1: and an external upload

+

This post has a local emoji :+1: and an external upload

smallest.png

HTML end + it "doesn't use the secure media URL for custom emoji" do + CustomEmoji.create!(name: 'trout', upload: upload) + Emoji.clear_cache + Emoji.load_custom + stored_path = Discourse.store.get_path_for_upload(upload) + upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}") + upload.update_column(:secure, true) + + the_post = Fabricate(:post, raw: "This post has a custom emoji :trout:") + the_post.cook(the_post.raw) + + cpp = CookedPostProcessor.new(the_post) + cpp.optimize_urls + + upload_url = upload.url.gsub(SiteSetting.Upload.absolute_base_url, "https://s3.cdn.com") + expect(cpp.html).to match_html <<~HTML +

This post has a custom emoji :trout:

+ HTML + end + context "media uploads" do fab!(:image_upload) { Fabricate(:upload) } fab!(:audio_upload) { Fabricate(:upload, extension: "ogg") } diff --git a/spec/components/email/sender_spec.rb b/spec/components/email/sender_spec.rb index 940326a1a4..cfad24c929 100644 --- a/spec/components/email/sender_spec.rb +++ b/spec/components/email/sender_spec.rb @@ -123,8 +123,10 @@ describe Email::Sender do let(:reply_key) { "abcd" * 8 } let(:message) do - message = Mail::Message.new to: 'eviltrout@test.domain', - body: '**hello**' + message = Mail::Message.new( + to: 'eviltrout@test.domain', + body: '**hello**' + ) message.stubs(:deliver_now) message end @@ -288,7 +290,13 @@ describe Email::Sender do end it "sets the 'References' header with the incoming email Message-ID if it exists on the first post" do - incoming = Fabricate(:incoming_email, topic: topic, post: post_1, message_id: "blah1234@someemailprovider.com") + incoming = Fabricate( + :incoming_email, + topic: topic, + post: post_1, + message_id: "blah1234@someemailprovider.com", + created_via: IncomingEmail.created_via_types[:handle_mail] + ) message.header['X-Discourse-Post-Id'] = post_1.id email_sender.send @@ -331,7 +339,9 @@ describe Email::Sender do end it "uses the incoming_email message_id when available, but always uses a random message-id" do - topic_incoming_email = IncomingEmail.create(topic: topic, post: post_1, message_id: "foo@bar") + topic_incoming_email = IncomingEmail.create( + topic: topic, post: post_1, message_id: "foo@bar", created_via: IncomingEmail.created_via_types[:handle_mail] + ) post_2_incoming_email = IncomingEmail.create(topic: topic, post: post_2, message_id: "bar@foo") post_4_incoming_email = IncomingEmail.create(topic: topic, post: post_4, message_id: "wat@wat") diff --git a/spec/components/guardian/user_guardian_spec.rb b/spec/components/guardian/user_guardian_spec.rb index f07a4062b3..56b532b298 100644 --- a/spec/components/guardian/user_guardian_spec.rb +++ b/spec/components/guardian/user_guardian_spec.rb @@ -243,10 +243,11 @@ describe UserGuardian do end it "isn't allowed when user created too many posts" do - Fabricate(:post, user: user) + topic = Fabricate(:topic) + Fabricate(:post, topic: topic, user: user) expect(guardian.can_delete_user?(user)).to eq(true) - Fabricate(:post, user: user) + Fabricate(:post, topic: topic, user: user) expect(guardian.can_delete_user?(user)).to eq(false) end @@ -319,16 +320,18 @@ describe UserGuardian do end it "correctly respects the delete_user_self_max_post_count setting" do + topic = Fabricate(:topic) + SiteSetting.delete_user_self_max_post_count = 0 expect(guardian.can_delete_user?(user)).to eq(true) - Fabricate(:post, user: user) + Fabricate(:post, topic: topic, user: user) expect(guardian.can_delete_user?(user)).to eq(false) SiteSetting.delete_user_self_max_post_count = 1 expect(guardian.can_delete_user?(user)).to eq(true) - Fabricate(:post, user: user) + Fabricate(:post, topic: topic, user: user) expect(guardian.can_delete_user?(user)).to eq(false) SiteSetting.delete_user_self_max_post_count = 2 diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 4634ff0226..a16b05b26b 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -1184,6 +1184,8 @@ describe Guardian do end describe "can_recover_topic?" do + fab!(:topic) { Fabricate(:topic, user: user) } + fab!(:post) { Fabricate(:post, user: user, topic: topic) } it "returns false for a nil user" do expect(Guardian.new(nil).can_recover_topic?(topic)).to be_falsey @@ -1198,11 +1200,6 @@ describe Guardian do end context 'as a moderator' do - before do - topic.save! - post.save! - end - describe 'when post has been deleted' do it "should return the right value" do expect(Guardian.new(moderator).can_recover_topic?(topic)).to be_falsey @@ -1227,9 +1224,6 @@ describe Guardian do fab!(:group_user) { Fabricate(:group_user) } before do - topic.save! - post.save! - SiteSetting.enable_category_group_moderation = true PostDestroyer.new(moderator, topic.first_post).destroy topic.reload @@ -1262,10 +1256,8 @@ describe Guardian do end context 'as a moderator' do - before do - topic.save! - post.save! - end + fab!(:topic) { Fabricate(:topic, user: user) } + fab!(:post) { Fabricate(:post, user: user, topic: topic) } describe 'when post has been deleted' do it "should return the right value" do diff --git a/spec/components/js_locale_helper_spec.rb b/spec/components/js_locale_helper_spec.rb index 887ae8dc0d..27c8290ef9 100644 --- a/spec/components/js_locale_helper_spec.rb +++ b/spec/components/js_locale_helper_spec.rb @@ -87,7 +87,7 @@ describe JsLocaleHelper do end it 'handles message format special keys' do - JsLocaleHelper.set_translations('en', "en" => { + JsLocaleHelper.set_translations('en', "en" => { "js" => { "hello" => "world", "test_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", @@ -113,7 +113,7 @@ describe JsLocaleHelper do expect(ctx.eval('I18n.messageFormat("foo_MF", { HELLO: "hi", COUNT: 4 })')).to eq("hi 4 ducks") end - it 'load pluralizations rules before precompile' do + it 'load pluralization rules before precompile' do message = JsLocaleHelper.compile_message_format(message_format_filename('ru'), 'ru', 'format') expect(message).not_to match 'Plural Function not found' end @@ -186,6 +186,43 @@ describe JsLocaleHelper do end end + it "correctly evaluates message formats in en fallback" do + JsLocaleHelper.set_translations("en", "en" => { + "js" => { + "something_MF" => "en mf", + }, + }) + + JsLocaleHelper.set_translations("de", "de" => { + "js" => { + "something_MF" => "de mf", + }, + }) + + TranslationOverride.upsert!("en", "js.something_MF", <<~MF.strip) + There { + UNREAD, plural, + =0 {are no} + one {is one unread} + other {are # unread} + } + MF + + ctx = MiniRacer::Context.new + ctx.eval("var window = this;") + ctx.load(Rails.root + "app/assets/javascripts/locales/i18n.js") + ctx.eval(JsLocaleHelper.output_locale("de")) + ctx.eval(JsLocaleHelper.output_client_overrides("de")) + ctx.eval(<<~JS) + for (let [key, value] of Object.entries(I18n._mfOverrides || {})) { + key = key.replace(/^[a-z_]*js\./, ""); + I18n._compiledMFs[key] = value; + } + JS + + expect(ctx.eval("I18n.messageFormat('something_MF', { UNREAD: 1 })")).to eq("There is one unread") + end + LocaleSiteSetting.values.each do |locale| it "generates valid date helpers for #{locale[:value]} locale" do js = JsLocaleHelper.output_locale(locale[:value]) @@ -207,12 +244,24 @@ describe JsLocaleHelper do end describe ".find_message_format_locale" do + it "finds locale's message format rules" do + locale, filename = JsLocaleHelper.find_message_format_locale([:de], fallback_to_english: false) + expect(locale).to eq("de") + expect(filename).to end_with("/de.js") + end + it "finds locale for en_GB" do - locale, filename = JsLocaleHelper.find_message_format_locale([:en_GB], fallback_to_english: false) + locale, filename = JsLocaleHelper.find_message_format_locale([:en_GB], fallback_to_english: false) expect(locale).to eq("en") expect(filename).to end_with("/en.js") - locale, filename = JsLocaleHelper.find_message_format_locale(["en_GB"], fallback_to_english: false) + locale, filename = JsLocaleHelper.find_message_format_locale(["en_GB"], fallback_to_english: false) + expect(locale).to eq("en") + expect(filename).to end_with("/en.js") + end + + it "falls back to en when locale doesn't have own message format rules" do + locale, filename = JsLocaleHelper.find_message_format_locale([:nonexistent], fallback_to_english: true) expect(locale).to eq("en") expect(filename).to end_with("/en.js") end diff --git a/spec/components/new_post_manager_spec.rb b/spec/components/new_post_manager_spec.rb index 29ac3ca83b..ff7085f4d3 100644 --- a/spec/components/new_post_manager_spec.rb +++ b/spec/components/new_post_manager_spec.rb @@ -4,12 +4,12 @@ require 'rails_helper' require 'new_post_manager' describe NewPostManager do - + fab!(:user) { Fabricate(:user) } fab!(:topic) { Fabricate(:topic) } context "default action" do it "creates the post by default" do - manager = NewPostManager.new(topic.user, raw: 'this is a new post', topic_id: topic.id) + manager = NewPostManager.new(user, raw: 'this is a new post', topic_id: topic.id) result = manager.perform expect(result.action).to eq(:create_post) @@ -25,7 +25,7 @@ describe NewPostManager do it "doesn't enqueue private messages" do SiteSetting.approve_unless_trust_level = 4 - manager = NewPostManager.new(topic.user, + manager = NewPostManager.new(user, raw: 'this is a new post', title: 'this is a new title', archetype: Archetype.private_message, @@ -40,7 +40,7 @@ describe NewPostManager do expect(result.post).to be_a(Post) # It doesn't enqueue replies to the private message either - manager = NewPostManager.new(topic.user, + manager = NewPostManager.new(user, raw: 'this is a new reply', topic_id: result.post.topic_id) @@ -56,7 +56,7 @@ describe NewPostManager do end context "default handler" do - let(:manager) { NewPostManager.new(topic.user, raw: 'this is new post content', topic_id: topic.id) } + let(:manager) { NewPostManager.new(user, raw: 'this is new post content', topic_id: topic.id) } context 'with the settings zeroed out' do before do @@ -126,8 +126,9 @@ describe NewPostManager do context 'with a high approval post count, but TL2' do before do SiteSetting.approve_post_count = 100 - topic.user.trust_level = 2 + user.update!(trust_level: 2) end + it "will return an enqueue result" do result = NewPostManager.default_handler(manager) expect(result).to be_nil @@ -188,8 +189,9 @@ describe NewPostManager do context 'with staged moderation setting enabled' do before do SiteSetting.approve_unless_staged = true - topic.user.staged = true + user.update!(staged: true) end + it "will return an enqueue result" do result = NewPostManager.default_handler(manager) expect(NewPostManager.queue_enabled?).to eq(true) @@ -209,17 +211,17 @@ describe NewPostManager do end context 'with a fast typer' do - let(:user) { manager.user } - before do user.update!(trust_level: 0) end it "adds the silence reason in the system locale" do manager = build_manager_with('this is new post content') + I18n.with_locale(:fr) do # Simulate french user result = NewPostManager.default_handler(manager) end + expect(user.silenced?).to eq(true) expect(user.silence_reason).to eq(I18n.t("user.new_user_typed_too_fast", locale: :en)) end @@ -235,12 +237,11 @@ describe NewPostManager do end def build_manager_with(raw) - NewPostManager.new(topic.user, raw: raw, topic_id: topic.id, first_post_checks: true) + NewPostManager.new(user, raw: raw, topic_id: topic.id, first_post_checks: true) end end context 'with media' do - let(:user) { manager.user } let(:manager_opts) do { raw: 'this is new post content', topic_id: topic.id, first_post_checks: false, @@ -258,7 +259,7 @@ describe NewPostManager do it 'queues the post for review because if it contains embedded media.' do SiteSetting.review_media_unless_trust_level = 1 - manager = NewPostManager.new(topic.user, manager_opts) + manager = NewPostManager.new(user, manager_opts) result = NewPostManager.default_handler(manager) @@ -268,7 +269,7 @@ describe NewPostManager do it 'does not enqueue the post if the poster is a trusted user' do SiteSetting.review_media_unless_trust_level = 0 - manager = NewPostManager.new(topic.user, manager_opts) + manager = NewPostManager.new(user, manager_opts) result = NewPostManager.default_handler(manager) @@ -278,7 +279,7 @@ describe NewPostManager do end context "new topic handler" do - let(:manager) { NewPostManager.new(topic.user, raw: 'this is new topic content', title: 'new topic title') } + let(:manager) { NewPostManager.new(user, raw: 'this is new topic content', title: 'new topic title') } context 'with a high trust level setting for new topics' do before do SiteSetting.approve_new_topics_unless_trust_level = 4 @@ -351,7 +352,7 @@ describe NewPostManager do end it "calls custom handlers" do - manager = NewPostManager.new(topic.user, raw: 'this post increases counter', topic_id: topic.id) + manager = NewPostManager.new(user, raw: 'this post increases counter', topic_id: topic.id) result = manager.perform @@ -409,7 +410,7 @@ describe NewPostManager do end it "if nothing returns a result it creates a post" do - manager = NewPostManager.new(topic.user, raw: 'this is a new post', topic_id: topic.id) + manager = NewPostManager.new(user, raw: 'this is a new post', topic_id: topic.id) result = manager.perform diff --git a/spec/components/oneboxer_spec.rb b/spec/components/oneboxer_spec.rb index 40ce9e49b8..562c06b311 100644 --- a/spec/components/oneboxer_spec.rb +++ b/spec/components/oneboxer_spec.rb @@ -160,28 +160,54 @@ describe Oneboxer do end end - it "does not crawl blocklisted URLs" do - SiteSetting.blocked_onebox_domains = "git.*.com|bitbucket.com" - url = 'https://github.com/discourse/discourse/commit/21b562852885f883be43032e03c709241e8e6d4f' - stub_request(:head, 'https://discourse.org/').to_return(status: 302, body: "", headers: { location: url }) + context ".external_onebox" do + html = <<~HTML + + + + + + +

body

+ + + HTML - expect(Oneboxer.external_onebox(url)[:onebox]).to be_empty - expect(Oneboxer.external_onebox('https://discourse.org/')[:onebox]).to be_empty - end + context "blacklisted domains" do - it "does not consider ignore_redirects domains as blocklisted" do - url = 'https://store.steampowered.com/app/271590/Grand_Theft_Auto_V/' - stub_request(:head, url).to_return(status: 200, body: "", headers: {}) - stub_request(:get, url).to_return(status: 200, body: "", headers: {}) + it "does not return a onebox if redirect uri final destination is in blacklist" do + SiteSetting.blocked_onebox_domains = "kitten.com" - expect(Oneboxer.external_onebox(url)[:onebox]).to be_present - end + stub_request(:get, "http://cat.com/meow").to_return(status: 301, body: "", headers: { "location" => "https://kitten.com" }) + stub_request(:head, "http://cat.com/meow").to_return(status: 301, body: "", headers: { "location" => "https://kitten.com" }) - it "censors external oneboxes" do - Fabricate(:watched_word, action: WatchedWord.actions[:censor], word: "bad word") + stub_request(:get, "https://kitten.com").to_return(status: 200, body: html, headers: {}) + stub_request(:head, "https://kitten.com").to_return(status: 200, body: "", headers: {}) - url = 'https://example.com/' - stub_request(:any, url).to_return(status: 200, body: <<~HTML, headers: {}) + expect(Oneboxer.external_onebox("http://cat.com/meow")[:onebox]).to be_empty + expect(Oneboxer.external_onebox("https://kitten.com")[:onebox]).to be_empty + end + + it "returns onebox if 'midway redirect' is blocked but final redirect uri is not blocked" do + SiteSetting.blocked_onebox_domains = "middle.com" + + stub_request(:get, "https://cat.com/start").to_return(status: 301, body: "a", headers: { "location" => "https://middle.com/midway" }) + stub_request(:head, "https://cat.com/start").to_return(status: 301, body: "a", headers: { "location" => "https://middle.com/midway" }) + + stub_request(:head, "https://middle.com/midway").to_return(status: 301, body: "b", headers: { "location" => "https://cat.com/end" }) + + stub_request(:get, "https://cat.com/end").to_return(status: 200, body: html) + stub_request(:head, "https://cat.com/end").to_return(status: 200, body: "", headers: {}) + + expect(Oneboxer.external_onebox("https://cat.com/start")[:onebox]).to be_present + end + end + + it "censors external oneboxes" do + Fabricate(:watched_word, action: WatchedWord.actions[:censor], word: "bad word") + + url = 'https://example.com/' + stub_request(:any, url).to_return(status: 200, body: <<~HTML, headers: {}) @@ -191,13 +217,23 @@ describe Oneboxer do

content with bad word

- HTML + HTML - onebox = Oneboxer.external_onebox(url) - expect(onebox[:onebox]).to include('title with') - expect(onebox[:onebox]).not_to include('bad word') - expect(onebox[:preview]).to include('title with') - expect(onebox[:preview]).not_to include('bad word') + onebox = Oneboxer.external_onebox(url) + expect(onebox[:onebox]).to include('title with') + expect(onebox[:onebox]).not_to include('bad word') + expect(onebox[:preview]).to include('title with') + expect(onebox[:preview]).not_to include('bad word') + end + + it "returns onebox" do + SiteSetting.blocked_onebox_domains = "not.me" + + stub_request(:get, "https://its.me").to_return(status: 200, body: html) + stub_request(:head, "https://its.me").to_return(status: 200, body: "", headers: {}) + + expect(Oneboxer.external_onebox("https://its.me")[:onebox]).to be_present + end end it "uses the Onebox custom user agent on specified hosts" do diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 484ff8140a..2c58aba637 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -14,7 +14,7 @@ describe PostCreator do context "new topic" do fab!(:category) { Fabricate(:category, user: user) } - let(:basic_topic_params) { { title: "hello world topic", raw: "my name is fred", archetype_id: 1 } } + let(:basic_topic_params) { { title: "hello world topic", raw: "my name is fred", archetype_id: 1, advance_draft: true } } let(:image_sizes) { { 'http://an.image.host/image.jpg' => { "width" => 111, "height" => 222 } } } let(:creator) { PostCreator.new(user, basic_topic_params) } @@ -50,6 +50,8 @@ describe PostCreator do expect(post.hidden_at).to be_present expect(post.hidden_reason_id).to eq(hri) expect(post.topic.visible).to eq(false) + expect(post.user.topic_count).to eq(0) + expect(post.user.post_count).to eq(0) end it "ensures the user can create the topic" do @@ -150,7 +152,7 @@ describe PostCreator do messages = MessageBus.track_publish do created_post = PostCreator.new(admin, basic_topic_params.merge(category: cat.id)).create - _reply = PostCreator.new(admin, raw: "this is my test reply 123 testing", topic_id: created_post.topic_id).create + _reply = PostCreator.new(admin, raw: "this is my test reply 123 testing", topic_id: created_post.topic_id, advance_draft: true).create end messages.filter! { |m| m.channel != "/distributed_hash" } @@ -303,6 +305,20 @@ describe PostCreator do end end + it 'clears the draft if advanced_draft is true' do + creator = PostCreator.new(user, basic_topic_params.merge(advance_draft: true)) + Draft.set(user, Draft::NEW_TOPIC, 0, 'test') + expect(Draft.where(user: user).size).to eq(1) + expect { creator.create }.to change { Draft.count }.by(-1) + end + + it 'does not clear the draft if advanced_draft is false' do + creator = PostCreator.new(user, basic_topic_params.merge(advance_draft: false)) + Draft.set(user, Draft::NEW_TOPIC, 0, 'test') + expect(Draft.where(user: user).size).to eq(1) + expect { creator.create }.to change { Draft.count }.by(0) + end + it "updates topic stats" do first_post = creator.create topic = first_post.topic.reload diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 51a2bcc538..d510e6cc5f 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -995,7 +995,7 @@ describe PostDestroyer do end it 'should destroy internal links when moderator deletes post' do - new_post = Post.create!(user: user, topic: topic, raw: "Link to other topic:\n\n#{url}\n") + new_post = create_post(user: user, topic: topic, raw: "Link to other topic:\n\n#{url}\n") TopicLink.extract_from(new_post) link_counts = TopicLink.counts_for(guardian, other_topic.reload, [other_post]) expect(link_counts.count).to eq(1) diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 8e180c5eb5..76d522d60c 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -37,7 +37,7 @@ describe PrettyText do

ddd

@@ -248,7 +248,7 @@ describe PrettyText do

ddd

@@ -269,7 +269,7 @@ describe PrettyText do

ddd

@@ -296,7 +296,7 @@ describe PrettyText do