From f37375f582819153a630a0e75d5e6f050ab0b65a Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Wed, 1 Dec 2021 18:21:44 +0400 Subject: [PATCH 001/119] DEV: avoid sending events to a destroying object and enable few skipped tests (#15030) --- app/assets/javascripts/discourse/app/controllers/flag.js | 8 +++++++- .../discourse/tests/acceptance/flag-post-test.js | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/app/controllers/flag.js b/app/assets/javascripts/discourse/app/controllers/flag.js index 042bea17de..49ea8b525d 100644 --- a/app/assets/javascripts/discourse/app/controllers/flag.js +++ b/app/assets/javascripts/discourse/app/controllers/flag.js @@ -275,6 +275,10 @@ export default Controller.extend(ModalFunctionality, { postAction .act(this.model, params) .then(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } + if (!params.skipClose) { this.send("closeModal"); } @@ -286,7 +290,9 @@ export default Controller.extend(ModalFunctionality, { }); }) .catch((error) => { - this.send("closeModal"); + if (!this.isDestroying && !this.isDestroyed) { + this.send("closeModal"); + } popupAjaxError(error); }); }, diff --git a/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js b/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js index ded890b5dd..587fd4473c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js @@ -6,7 +6,7 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import { click, fillIn, visit } from "@ember/test-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; -import { skip, test } from "qunit"; +import { test } from "qunit"; import userFixtures from "discourse/tests/fixtures/user-fixtures"; import { run } from "@ember/runloop"; @@ -153,7 +153,7 @@ acceptance("flagging", function (needs) { assert.ok(!exists(".bootbox.modal:visible")); }); - skip("CTRL + ENTER accepts the modal", async function (assert) { + test("CTRL + ENTER accepts the modal", async function (assert) { await visit("/t/internationalization-localization/280"); await openFlagModal(); @@ -169,7 +169,7 @@ acceptance("flagging", function (needs) { assert.ok(!exists("#discourse-modal:visible"), "The modal was closed"); }); - skip("CMD or WINDOWS-KEY + ENTER accepts the modal", async function (assert) { + test("CMD or WINDOWS-KEY + ENTER accepts the modal", async function (assert) { await visit("/t/internationalization-localization/280"); await openFlagModal(); From 1fa7a87f866115b6f779cdffcd0d58bcdb5cb84e Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 1 Dec 2021 16:10:40 +0000 Subject: [PATCH 002/119] SECURITY: Remove ember-cli specific response from application routes (#15155) Under some conditions, these varied responses could lead to cache poisoning, hence the 'security' label. Previously the Rails application would serve JSON data in place of HTML whenever Ember CLI requested an `application.html.erb`-rendered page. This commit removes that logic, and instead parses the HTML out of the standard response. This means that Rails doesn't need to customize its response for Ember CLI. --- .../javascripts/discourse/ember-cli-build.js | 3 + .../discourse/lib/bootstrap-json/index.js | 58 ++++-- app/assets/javascripts/discourse/package.json | 1 + app/assets/javascripts/yarn.lock | 185 +++++++++++++++++- app/controllers/application_controller.rb | 30 +-- app/controllers/bootstrap_controller.rb | 13 +- app/helpers/application_helper.rb | 6 - app/views/layouts/application.html.erb | 1 - lib/discourse.rb | 5 - spec/components/discourse_spec.rb | 6 - spec/requests/bootstrap_controller_spec.rb | 34 ++++ 11 files changed, 265 insertions(+), 77 deletions(-) diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 59c48a1463..d6301bd569 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -25,6 +25,9 @@ module.exports = function (defaults) { // This forces the use of `fast-sourcemap-concat` which works in production. enabled: true, }, + autoImport: { + forbidEval: true, + }, }); // Ember CLI does this by default for the app tree, but for our extra bundles we diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index 87342b34d7..197f9975be 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -6,6 +6,7 @@ const { encode } = require("html-entities"); const cleanBaseURL = require("clean-base-url"); const path = require("path"); const { promises: fs } = require("fs"); +const { JSDOM } = require("jsdom"); // via https://stackoverflow.com/a/6248722/165668 function generateUID() { @@ -168,12 +169,14 @@ function replaceIn(bootstrap, template, id, headers, baseURL) { return template.replace(``, contents); } -async function applyBootstrap(bootstrap, template, response, baseURL) { - // If our initial page added some preload data let's not lose that. - let json = await response.json(); - if (json && json.preloaded) { - bootstrap.preloaded = Object.assign(json.preloaded, bootstrap.preloaded); - } +function extractPreloadJson(html) { + const dom = new JSDOM(html); + return dom.window.document.querySelector("#data-preloaded")?.dataset + ?.preloaded; +} + +async function applyBootstrap(bootstrap, template, response, baseURL, preload) { + bootstrap.preloaded = Object.assign(JSON.parse(preload), bootstrap.preloaded); Object.keys(BUILDERS).forEach((id) => { template = replaceIn(bootstrap, template, id, response.headers, baseURL); @@ -181,23 +184,20 @@ async function applyBootstrap(bootstrap, template, response, baseURL) { return template; } -async function buildFromBootstrap(proxy, baseURL, req, response) { +async function buildFromBootstrap(proxy, baseURL, req, response, preload) { try { const template = await fs.readFile( path.join(process.cwd(), "dist", "index.html"), "utf8" ); - let url = `${proxy}${baseURL}bootstrap.json`; - const queryLoc = req.url.indexOf("?"); - if (queryLoc !== -1) { - url += req.url.substr(queryLoc); - } + let url = new URL(`${proxy}${baseURL}bootstrap.json`); + url.searchParams.append("for_url", req.url); const res = await fetch(url, { headers: req.headers }); const json = await res.json(); - return applyBootstrap(json.bootstrap, template, response, baseURL); + return applyBootstrap(json.bootstrap, template, response, baseURL, preload); } catch (error) { throw new Error( `Could not get ${proxy}${baseURL}bootstrap.json\n\n${error}` @@ -229,7 +229,6 @@ async function handleRequest(proxy, baseURL, req, res) { if (req.method === "GET") { req.headers["X-Discourse-Ember-CLI"] = "true"; - req.headers["X-Discourse-Asset-Path"] = req.path; } const response = await fetch(url, { @@ -251,20 +250,37 @@ async function handleRequest(proxy, baseURL, req, res) { const csp = response.headers.get("content-security-policy"); if (csp) { - const newCSP = csp.replace( - new RegExp(proxy, "g"), - `http://${originalHost}` - ); + const emberCliAdditions = [ + `http://${originalHost}/assets/`, + `http://${originalHost}/ember-cli-live-reload.js`, + `http://${originalHost}/_lr/`, + ]; + const newCSP = csp + .replace(new RegExp(proxy, "g"), `http://${originalHost}`) + .replace( + new RegExp("script-src ", "g"), + `script-src ${emberCliAdditions.join(" ")} ` + ); res.set("content-security-policy", newCSP); } - if (response.headers.get("x-discourse-bootstrap-required") === "true") { - const html = await buildFromBootstrap(proxy, baseURL, req, response); + const isHTML = response.headers["content-type"]?.startsWith("text/html"); + const responseText = await response.text(); + const preload = isHTML ? extractPreloadJson(responseText) : null; + + if (preload) { + const html = await buildFromBootstrap( + proxy, + baseURL, + req, + response, + preload + ); res.set("content-type", "text/html"); res.send(html); } else { res.status(response.status); - res.send(await response.text()); + res.send(responseText); } } diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 59735ab571..32b109253a 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -54,6 +54,7 @@ "eslint": "^7.27.0", "html-entities": "^2.1.0", "js-yaml": "^4.0.0", + "jsdom": "^18.1.1", "loader.js": "^4.7.0", "message-bus-client": "^3.3.0", "messageformat": "0.1.5", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index 7be414245a..691b5eea85 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -1317,6 +1317,11 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@transloadit/prettier-bytes@0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b" @@ -1715,11 +1720,23 @@ acorn@^8.1.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.1.tgz#fb0026885b9ac9f48bac1e185e4af472971149ff" integrity sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g== +acorn@^8.5.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" + integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -4080,7 +4097,7 @@ colors@^1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -4405,6 +4422,11 @@ cssom@^0.4.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -4443,6 +4465,15 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-urls@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8" + integrity sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4450,6 +4481,13 @@ debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3. dependencies: ms "2.0.0" +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + debug@^3.0.1, debug@^3.1.0, debug@^3.1.1: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -4457,13 +4495,6 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.1.1: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -4483,6 +4514,11 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== +decimal.js@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -4653,6 +4689,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -6391,6 +6434,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -7122,6 +7174,13 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-entities@^2.1.0: version "2.3.2" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" @@ -7169,6 +7228,15 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-proxy@^1.13.1, http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" @@ -7192,6 +7260,14 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + https@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https/-/https-1.0.0.tgz#3c37c7ae1a8eeb966904a2ad1e975a194b7ed3a4" @@ -7209,6 +7285,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -7584,7 +7667,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-potential-custom-element-name@^1.0.0: +is-potential-custom-element-name@^1.0.0, is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== @@ -7803,6 +7886,39 @@ jsdom@^16.4.0: ws "^7.4.4" xml-name-validator "^3.0.0" +jsdom@^18.1.1: + version "18.1.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-18.1.1.tgz#15ec896f5ab7df9669a62375606f47c8c09551aa" + integrity sha512-NmJQbjQ/gpS/1at/ce3nCx89HbXL/f5OcenBe8wU1Eik0ROhyUc3LtmG3567dEHAGXkN8rmILW/qtCOPxPHQJw== + dependencies: + abab "^2.0.5" + acorn "^8.5.0" + acorn-globals "^6.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.1" + decimal.js "^10.3.1" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + ws "^8.2.3" + xml-name-validator "^4.0.0" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -10208,7 +10324,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -11272,6 +11388,13 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -11688,6 +11811,13 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" +w3c-xmlserializer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" + integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== + dependencies: + xml-name-validator "^4.0.0" + walk-sync@^0.2.5: version "0.2.7" resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.2.7.tgz#b49be4ee6867657aeb736978b56a29d10fa39969" @@ -11785,6 +11915,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-sources@^1.4.0, webpack-sources@^1.4.1: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" @@ -11843,11 +11978,31 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da" + integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -11987,6 +12142,11 @@ ws@^7.4.4, ws@~7.4.2: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +ws@^8.2.3: + version "8.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d" + integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw== + x-is-array@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/x-is-array/-/x-is-array-0.1.0.tgz#de520171d47b3f416f5587d629b89d26b12dc29d" @@ -12007,6 +12167,11 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1351d76037..7dd73b3f82 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -119,23 +119,9 @@ class ApplicationController < ActionController::Base class RenderEmpty < StandardError; end class PluginDisabled < StandardError; end - class EmberCLIHijacked < StandardError; end - - def catch_ember_cli_hijack - yield - rescue ActionView::Template::Error => ex - raise ex unless ex.cause.is_a?(EmberCLIHijacked) - send_ember_cli_bootstrap - end rescue_from RenderEmpty do - catch_ember_cli_hijack do - with_resolved_locale { render 'default/empty' } - end - end - - rescue_from EmberCLIHijacked do - send_ember_cli_bootstrap + with_resolved_locale { render 'default/empty' } end rescue_from ArgumentError do |e| @@ -324,21 +310,13 @@ class ApplicationController < ActionController::Base rescue Discourse::InvalidAccess return render plain: message, status: status_code end - catch_ember_cli_hijack do - with_resolved_locale do - error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember' - render html: build_not_found_page(error_page_opts) - end + with_resolved_locale do + error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember' + render html: build_not_found_page(error_page_opts) end end end - def send_ember_cli_bootstrap - response.headers['X-Discourse-Bootstrap-Required'] = true - response.headers['Content-Type'] = "application/json" - render json: { preloaded: @preloaded } - end - # If a controller requires a plugin, it will raise an exception if that plugin is # disabled. This allows plugins to be disabled programmatically. def self.requires_plugin(plugin_name) diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb index 7c6c519261..811058d752 100644 --- a/app/controllers/bootstrap_controller.rb +++ b/app/controllers/bootstrap_controller.rb @@ -27,12 +27,21 @@ class BootstrapController < ApplicationController add_style(mobile_view? ? :mobile : :desktop) end add_style(:admin) if staff? + + assets_fake_request = ActionDispatch::Request.new(request.env.dup) + assets_for_url = params[:for_url] + if assets_for_url + path, query = assets_for_url.split("?", 2) + assets_fake_request.env["PATH_INFO"] = path + assets_fake_request.env["QUERY_STRING"] = query + end + Discourse.find_plugin_css_assets( include_official: allow_plugins?, include_unofficial: allow_third_party_plugins?, mobile_view: mobile_view?, desktop_view: !mobile_view?, - request: request + request: assets_fake_request ).each do |file| add_style(file, plugin: true) end @@ -49,7 +58,7 @@ class BootstrapController < ApplicationController plugin_js = Discourse.find_plugin_js_assets( include_official: allow_plugins?, include_unofficial: allow_third_party_plugins?, - request: request + request: assets_fake_request ).map { |f| script_asset_path(f) } bootstrap = { diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a5cfbb9aba..c5a9627123 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -647,10 +647,4 @@ module ApplicationHelper current_user ? nil : value end end - - def hijack_if_ember_cli! - if request.headers["HTTP_X_DISCOURSE_EMBER_CLI"] == "true" - raise ApplicationController::EmberCLIHijacked.new - end - end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 53a4410a6c..ed984a5bb5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -131,4 +131,3 @@ <%- end %> -<%- hijack_if_ember_cli! -%> diff --git a/lib/discourse.rb b/lib/discourse.rb index 3e641b4e29..303b94293b 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -338,11 +338,6 @@ module Discourse path = request.fullpath result[:path] = path if path.present? - # When we bootstrap using the JSON method, we want to be able to filter assets on - # the path we're bootstrapping for. - asset_path = request.headers["HTTP_X_DISCOURSE_ASSET_PATH"] - result[:path] = asset_path if asset_path.present? - result end diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb index aa2542e04b..2f47dd0bb5 100644 --- a/spec/components/discourse_spec.rb +++ b/spec/components/discourse_spec.rb @@ -76,12 +76,6 @@ describe Discourse do opts = Discourse.asset_filter_options(:js, req) expect(opts[:path]).to eq("/hello") end - - it "overwrites the path if the asset path is present" do - req = stub(fullpath: "/bootstrap.json", headers: { "HTTP_X_DISCOURSE_ASSET_PATH" => "/hello" }) - opts = Discourse.asset_filter_options(:js, req) - expect(opts[:path]).to eq("/hello") - end end context 'plugins' do diff --git a/spec/requests/bootstrap_controller_spec.rb b/spec/requests/bootstrap_controller_spec.rb index b11e09330f..0eb71ddec2 100644 --- a/spec/requests/bootstrap_controller_spec.rb +++ b/spec/requests/bootstrap_controller_spec.rb @@ -93,4 +93,38 @@ describe BootstrapController do expect(bootstrap['authentication_data']).to eq(cookie_data) end end + + context 'with a plugin asset filter' do + let :plugin do + plugin = Plugin::Instance.new + plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb" + plugin.register_asset_filter do |type, request| + next true if request.path == "/mypluginroute" + false + end + plugin + end + + before do + Discourse.plugins << plugin + plugin.activate! + end + + after do + Discourse.plugins.delete plugin + end + + it "filters assets using the given path" do + get "/bootstrap.json" + expect(response.status).to eq(200) + plugin_assets = response.parsed_body.dig("bootstrap", "plugin_js") + expect(plugin_assets).not_to include(a_string_matching "my_plugin") + + get "/bootstrap.json?for_url=/mypluginroute" + expect(response.status).to eq(200) + plugin_assets = response.parsed_body.dig("bootstrap", "plugin_js") + expect(plugin_assets).to include(a_string_matching "my_plugin") + end + + end end From c4d3b6556d750b9157a766ee370f2b4945dbb986 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 1 Dec 2021 11:40:49 -0500 Subject: [PATCH 003/119] Version bump to v2.8.0.beta9 (#15152) --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index e44370aacc..777e7c7471 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -10,7 +10,7 @@ module Discourse MAJOR = 2 MINOR = 8 TINY = 0 - PRE = 'beta8' + PRE = 'beta9' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end From 754c2ec6c12c417abd841cb06e54815582aa5718 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Wed, 1 Dec 2021 13:53:20 -0300 Subject: [PATCH 004/119] Build(deps): Bump mini_suffix from 0.3.2 to 0.3.3 (#15151) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb80bdea03..06868e8af4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -233,7 +233,7 @@ GEM mini_scheduler (0.13.0) sidekiq (>= 4.2.3) mini_sql (1.1.3) - mini_suffix (0.3.2) + mini_suffix (0.3.3) ffi (~> 1.9) minitest (5.14.4) mocha (1.13.0) From 20f736aa112145efa3f09b6ab120904e2faa1b90 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Wed, 1 Dec 2021 19:57:36 +0300 Subject: [PATCH 005/119] FIX: Skip themes that have blank URL in the `themes:update` rake task (#15156) Themes that are imported via a ZIP file do have a `remote_theme` record in the database but the record has a blank value for the `remote_url` field which means attempting to do an update git via will result in an error. --- lib/tasks/themes.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/themes.rake b/lib/tasks/themes.rake index 5dbcae6367..34893c8f29 100644 --- a/lib/tasks/themes.rake +++ b/lib/tasks/themes.rake @@ -56,7 +56,7 @@ def update_themes Theme.includes(:remote_theme).where(enabled: true, auto_update: true).find_each do |theme| begin remote_theme = theme.remote_theme - next if remote_theme.blank? + next if remote_theme.blank? || remote_theme.remote_url.blank? puts "Updating '#{theme.name}' for '#{RailsMultisite::ConnectionManagement.current_db}'..." remote_theme.update_from_remote From 1d69261bc03e85474e963c7085506d9b4318663f Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Wed, 1 Dec 2021 19:58:13 +0300 Subject: [PATCH 006/119] FIX: Set `auto_update` to false for non-git themes/components (#15157) Related to: https://github.com/discourse/discourse/commit/20f736aa112145efa3f09b6ab120904e2faa1b90. `auto_update` is true by default at the database level, but it doesn't make sense for `auto_update` to be true on themes that are not imported from a Git repository. --- app/controllers/admin/themes_controller.rb | 11 +++++++++-- app/models/remote_theme.rb | 2 +- spec/requests/admin/themes_controller_spec.rb | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index 33cfe6db3c..428f8ff579 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -59,7 +59,7 @@ class Admin::ThemesController < Admin::AdminController json = JSON::parse(params[:theme].read) theme = json['theme'] - @theme = Theme.new(name: theme["name"], user_id: theme_user.id) + @theme = Theme.new(name: theme["name"], user_id: theme_user.id, auto_update: false) theme["theme_fields"]&.each do |field| if field["raw_upload"] @@ -116,7 +116,14 @@ class Admin::ThemesController < Admin::AdminController update_components = params[:components] match_theme_by_name = !!params[:bundle] && !params.key?(:theme_id) # Old theme CLI behavior, match by name. Remove Jan 2020 begin - @theme = RemoteTheme.update_zipped_theme(bundle.path, bundle.original_filename, match_theme: match_theme_by_name, user: theme_user, theme_id: theme_id, update_components: update_components) + @theme = RemoteTheme.update_zipped_theme( + bundle.path, + bundle.original_filename, + match_theme: match_theme_by_name, + user: theme_user, + theme_id: theme_id, + update_components: update_components + ) log_theme_change(nil, @theme) render json: @theme, status: :created rescue RemoteTheme::ImportError => e diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 80e515cae7..d19f800c82 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -40,7 +40,7 @@ class RemoteTheme < ActiveRecord::Base existing = true if theme.blank? - theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"]) + theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"], auto_update: false) existing = false end diff --git a/spec/requests/admin/themes_controller_spec.rb b/spec/requests/admin/themes_controller_spec.rb index 92b7020e4f..391eb2290f 100644 --- a/spec/requests/admin/themes_controller_spec.rb +++ b/spec/requests/admin/themes_controller_spec.rb @@ -149,6 +149,7 @@ describe Admin::ThemesController do expect(json["theme"]["name"]).to eq("Sam's Simple Theme") expect(json["theme"]["theme_fields"].length).to eq(2) + expect(json["theme"]["auto_update"]).to eq(false) expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) end @@ -163,6 +164,7 @@ describe Admin::ThemesController do expect(json["theme"]["name"]).to eq("Header Icons") expect(json["theme"]["theme_fields"].length).to eq(5) + expect(json["theme"]["auto_update"]).to eq(false) expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) end @@ -219,6 +221,7 @@ describe Admin::ThemesController do expect(json["theme"]["name"]).to eq("Header Icons") expect(json["theme"]["id"]).not_to eq(existing_theme.id) expect(json["theme"]["theme_fields"].length).to eq(5) + expect(json["theme"]["auto_update"]).to eq(false) expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) end end From 64e1ca6daa54d02ac01fb893008a2ec1e9b6fc8c Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Wed, 1 Dec 2021 09:59:03 -0800 Subject: [PATCH 007/119] UX: only apply link formats on paste to selections that do not contain links --- .../app/mixins/textarea-text-manipulation.js | 3 ++- .../tests/integration/components/d-editor-test.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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 6dd67f74ad..f8ec19f1ff 100644 --- a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js +++ b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js @@ -285,7 +285,8 @@ export default Mixin.create({ this._cachedLinkify && plainText && !handled && - selected.end > selected.start + selected.end > selected.start && + !this._cachedLinkify.test(selectedValue) ) { if (this._cachedLinkify.test(plainText)) { const match = this._cachedLinkify.match(plainText)[0]; 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 aa0808ed86..623739695d 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 @@ -833,6 +833,19 @@ third line` } ); + testCase( + `pasting a url onto a selection that contains urls and other content will use default paste behavior`, + async function (assert, textarea) { + this.set("value", "Try https://www.discourse.org"); + setTextareaSelection(textarea, 0, 29); + const element = query(".d-editor"); + const event = await paste(element, "https://www.discourse.com/"); + // Synthetic paste events do not manipulate document content. + assert.strictEqual(this.value, "Try https://www.discourse.org"); + assert.strictEqual(event.defaultPrevented, false); + } + ); + (() => { // Tests to check cursor/selection after replace-text event. const BEFORE = "red green blue"; From df6e8b924e79839bc1ba9305788c920dcd2c8dc9 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Wed, 1 Dec 2021 19:30:33 +0100 Subject: [PATCH 008/119] DEV: Make legacy ember tests less likely to fail (#15147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …on launch --- lib/tasks/qunit.rake | 3 ++- test/run-qunit.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index 0f53c6109c..37c0da09f1 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -54,7 +54,8 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args| "DISCOURSE_SKIP_CSS_WATCHER" => "1", "UNICORN_LISTENER" => "127.0.0.1:#{unicorn_port}", "LOGSTASH_UNICORN_URI" => nil, - "UNICORN_WORKERS" => "3" + "UNICORN_WORKERS" => "1", + "UNICORN_TIMEOUT" => "90", } cmd = if ember_cli diff --git a/test/run-qunit.js b/test/run-qunit.js index 5c22125e24..2ec905fc50 100644 --- a/test/run-qunit.js +++ b/test/run-qunit.js @@ -148,7 +148,7 @@ async function runAllTests() { }); } - console.log("navigate to ", url); + console.log("navigate to", url); Page.navigate({ url }); Page.loadEventFired(async () => { From abe30a17da063a5f72c9535f9968e417099d5341 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 1 Dec 2021 18:31:52 +0000 Subject: [PATCH 009/119] DEV: Fix ember CLI bootstrap logic (#15160) When 1fa7a87f was rebased onto `main`, it didn't take into account the recent changes in c0781d7d. This commit updates the logic to work properly. --- app/assets/javascripts/discourse/lib/bootstrap-json/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index 197f9975be..1a31ad5f9f 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -264,7 +264,7 @@ async function handleRequest(proxy, baseURL, req, res) { res.set("content-security-policy", newCSP); } - const isHTML = response.headers["content-type"]?.startsWith("text/html"); + const isHTML = response.headers.get("content-type")?.startsWith("text/html"); const responseText = await response.text(); const preload = isHTML ? extractPreloadJson(responseText) : null; From bd140948e34f520b7851bf0da043f92fd7d26f37 Mon Sep 17 00:00:00 2001 From: Mark VanLandingham Date: Wed, 1 Dec 2021 13:24:16 -0600 Subject: [PATCH 010/119] DEV: Changes to support chat uploads (#15153) --- app/assets/javascripts/discourse/app/lib/uploads.js | 2 +- .../discourse/app/mixins/composer-upload-uppy.js | 1 + app/jobs/scheduled/clean_up_uploads.rb | 10 +++++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/app/lib/uploads.js b/app/assets/javascripts/discourse/app/lib/uploads.js index ba3a214b9f..e5c7239659 100644 --- a/app/assets/javascripts/discourse/app/lib/uploads.js +++ b/app/assets/javascripts/discourse/app/lib/uploads.js @@ -120,7 +120,7 @@ export function validateUploadedFile(file, opts) { return true; } -const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico|heic|heif|webp)/i; +export const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico|heic|heif|webp)/i; function extensionsToArray(exts) { return exts 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 8db042cc1d..a39c77c2cb 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -273,6 +273,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { fileName: file.name, id: file.id, progress: 0, + extension: file.extension, }) ); const placeholder = this._uploadPlaceholder(file); diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index db666f78d2..561691a797 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -39,9 +39,13 @@ module Jobs next if ReviewableQueuedPost.pending.where("payload->>'raw' LIKE '%#{upload.sha1}%' OR payload->>'raw' LIKE '%#{encoded_sha}%'").exists? next if Draft.where("data LIKE '%#{upload.sha1}%' OR data LIKE '%#{encoded_sha}%'").exists? next if UserProfile.where("bio_raw LIKE '%#{upload.sha1}%' OR bio_raw LIKE '%#{encoded_sha}%'").exists? - if defined?(ChatMessage) && - ChatMessage.where("message LIKE ? OR message LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists? - next + if defined?(ChatMessage) + # TODO after May 2022 - remove this. No longer needed as chat uploads are in a table + next if ChatMessage.where("message LIKE ? OR message LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists? + end + + if defined?(ChatUpload) + next if ChatUpload.where(upload: upload).exists? end upload.destroy else From 1b3d124a4ec48abf429a06428f9a9e85fbcbb418 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Wed, 1 Dec 2021 22:04:56 +0100 Subject: [PATCH 011/119] DEV: Don't use `?.` in bootstrap-json (#15162) That code is not transpiled, so it doesn't work on older node versions. --- .../discourse/lib/bootstrap-json/index.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index 1a31ad5f9f..cb38fbc67c 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -171,8 +171,13 @@ function replaceIn(bootstrap, template, id, headers, baseURL) { function extractPreloadJson(html) { const dom = new JSDOM(html); - return dom.window.document.querySelector("#data-preloaded")?.dataset - ?.preloaded; + const dataElement = dom.window.document.querySelector("#data-preloaded"); + + if (!dataElement || !dataElement.dataset) { + return; + } + + return dataElement.dataset.preloaded; } async function applyBootstrap(bootstrap, template, response, baseURL, preload) { @@ -264,17 +269,16 @@ async function handleRequest(proxy, baseURL, req, res) { res.set("content-security-policy", newCSP); } - const isHTML = response.headers.get("content-type")?.startsWith("text/html"); + const contentType = response.headers.get("content-type"); const responseText = await response.text(); - const preload = isHTML ? extractPreloadJson(responseText) : null; - if (preload) { + if (contentType && contentType.startsWith("text/html")) { const html = await buildFromBootstrap( proxy, baseURL, req, response, - preload + extractPreloadJson(responseText) ); res.set("content-type", "text/html"); res.send(html); From 82cd2596c8983bc24342af95f9a6ed7708c1990d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Dec 2021 22:17:41 +0100 Subject: [PATCH 012/119] Build(deps-dev): Bump rubocop-discourse from 2.4.2 to 2.5.0 (#15165) Bumps [rubocop-discourse](https://github.com/discourse/rubocop-discourse) from 2.4.2 to 2.5.0. - [Release notes](https://github.com/discourse/rubocop-discourse/releases) - [Commits](https://github.com/discourse/rubocop-discourse/compare/v2.4.2...v2.5.0) --- updated-dependencies: - dependency-name: rubocop-discourse dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 06868e8af4..5c8b9c96d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -400,7 +400,7 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.13.0) parser (>= 3.0.1.1) - rubocop-discourse (2.4.2) + rubocop-discourse (2.5.0) rubocop (>= 1.1.0) rubocop-rspec (>= 2.0.0) rubocop-rspec (2.6.0) From da9a226bcb765268b40d890ee2dc9a264fc87811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Dec 2021 22:28:04 +0100 Subject: [PATCH 013/119] Build(deps): Bump logster from 2.10.0 to 2.10.1 (#15163) Bumps [logster](https://github.com/discourse/logster) from 2.10.0 to 2.10.1. - [Release notes](https://github.com/discourse/logster/releases) - [Changelog](https://github.com/discourse/logster/blob/main/CHANGELOG.md) - [Commits](https://github.com/discourse/logster/compare/v2.10.0...v2.10.1) --- updated-dependencies: - dependency-name: logster dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5c8b9c96d6..6c5be3c315 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,7 +215,7 @@ GEM logstash-event (1.2.02) logstash-logger (0.26.1) logstash-event (~> 1.2) - logster (2.10.0) + logster (2.10.1) loofah (2.12.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) From 3a73028a70309fd2597a397d05e2ba5c542b298c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 2 Dec 2021 15:08:22 +1100 Subject: [PATCH 014/119] PERF: migrate normalized_emails in a migrations (#15166) Old OnceOff job could perform pretty slowly on sites with millions of emails New implementation operates in batches in a migration, minimizing locking. --- app/jobs/onceoff/migrate_normalized_emails.rb | 11 ---- ...21028_migrate_email_to_normalized_email.rb | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) delete mode 100644 app/jobs/onceoff/migrate_normalized_emails.rb create mode 100644 db/post_migrate/20211201221028_migrate_email_to_normalized_email.rb diff --git a/app/jobs/onceoff/migrate_normalized_emails.rb b/app/jobs/onceoff/migrate_normalized_emails.rb deleted file mode 100644 index 4b1ff23c0f..0000000000 --- a/app/jobs/onceoff/migrate_normalized_emails.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Jobs - class MigrateNormalizedEmails < ::Jobs::Onceoff - def execute_onceoff(args) - ::UserEmail.find_each do |user_email| - user_email.update(normalized_email: user_email.normalize_email) - end - end - end -end diff --git a/db/post_migrate/20211201221028_migrate_email_to_normalized_email.rb b/db/post_migrate/20211201221028_migrate_email_to_normalized_email.rb new file mode 100644 index 0000000000..2839c8f2fd --- /dev/null +++ b/db/post_migrate/20211201221028_migrate_email_to_normalized_email.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class MigrateEmailToNormalizedEmail < ActiveRecord::Migration[6.1] + + # minimize locking on user_email table + disable_ddl_transaction! + + def up + + min, max = DB.query_single "SELECT MIN(id), MAX(id) FROM user_emails" + # scaling is needed to compensate for "holes" where records were deleted + # and pathological cases where for some reason id 100_000_000 and 0 exist + + # avoid doing any work on empty dbs + return if min.nil? + + bounds = DB.query_single <<~SQL + SELECT t.id + FROM ( + SELECT *, row_number() OVER(ORDER BY id ASC) AS row + FROM user_emails + ) t + WHERE t.row % 100000 = 0 + SQL + + # subtle but loop does < not <= + # includes low, excludes high + bounds << (max + 1) + + low_id = min + bounds.each do |high_id| + + # using execute cause MiniSQL is not logging at the moment + # to_i is not needed, but specified so it is explicit there is no SQL injection + execute <<~SQL + UPDATE user_emails + SET normalized_email = REPLACE(REGEXP_REPLACE(email,'([+@].*)',''),'.','') || REGEXP_REPLACE(email, '[^@]*', '') + WHERE (normalized_email IS NULL OR normalized_email <> (REPLACE(REGEXP_REPLACE(email,'([+@].*)',''),'.','') || REGEXP_REPLACE(email, '[^@]*', ''))) + AND (id >= #{low_id.to_i} AND id < #{high_id.to_i}) + SQL + + low_id = high_id + end + + end + + def down + execute "UPDATE user_emails SET normalized_email = null" + end +end From 44d16fcd8e267307f0f8a06b4cfaa7bb51cbb48d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 2 Dec 2021 10:58:03 +0000 Subject: [PATCH 015/119] DEV: Print full stack trace on ember-cli bootstrap error (#15167) --- app/assets/javascripts/discourse/lib/bootstrap-json/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index cb38fbc67c..6cea5eeb84 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -321,8 +321,8 @@ to serve API requests. For example: } catch (error) { res.send(` -

Discourse Build Error

-
${error}
+

Discourse Ember CLI Proxy Error

+
${error.stack}
`); } finally { From ceca34aca6cc34113446eaabb2bcf91415f29aad Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 2 Dec 2021 10:58:54 +0000 Subject: [PATCH 016/119] DEV: Ensure ember-cli does not attempt to bootstrap non-ember pages (#15168) 1b3d124a introduced a logic change which meant that we attempted to bootstrap, even on pages without any `preloadJson` (i.e. non-ember HTML pages from Discourse). This commit restores the original logic, making sure to avoid `?.`. --- app/assets/javascripts/discourse/lib/bootstrap-json/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index 6cea5eeb84..bf3b1cc7c4 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -270,9 +270,11 @@ async function handleRequest(proxy, baseURL, req, res) { } const contentType = response.headers.get("content-type"); + const isHTML = contentType && contentType.startsWith("text/html"); const responseText = await response.text(); + const preloadJson = isHTML ? extractPreloadJson(responseText) : null; - if (contentType && contentType.startsWith("text/html")) { + if (preloadJson) { const html = await buildFromBootstrap( proxy, baseURL, From 1c0022c195fa579540abbea915a8c46c99eed791 Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Thu, 2 Dec 2021 14:42:23 +0100 Subject: [PATCH 017/119] FIX: extract and fix overriding of usernames by external auth (#14637) --- app/models/discourse_single_sign_on.rb | 6 +- app/models/user.rb | 4 ++ app/services/username_changer.rb | 16 ++++++ lib/auth/result.rb | 5 +- spec/models/user_spec.rb | 26 +++++++++ spec/services/username_changer_spec.rb | 80 +++++++++++++++++++++++++- 6 files changed, 128 insertions(+), 9 deletions(-) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 31a4b830ad..5b553f14bf 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -323,11 +323,7 @@ class DiscourseSingleSignOn < SingleSignOn end if SiteSetting.auth_overrides_username? && username.present? - if user.username.downcase == username.downcase - user.username = username # there may be a change of case - elsif user.username != UserNameSuggester.fix_username(username) - user.username = UserNameSuggester.suggest(username) - end + UsernameChanger.override(user, username) end if SiteSetting.auth_overrides_name && user.name != name && name.present? diff --git a/app/models/user.rb b/app/models/user.rb index 55afe32263..fc62d99047 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1455,6 +1455,10 @@ class User < ActiveRecord::Base seen_since?(30.days.ago) end + def username_equals_to?(another_username) + username_lower == User.normalize_username(another_username) + end + protected def badge_grant diff --git a/app/services/username_changer.rb b/app/services/username_changer.rb index 0c8c472805..fff781ab97 100644 --- a/app/services/username_changer.rb +++ b/app/services/username_changer.rb @@ -13,7 +13,23 @@ class UsernameChanger self.new(user, new_username, actor).change end + def self.override(user, new_username) + if user.username_equals_to?(new_username) + # override anyway since case could've been changed: + UsernameChanger.change(user, new_username, user) + true + elsif user.username != UserNameSuggester.fix_username(new_username) + suggested_username = UserNameSuggester.suggest(new_username) + UsernameChanger.change(user, suggested_username, user) + true + else + false + end + end + def change(asynchronous: true, run_update_job: true) + return false if @user.username == @new_username + @user.username = @new_username if @user.save diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 5f68606d2a..a5732810d1 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -77,9 +77,8 @@ class Auth::Result def apply_user_attributes! change_made = false - if SiteSetting.auth_overrides_username? && username.present? && UserNameSuggester.fix_username(username) != user.username - user.username = UserNameSuggester.suggest(username) - change_made = true + if SiteSetting.auth_overrides_username? && username.present? + change_made = UsernameChanger.override(user, username) end if SiteSetting.auth_overrides_email && email_valid && email.present? && user.email != Email.downcase(email) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f1b2f7a27d..564503d6b3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2601,4 +2601,30 @@ describe User do expect(user.invited_by).to eq(invite.invited_by) end end + + describe "#username_equals_to?" do + [ + ["returns true for equal usernames", "john", "john", true], + ["returns false for different usernames", "john", "bill", false], + ["considers usernames that are different only in case as equal", "john", "JoHN", true] + ].each do |testcase_name, current_username, another_username, is_equal| + it "#{testcase_name}" do + user = Fabricate(:user, username: current_username) + result = user.username_equals_to?(another_username) + + expect(result).to be(is_equal) + end + end + + it "considers usernames that are equal after unicode normalization as equal" do + SiteSetting.unicode_usernames = true + + raw = "Lo\u0308we" # Löwe, u0308 stands for ¨, so o\u0308 adds up to ö + normalized = "l\u00F6we" # Löwe normilized, \u00F6 stands for ö + user = Fabricate(:user, username: normalized) + result = user.username_equals_to?(raw) + + expect(result).to be(true) + end + end end diff --git a/spec/services/username_changer_spec.rb b/spec/services/username_changer_spec.rb index c0d935e9c8..a95f64e2a7 100644 --- a/spec/services/username_changer_spec.rb +++ b/spec/services/username_changer_spec.rb @@ -12,9 +12,10 @@ describe UsernameChanger do context 'success' do let!(:old_username) { user.username } - let(:new_username) { "#{user.username}1234" } it 'should change the username' do + new_username = "#{user.username}1234" + events = DiscourseEvent.track_events { @result = UsernameChanger.change(user, new_username) }.last(2) @@ -34,6 +35,17 @@ describe UsernameChanger do expect(user.username).to eq(new_username) expect(user.username_lower).to eq(new_username.downcase) end + + it 'do nothing if the new username is the same' do + new_username = user.username + + events = DiscourseEvent.track_events { + @result = UsernameChanger.change(user, new_username) + } + + expect(@result).to eq(false) + expect(events.count).to be_zero + end end context 'failure' do @@ -591,4 +603,70 @@ describe UsernameChanger do end end + describe '#override' do + common_test_cases = [ + [ + "overrides the username if a new name is different", + "john", "bill", "bill", false + ], + [ + "does not change the username if a new name is the same", + "john", "john", "john", false + ], + [ + "overrides the username if a new name has different case", + "john", "JoHN", "JoHN", false + ] + ] + + context "unicode_usernames is off" do + before do + SiteSetting.unicode_usernames = false + end + + [ + *common_test_cases, + [ + "does not change the username if a new name after unicode normalization is the same", + "john", "john¥¥", "john" + ], + ].each do |testcase_name, current, new, overrode| + it "#{testcase_name}" do + user = Fabricate(:user, username: current) + UsernameChanger.override(user, new) + expect(user.username).to eq(overrode) + end + end + + it "overrides the username with username suggestions in case the username is already taken" do + user = Fabricate(:user, username: "bill") + Fabricate(:user, username: "john") + + UsernameChanger.override(user, "john") + + expect(user.username).to eq("john1") + end + end + + context "unicode_usernames is on" do + before do + SiteSetting.unicode_usernames = true + end + + [ + *common_test_cases, + [ + "overrides the username if a new name after unicode normalization is different only in case", + "lo\u0308we", "L\u00F6wee", "L\u00F6wee" + ], + ].each do |testcase_name, current, new, overrode| + it "#{testcase_name}" do + user = Fabricate(:user, username: current) + UsernameChanger.override(user, new) + expect(user.username).to eq(overrode) + end + end + end + end + end From 732678f642015b08ba22d0995dc74c13a086ac92 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 2 Dec 2021 09:45:33 -0500 Subject: [PATCH 018/119] UX: Fix alignment in group navigation bar (#15169) Same as #15145. --- .../stylesheets/common/components/navs.scss | 7 +++-- app/assets/stylesheets/desktop/group.scss | 30 ------------------- .../javascripts/discourse/lib/dummy-data.js | 2 ++ .../styleguide/molecules/navigation-bar.hbs | 12 ++++++++ 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 6bef0c18f1..49a6a767fd 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -21,9 +21,12 @@ @extend %nav; @extend .clearfix; + display: flex; + flex-direction: row; + align-items: stretch; + > li { display: flex; - float: left; margin-right: 0.5em; > a { @@ -36,7 +39,7 @@ min-height: 30px; display: flex; align-items: center; - transition: background 0.15s; + transition: background-color 0.2s, color 0.2s; .d-icon { margin-right: 5px; diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss index 606b402ff8..0b095a67ab 100644 --- a/app/assets/stylesheets/desktop/group.scss +++ b/app/assets/stylesheets/desktop/group.scss @@ -1,33 +1,3 @@ -.group-nav { - .group-dropdown { - margin-right: 10px; - } -} - -.group-navigation { - width: 15%; - background-color: transparent; - - li { - border: none; - - a { - color: var(--primary-med-or-secondary-high); - padding: 8px 0; - - &.active { - background-color: transparent; - font-weight: bold; - color: var(--primary); - - &:after { - display: none; - } - } - } - } -} - .group-activity-outlet, .group-messages-outlet, .group-manage-outlet { diff --git a/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js b/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js index df2f90450e..29597f9e57 100644 --- a/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js +++ b/plugins/styleguide/assets/javascripts/discourse/lib/dummy-data.js @@ -262,6 +262,8 @@ export function createData(store) { { name: "admin", id: 3, automatic: false }, ], + groupNames: ["staff", "lounge", "admin"], + selectedGroups: [1, 2], settings: "bold|italic|strike|underline", diff --git a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/molecules/navigation-bar.hbs b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/molecules/navigation-bar.hbs index 53a46ed927..d62354700e 100644 --- a/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/molecules/navigation-bar.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/templates/styleguide/molecules/navigation-bar.hbs @@ -9,3 +9,15 @@ {{/each}} {{/mobile-nav}} {{/styleguide-example}} + +{{#styleguide-example title="group page navigation-bar"}} + {{#mobile-nav class="group-nav" desktopClass="nav nav-pills"}} +
  • + {{group-dropdown groups=dummy.groupNames value="staff"}} +
  • + + {{#each dummy.navItems as |ni|}} +
  • {{ni.displayName}}
  • + {{/each}} + {{/mobile-nav}} +{{/styleguide-example}} From 55cbc70f3f5452305b91d52285797ec7a95684a2 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 2 Dec 2021 15:03:45 +0000 Subject: [PATCH 019/119] DEV: Ensure redirects are passed through to the client by ember-cli (#15170) By default, `fetch` will transparently follow redirects, even across domain boundaries --- app/assets/javascripts/discourse/lib/bootstrap-json/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index bf3b1cc7c4..07071809dd 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -240,6 +240,7 @@ async function handleRequest(proxy, baseURL, req, res) { method: req.method, body: /GET|HEAD/.test(req.method) ? null : req.body, headers: req.headers, + redirect: "manual", }); response.headers.forEach((value, header) => { From cfb6199a9511773c7eeffe16e42ad62cc45138ce Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 2 Dec 2021 15:12:25 +0000 Subject: [PATCH 020/119] FIX: Don't redirect XHR/JSON requests when login is required (#15093) When redirecting to login, we store a destination_url cookie, which the user is then redirected to after login. We never want the user to be redirected to a JSON URL. Instead, we should return a 403 in these situations. This should also be much less confusing for API consumers - a 403 is a better representation than a 302. --- app/controllers/application_controller.rb | 6 ++- spec/integration/api_keys_spec.rb | 4 +- spec/requests/application_controller_spec.rb | 8 ++++ spec/requests/topics_controller_spec.rb | 39 ++++++++++++++++---- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7dd73b3f82..dbe83dc2ef 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -819,7 +819,11 @@ class ApplicationController < ActionController::Base if !current_user && SiteSetting.login_required? flash.keep - redirect_to_login + if (request.format && request.format.json?) || request.xhr? || !request.get? + ensure_logged_in + else + redirect_to_login + end return end diff --git a/spec/integration/api_keys_spec.rb b/spec/integration/api_keys_spec.rb index 0e8d7249b0..60598cfbcf 100644 --- a/spec/integration/api_keys_spec.rb +++ b/spec/integration/api_keys_spec.rb @@ -45,7 +45,7 @@ describe 'api keys' do # Confirm not allowed for json get "/latest.json?api_key=#{api_key.key}&api_username=#{user.username.downcase}" - expect(response.status).to eq(302) + expect(response.status).to eq(403) end context "with a plugin registered filter" do @@ -96,7 +96,7 @@ describe 'user api keys' do # Confirm not allowed for json get "/latest.json?user_api_key=#{user_api_key.key}" - expect(response.status).to eq(302) + expect(response.status).to eq(403) end it "can restrict scopes by parameters" do diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 112e637271..4ffee8d2e3 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -120,6 +120,14 @@ RSpec.describe ApplicationController do expect(response.body).not_to include("data-authentication-data=") expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie end + + it "returns a 403 for json requests" do + get '/latest' + expect(response.status).to eq(302) + + get '/latest.json' + expect(response.status).to eq(403) + end end describe '#redirect_to_second_factor_if_required' do diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 7a76aa043a..875fc50733 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -1928,19 +1928,21 @@ RSpec.describe TopicsController do let!(:nonexistent_topic_id) { Topic.last.id + 10000 } fab!(:secure_accessible_topic) { Fabricate(:topic, category: accessible_category) } - shared_examples "various scenarios" do |expected| + shared_examples "various scenarios" do |expected, request_json: false| expected.each do |key, value| it "returns #{value} for #{key}" do slug = key == :nonexistent ? "garbage-slug" : send(key.to_s).slug topic_id = key == :nonexistent ? nonexistent_topic_id : send(key.to_s).id - get "/t/#{slug}/#{topic_id}.json" + format = request_json ? ".json" : "" + get "/t/#{slug}/#{topic_id}#{format}" expect(response.status).to eq(value) end end expected_slug_response = expected[:secure_topic] == 200 ? 301 : expected[:secure_topic] it "will return a #{expected_slug_response} when requesting a secure topic by slug" do - get "/t/#{secure_topic.slug}" + format = request_json ? ".json" : "" + get "/t/#{secure_topic.slug}#{format}" expect(response.status).to eq(expected_slug_response) end end @@ -1981,6 +1983,23 @@ RSpec.describe TopicsController do include_examples "various scenarios", expected end + context 'anonymous with login required, requesting json' do + before do + SiteSetting.login_required = true + end + expected = { + normal_topic: 403, + secure_topic: 403, + private_topic: 403, + deleted_topic: 403, + deleted_secure_topic: 403, + deleted_private_topic: 403, + nonexistent: 403, + secure_accessible_topic: 403 + } + include_examples "various scenarios", expected, request_json: true + end + context 'normal user' do before do sign_in(user) @@ -2070,7 +2089,7 @@ RSpec.describe TopicsController do nonexistent: 404, secure_accessible_topic: 403 } - include_examples "various scenarios", expected + include_examples "various scenarios", expected, request_json: true end context 'anonymous with login required' do @@ -2105,7 +2124,7 @@ RSpec.describe TopicsController do nonexistent: 404, secure_accessible_topic: 403 } - include_examples "various scenarios", expected + include_examples "various scenarios", expected, request_json: true end context 'allowed user' do @@ -2395,12 +2414,16 @@ RSpec.describe TopicsController do context 'and the user is not logged in' do let(:api_key) { Fabricate(:api_key, user: topic.user) } - it 'redirects to the login page' do - get "/t/#{topic.slug}/#{topic.id}.json" - + it 'redirects browsers to the login page' do + get "/t/#{topic.slug}/#{topic.id}" expect(response).to redirect_to login_path end + it 'raises a 403 for json requests' do + get "/t/#{topic.slug}/#{topic.id}.json" + expect(response.status).to eq(403) + end + it 'shows the topic if valid api key is provided' do get "/t/#{topic.slug}/#{topic.id}.json", headers: { "HTTP_API_KEY" => api_key.key } From 2f04a9b9fb850df90bc021d7b5ee73cf61ab6c61 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Thu, 2 Dec 2021 09:33:03 -0600 Subject: [PATCH 021/119] DEV: Remove site_setting_saved event (#15164) We said we would drop it from 2.4, so this is long overdue Co-authored-by: Jarek Radosz --- app/models/site_setting.rb | 5 -- lib/discourse_event.rb | 2 +- lib/site_settings/local_process_provider.rb | 1 - .../components/site_setting_extension_spec.rb | 53 ++----------------- 4 files changed, 4 insertions(+), 57 deletions(-) diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 8f0b251628..33799aba84 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -7,11 +7,6 @@ class SiteSetting < ActiveRecord::Base validates_presence_of :name validates_presence_of :data_type - after_save do |site_setting| - DiscourseEvent.trigger(:site_setting_saved, site_setting) - true - end - def self.load_settings(file, plugin: nil) SiteSettings::YamlLoader.new(file).load do |category, name, default, opts| setting(name, default, opts.merge(category: category, plugin: plugin)) diff --git a/lib/discourse_event.rb b/lib/discourse_event.rb index b2e03b8b1d..c2db7d1002 100644 --- a/lib/discourse_event.rb +++ b/lib/discourse_event.rb @@ -17,7 +17,7 @@ class DiscourseEvent def self.on(event_name, &block) if event_name == :site_setting_saved - Discourse.deprecate("The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", since: "2.3.0beta8", drop_from: "2.4") + Discourse.deprecate("The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", since: "2.3.0beta8", drop_from: "2.4", raise_error: true) end events[event_name] << block end diff --git a/lib/site_settings/local_process_provider.rb b/lib/site_settings/local_process_provider.rb index 4294a3f02e..37e5faad56 100644 --- a/lib/site_settings/local_process_provider.rb +++ b/lib/site_settings/local_process_provider.rb @@ -45,7 +45,6 @@ class SiteSettings::LocalProcessProvider settings[name] = setting end setting.value = value.to_s - DiscourseEvent.trigger(:site_setting_saved, setting) setting end diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 5be49b0ff0..956a390fae 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -176,9 +176,9 @@ describe SiteSettingExtension do no_change_events = DiscourseEvent.track_events { settings.test_setting = 2 } default_events = DiscourseEvent.track_events { settings.test_setting = 1 } - expect(override_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_changed, :site_setting_saved) - expect(no_change_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_saved) - expect(default_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_changed, :site_setting_saved) + expect(override_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_changed) + expect(no_change_events.map { |e| e[:event_name] }).to be_empty + expect(default_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_changed) changed_event_1 = override_events.find { |e| e[:event_name] == :site_setting_changed } changed_event_2 = default_events.find { |e| e[:event_name] == :site_setting_changed } @@ -186,53 +186,6 @@ describe SiteSettingExtension do expect(changed_event_1[:params]).to eq([:test_setting, 1, 2]) expect(changed_event_2[:params]).to eq([:test_setting, 2, 1]) end - - it "provides the correct values when using site_setting_changed" do - event_new_value = nil - event_old_value = nil - site_setting_value = nil - - test_lambda = -> (name, old_val, new_val) do - event_old_value = old_val - event_new_value = new_val - site_setting_value = settings.test_setting - end - - begin - DiscourseEvent.on(:site_setting_changed, &test_lambda) - settings.test_setting = 2 - ensure - DiscourseEvent.off(:site_setting_changed, &test_lambda) - end - - expect(event_old_value).to eq(1) - expect(event_new_value).to eq(2) - expect(site_setting_value).to eq(2) - end - - it "can produce confusing results when using site_setting_saved" do - # site_setting_saved is deprecated. This test case illustrates why it can be confusing - - active_record_value = nil - site_setting_value = nil - - test_lambda = -> (setting) do - active_record_value = setting.value - site_setting_value = settings.test_setting - end - - begin - DiscourseEvent.on(:site_setting_saved, &test_lambda) - settings.test_setting = 2 - ensure - DiscourseEvent.off(:site_setting_saved, &test_lambda) - end - - # Problem 1, the site_setting_changed event gives us the database value, not the ruby value - expect(active_record_value).to eq("2") - # Problem 2, calling SiteSetting.test_setting inside the event will still return the old value - expect(site_setting_value).to eq(1) - end end describe "int setting" do From 9b5836aa1db71973569f3efe18f807e2aecaa45d Mon Sep 17 00:00:00 2001 From: Michelle Bueno Saquetim Vendrame Date: Thu, 2 Dec 2021 17:11:55 +0000 Subject: [PATCH 022/119] Add three reports (#14338) * Add report top_users_by_received_likes * Add report top_users_by_received_likes_from_inferior_trust_level * Add report top_users_by_likes_received_from_a_variety_of_people * Add test to report_top_users_by_received_likes * add top_users_by_likes_received_from_a_variety_of_people report test * add top_users_by_likes_received_from_inferior_trust_level report tests --- .../reports/top_users_by_likes_received.rb | 58 ++++++++++ ...likes_received_from_a_variety_of_people.rb | 60 ++++++++++ ...ikes_received_from_inferior_trust_level.rb | 71 ++++++++++++ app/models/report.rb | 3 + config/locales/server.en.yml | 19 ++++ install-imagemagick | 86 +++++++++++++++ spec/models/report_spec.rb | 104 ++++++++++++++++++ 7 files changed, 401 insertions(+) create mode 100644 app/models/concerns/reports/top_users_by_likes_received.rb create mode 100644 app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb create mode 100644 app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb create mode 100755 install-imagemagick diff --git a/app/models/concerns/reports/top_users_by_likes_received.rb b/app/models/concerns/reports/top_users_by_likes_received.rb new file mode 100644 index 0000000000..2f1b7c1132 --- /dev/null +++ b/app/models/concerns/reports/top_users_by_likes_received.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Reports::TopUsersByLikesReceived + extend ActiveSupport::Concern + + class_methods do + def report_top_users_by_likes_received(report) + report.icon = 'heart' + report.data = [] + + report.modes = [:table] + + report.dates_filtering = true + + report.labels = [ + { + type: :user, + properties: { + id: :user_id, + username: :username, + avatar: :user_avatar_template, + }, + title: I18n.t("reports.top_users_by_likes_received.labels.user") + }, + { + type: :number, + property: :qtt_like, + title: I18n.t("reports.top_users_by_likes_received.labels.qtt_like") + }, + ] + + sql = <<~SQL + SELECT + ua.user_id AS user_id, + u.username as username, + u.uploaded_avatar_id as uploaded_avatar_id, + COUNT(*) qtt_like + FROM user_actions ua + INNER JOIN users u on ua.user_id = u.id + WHERE ua.created_at::date BETWEEN :start_date AND :end_date + AND ua.action_type = 2 + GROUP BY ua.user_id, u.username, u.uploaded_avatar_id + ORDER BY qtt_like DESC + LIMIT 10 + SQL + + DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| + report.data << { + user_id: row.user_id, + username: row.username, + user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + qtt_like: row.qtt_like, + } + end + + end + end +end diff --git a/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb b/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb new file mode 100644 index 0000000000..0bf6a7348c --- /dev/null +++ b/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Reports::TopUsersByLikesReceivedFromAVarietyOfPeople + extend ActiveSupport::Concern + + class_methods do + def report_top_users_by_likes_received_from_a_variety_of_people(report) + report.icon = 'heart' + report.data = [] + + report.modes = [:table] + + report.dates_filtering = true + + report.labels = [ + { + type: :user, + properties: { + id: :user_id, + username: :username, + avatar: :user_avatar_template, + }, + title: I18n.t("reports.top_users_by_likes_received_from_a_variety_of_people.labels.user") + }, + { + type: :number, + property: :qtt_like, + title: I18n.t("reports.top_users_by_likes_received_from_a_variety_of_people.labels.qtt_like") + }, + ] + + sql = <<~SQL + SELECT + p.user_id, + u.username as username, + u.uploaded_avatar_id as uploaded_avatar_id, + COUNT(DISTINCT ua.user_id) qtt_like + FROM user_actions ua + INNER JOIN posts p ON p.id = ua.target_post_id + INNER JOIN users u on p.user_id = u.id + WHERE ua.created_at::date BETWEEN :start_date AND :end_date + AND ua.action_type = 1 + AND p.user_id > 0 + GROUP BY p.user_id, u.username, u.uploaded_avatar_id + ORDER BY qtt_like DESC + LIMIT 10 + SQL + + DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| + report.data << { + user_id: row.user_id, + username: row.username, + user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + qtt_like: row.qtt_like, + } + end + + end + end +end diff --git a/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb b/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb new file mode 100644 index 0000000000..d0b41d57d6 --- /dev/null +++ b/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Reports::TopUsersByLikesReceivedFromInferiorTrustLevel + extend ActiveSupport::Concern + + class_methods do + def report_top_users_by_likes_received_from_inferior_trust_level(report) + report.icon = 'heart' + report.data = [] + + report.modes = [:table] + + report.dates_filtering = true + + report.labels = [ + { + type: :user, + properties: { + id: :user_id, + username: :username, + avatar: :user_avatar_template, + }, + title: I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.user") + }, + { + type: :number, + property: :trust_level, + title: I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.trust_level") + }, + { + type: :number, + property: :qtt_like, + title: I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.qtt_like") + }, + ] + + sql = <<~SQL + WITH user_liked_tl_lower AS ( + SELECT + users.id user_id, + users.username as username, + users.uploaded_avatar_id as uploaded_avatar_id, + users.trust_level, + COUNT(*) qtt_like, + rank() OVER (PARTITION BY users.trust_level ORDER BY COUNT(*) DESC) + FROM users + INNER JOIN posts p ON p.user_id = users.id + INNER JOIN user_actions ua ON ua.target_post_id = p.id AND ua.action_type = 1 + INNER JOIN users u_liked ON ua.user_id = u_liked.id AND u_liked.trust_level < users.trust_level + WHERE ua.created_at::date BETWEEN :start_date AND :end_date + GROUP BY users.id + ORDER BY trust_level DESC, qtt_like DESC + ) + + SELECT * FROM user_liked_tl_lower + WHERE rank <= 10 + SQL + + DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| + report.data << { + user_id: row.user_id, + username: row.username, + user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + trust_level: row.trust_level, + qtt_like: row.qtt_like, + } + end + + end + end +end diff --git a/app/models/report.rb b/app/models/report.rb index 4bd185641a..91a6a808b6 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -46,6 +46,9 @@ class Report include Reports::ModeratorWarningPrivateMessages include Reports::ProfileViews include Reports::TopUploads + include Reports::TopUsersByLikesReceived + include Reports::TopUsersByLikesReceivedFromInferiorTrustLevel + include Reports::TopUsersByLikesReceivedFromAVarietyOfPeople attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :labels, :prev_period, :facets, :limit, :average, diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 14301fe56a..1baf352b26 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1420,6 +1420,25 @@ en: ignores_count: Ignores count mutes_count: Mutes count description: "Users who have been muted and/or ignored by many other users." + top_users_by_likes_received: + title: "Top Users by likes received" + labels: + user: User + qtt_like: Likes Received + description: "Top 10 users who have been received more likes." + top_users_by_likes_received_from_inferior_trust_level: + title: "Top Users by likes received from a user with a lower trust level" + labels: + user: User + trust_level: Trust level + qtt_like: Likes Received + description: "Top 10 users in a higher trust level being liked by people in a lower trust level." + top_users_by_likes_received_from_a_variety_of_people: + title: "Top Users by likes received from a variety of people" + labels: + user: User + qtt_like: Likes Received + description: "Top 10 users who have had the likes from a wide range of people." dashboard: rails_env_warning: "Your server is running in %{env} mode." diff --git a/install-imagemagick b/install-imagemagick new file mode 100755 index 0000000000..7122613547 --- /dev/null +++ b/install-imagemagick @@ -0,0 +1,86 @@ +#!/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/spec/models/report_spec.rb b/spec/models/report_spec.rb index 6c5d22d612..ccb1caef6e 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -1325,4 +1325,108 @@ describe Report do end end end + + describe 'top_users_by_likes_received' do + let(:report) { Report.find('top_users_by_likes_received') } + + include_examples 'no data' + + context 'with data' do + before do + user_1 = Fabricate(:user, username: "jonah") + user_2 = Fabricate(:user, username: "jake") + user_3 = Fabricate(:user, username: "john") + + 3.times { UserAction.create!(user_id: user_1.id, action_type: UserAction::WAS_LIKED) } + 9.times { UserAction.create!(user_id: user_2.id, action_type: UserAction::WAS_LIKED) } + 6.times { UserAction.create!(user_id: user_3.id, action_type: UserAction::WAS_LIKED) } + end + + it "with category filtering" do + report = Report.find('top_users_by_likes_received') + + expect(report.data.length).to eq(3) + expect(report.data[0][:username]).to eq("jake") + expect(report.data[1][:username]).to eq("john") + expect(report.data[2][:username]).to eq("jonah") + end + end + end + + describe 'top_users_by_likes_received_from_a_variety_of_people' do + let(:report) { Report.find('top_users_by_likes_received_from_a_variety_of_people') } + + include_examples 'no data' + + context 'with data' do + before do + user_1 = Fabricate(:user, username: "jonah") + user_2 = Fabricate(:user, username: "jake") + user_3 = Fabricate(:user, username: "john") + user_4 = Fabricate(:user, username: "joseph") + user_5 = Fabricate(:user, username: "joanne") + user_6 = Fabricate(:user, username: "jerome") + + topic_1 = Fabricate(:topic, user: user_1) + topic_2 = Fabricate(:topic, user: user_2) + topic_3 = Fabricate(:topic, user: user_3) + + post_1 = Fabricate(:post, topic: topic_1, user: user_1) + post_2 = Fabricate(:post, topic: topic_2, user: user_2) + post_3 = Fabricate(:post, topic: topic_3, user: user_3) + + 3.times { UserAction.create!(user_id: user_4.id, target_post_id: post_1.id, action_type: UserAction::LIKE) } + 6.times { UserAction.create!(user_id: user_5.id, target_post_id: post_2.id, action_type: UserAction::LIKE) } + 9.times { UserAction.create!(user_id: user_6.id, target_post_id: post_3.id, action_type: UserAction::LIKE) } + + end + + it "with category filtering" do + report = Report.find('top_users_by_likes_received_from_a_variety_of_people') + + expect(report.data.length).to eq(3) + expect(report.data[0][:username]).to eq("jonah") + expect(report.data[1][:username]).to eq("jake") + expect(report.data[2][:username]).to eq("john") + end + end + end + + describe 'top_users_by_likes_received_from_inferior_trust_level' do + let(:report) { Report.find('top_users_by_likes_received_from_inferior_trust_level') } + + include_examples 'no data' + + context 'with data' do + before do + user_1 = Fabricate(:user, username: "jonah", trust_level: 2) + user_2 = Fabricate(:user, username: "jake", trust_level: 2) + user_3 = Fabricate(:user, username: "john", trust_level: 2) + user_4 = Fabricate(:user, username: "joseph", trust_level: 1) + user_5 = Fabricate(:user, username: "joanne", trust_level: 1) + user_6 = Fabricate(:user, username: "jerome", trust_level: 2) + + topic_1 = Fabricate(:topic, user: user_1) + topic_2 = Fabricate(:topic, user: user_2) + topic_3 = Fabricate(:topic, user: user_3) + + post_1 = Fabricate(:post, topic: topic_1, user: user_1) + post_2 = Fabricate(:post, topic: topic_2, user: user_2) + post_3 = Fabricate(:post, topic: topic_3, user: user_3) + + 3.times { UserAction.create!(user_id: user_4.id, target_post_id: post_1.id, action_type: UserAction::LIKE) } + 6.times { UserAction.create!(user_id: user_5.id, target_post_id: post_2.id, action_type: UserAction::LIKE) } + 9.times { UserAction.create!(user_id: user_6.id, target_post_id: post_3.id, action_type: UserAction::LIKE) } + + end + + it "with category filtering" do + report = Report.find('top_users_by_likes_received_from_inferior_trust_level') + + expect(report.data.length).to eq(2) + expect(report.data[0][:username]).to eq("jake") + expect(report.data[1][:username]).to eq("jonah") + end + end + end end From bd10f113e92b46314115a90ea4783b2ed5420210 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Thu, 2 Dec 2021 12:16:55 -0600 Subject: [PATCH 023/119] DEV: Raise errors for (black|white)list accesses (#15174) These have been deprecated for a while --- app/models/site_setting.rb | 4 ++-- lib/plugin/instance.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 33799aba84..0bbaf50860 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -238,11 +238,11 @@ class SiteSetting < ActiveRecord::Base ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair do |old_method, new_method| self.define_singleton_method(old_method) do - Discourse.deprecate("#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", drop_from: "2.6") + Discourse.deprecate("#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", drop_from: "2.6", raise_error: true) send(new_method) end self.define_singleton_method("#{old_method}=") do |args| - Discourse.deprecate("#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", drop_from: "2.6") + Discourse.deprecate("#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", drop_from: "2.6", raise_error: true) send("#{new_method}=", args) end end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index d24e49e374..71b92a42de 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -157,7 +157,7 @@ class Plugin::Instance end def whitelist_staff_user_custom_field(field) - Discourse.deprecate("whitelist_staff_user_custom_field is deprecated, use the allow_staff_user_custom_field.", drop_from: "2.6") + Discourse.deprecate("whitelist_staff_user_custom_field is deprecated, use the allow_staff_user_custom_field.", drop_from: "2.6", raise_error: true) allow_staff_user_custom_field(field) end @@ -166,7 +166,7 @@ class Plugin::Instance end def whitelist_public_user_custom_field(field) - Discourse.deprecate("whitelist_public_user_custom_field is deprecated, use the allow_public_user_custom_field.", drop_from: "2.6") + Discourse.deprecate("whitelist_public_user_custom_field is deprecated, use the allow_public_user_custom_field.", drop_from: "2.6", raise_error: true) allow_public_user_custom_field(field) end @@ -319,7 +319,7 @@ class Plugin::Instance end def topic_view_post_custom_fields_whitelister(&block) - Discourse.deprecate("topic_view_post_custom_fields_whitelister is deprecated, use the topic_view_post_custom_fields_allowlister.", drop_from: "2.6") + Discourse.deprecate("topic_view_post_custom_fields_whitelister is deprecated, use the topic_view_post_custom_fields_allowlister.", drop_from: "2.6", raise_error: true) topic_view_post_custom_fields_allowlister(&block) end From 7456a59022738c52c39eb6eb489f158b42f3a61e Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Thu, 2 Dec 2021 19:46:40 +0100 Subject: [PATCH 024/119] FEATURE: Add the ability to go back and forth between PM and New Topic (#15173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this, if you were composing a new topic and then switched the mode to "New Message", the dropdown would disappear. So if you changed your mind, you'd have to copy the text you typed, cancel, click "New Topic" again, and then paste the text. (and if you already had a title entered too, things would be more complicated…) --- .../tests/acceptance/composer-actions-test.js | 17 +++++++--- .../addon/components/composer-actions.js | 34 ++++++------------- 2 files changed, 23 insertions(+), 28 deletions(-) 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 65b353ef8a..5d1a5efc42 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-actions-test.js @@ -2,6 +2,7 @@ import { acceptance, count, exists, + query, queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; @@ -187,7 +188,7 @@ acceptance("Composer Actions", function (needs) { assert.deepEqual(privateMessageUsers.header().value(), "foo,foo_group"); }); - test("hide component if no content", async function (assert) { + test("allow switching back to New Topic", async function (assert) { await visit("/"); await click("button#create-topic"); @@ -195,12 +196,18 @@ acceptance("Composer Actions", function (needs) { await composerActions.expand(); await composerActions.selectRowByValue("reply_as_private_message"); - assert.ok(composerActions.el().hasClass("is-hidden")); - assert.strictEqual(composerActions.el().children().length, 0); + assert.strictEqual( + query(".action-title").innerText, + I18n.t("topic.private_message") + ); - await click("button#create-topic"); await composerActions.expand(); - assert.strictEqual(composerActions.rows().length, 2); + await composerActions.selectRowByValue("create_topic"); + + assert.strictEqual( + query(".action-title").innerText, + I18n.t("topic.create_long") + ); }); test("interactions", async function (assert) { diff --git a/app/assets/javascripts/select-kit/addon/components/composer-actions.js b/app/assets/javascripts/select-kit/addon/components/composer-actions.js index 705c0002c1..0e5434574e 100644 --- a/app/assets/javascripts/select-kit/addon/components/composer-actions.js +++ b/app/assets/javascripts/select-kit/addon/components/composer-actions.js @@ -208,11 +208,6 @@ export default DropdownSelectBoxComponent.extend({ }); } - let showCreateTopic = false; - if (this.action === CREATE_SHARED_DRAFT) { - showCreateTopic = true; - } - if (this.action === CREATE_TOPIC) { if (this.site.shared_drafts_category_id) { // Shared Drafts Choice @@ -223,24 +218,6 @@ export default DropdownSelectBoxComponent.extend({ id: "shared_draft", }); } - - // Edge case: If personal messages are disabled, it is possible to have - // no items which still renders a button that pops up nothing. In this - // case, add an option for what you're currently doing. - if (items.length === 0) { - showCreateTopic = true; - } - } - - if (showCreateTopic) { - items.push({ - name: I18n.t("composer.composer_actions.create_topic.label"), - description: I18n.t( - "composer.composer_actions.reply_as_new_topic.desc" - ), - icon: "share", - id: "create_topic", - }); } const showToggleTopicBump = @@ -256,6 +233,17 @@ export default DropdownSelectBoxComponent.extend({ }); } + if (items.length === 0) { + items.push({ + name: I18n.t("composer.composer_actions.create_topic.label"), + description: I18n.t( + "composer.composer_actions.reply_as_new_topic.desc" + ), + icon: "share", + id: "create_topic", + }); + } + return items; }, From 9ecf454074284536831c7c97283245abb3b4afc6 Mon Sep 17 00:00:00 2001 From: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com> Date: Thu, 2 Dec 2021 13:18:11 -0600 Subject: [PATCH 025/119] DEV: Invite page changes (#15175) --- .../discourse/app/controllers/user-invited-show.js | 13 ++++++++++--- .../discourse/app/templates/user-invited-show.hbs | 12 ++++++------ app/assets/stylesheets/common/base/user.scss | 2 +- app/assets/stylesheets/desktop/user.scss | 5 ++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js index 50c36460c1..866ddebd95 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js +++ b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js @@ -45,6 +45,13 @@ export default Controller.extend({ inviteExpired: equal("filter", "expired"), invitePending: equal("filter", "pending"), + @discourseComputed("model") + hasEmailInvites(model) { + return model.invites.some((invite) => { + return invite.email; + }); + }, + @discourseComputed("filter") showBulkActionButtons(filter) { return ( @@ -57,9 +64,9 @@ export default Controller.extend({ canInviteToForum: reads("currentUser.can_invite_to_forum"), canBulkInvite: reads("currentUser.admin"), - @discourseComputed("invitesCount.total") - showSearch(invitesCountTotal) { - return invitesCountTotal > 0; + @discourseComputed("invitesCount", "filter") + showSearch(invitesCount, filter) { + return invitesCount[filter] > 5; }, @action 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 a3b616b477..73eb1be666 100644 --- a/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/app/templates/user-invited-show.hbs @@ -17,9 +17,9 @@ {{#if showBulkActionButtons}} {{#if inviteExpired}} {{#if removedAll}} -
  • + {{i18n "user.invited.removed_all"}} -
  • + {{else}} {{d-button icon="times" action=(action "destroyAllExpired") label="user.invited.remove_all"}} {{/if}} @@ -27,10 +27,10 @@ {{#if invitePending}} {{#if reinvitedAll}} -
  • - {{i18n "user.invited.reinvited_all"}} -
  • - {{else}} + + {{d-button icon="check" disabled=true label="user.invited.reinvited_all"}} + + {{else if hasEmailInvites}} {{d-button class="btn-default" icon="sync" action=(action "reinviteAll") label="user.invited.reinvite_all"}} {{/if}} {{/if}} diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 9d87e21fbf..e6106f08a0 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -7,7 +7,7 @@ display: grid; grid-template-columns: 1fr 5fr; grid-template-rows: auto auto 1fr auto; - grid-row-gap: 20px; + grid-gap: 20px; .user-primary-navigation { grid-column-start: 1; grid-column-end: 3; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 09bdd5ed72..9bc4fd2d8d 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -29,7 +29,10 @@ li { border-bottom: none; - + &:hover, + &.active { + background: var(--primary-very-low); + } &.archive { padding-left: 1.4em; } From 7e005f2ea3c5afca4f4f27aa3a4481d20e3eef25 Mon Sep 17 00:00:00 2001 From: Mark VanLandingham Date: Thu, 2 Dec 2021 14:50:30 -0600 Subject: [PATCH 026/119] DEV: Don't error when emoji-picker is used outside composer (#15172) --- .../discourse/app/components/emoji-picker.js | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index 3d04b226bb..1d6867a06b 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -88,25 +88,24 @@ export default Component.extend({ return; } - if (!this.site.isMobileDevice) { - this._popper = createPopper( - document.querySelector(".d-editor-textarea-wrapper"), - emojiPicker, - { - placement: "auto", - modifiers: [ - { - name: "preventOverflow", + const textareaWrapper = document.querySelector( + ".d-editor-textarea-wrapper" + ); + if (!this.site.isMobileDevice && textareaWrapper) { + this._popper = createPopper(textareaWrapper, emojiPicker, { + placement: "auto", + modifiers: [ + { + name: "preventOverflow", + }, + { + name: "offset", + options: { + offset: [5, 5], }, - { - name: "offset", - options: { - offset: [5, 5], - }, - }, - ], - } - ); + }, + ], + }); } // this is a low-tech trick to prevent appending hundreds of emojis From 588dfdc7e21de86ff375720bf9ecf88978d2a2f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Dec 2021 22:29:32 +0100 Subject: [PATCH 027/119] Build(deps): Bump rubocop-ast from 1.13.0 to 1.14.0 (#15176) Bumps [rubocop-ast](https://github.com/rubocop/rubocop-ast) from 1.13.0 to 1.14.0. - [Release notes](https://github.com/rubocop/rubocop-ast/releases) - [Changelog](https://github.com/rubocop/rubocop-ast/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop-ast/compare/v1.13.0...v1.14.0) --- updated-dependencies: - dependency-name: rubocop-ast dependency-type: indirect update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6c5be3c315..e640206bfc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -398,7 +398,7 @@ GEM rubocop-ast (>= 1.12.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.13.0) + rubocop-ast (1.14.0) parser (>= 3.0.1.1) rubocop-discourse (2.5.0) rubocop (>= 1.1.0) From f9e2ab570b334f1d2c43e0fb61110a202c9ad616 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 3 Dec 2021 01:47:33 +0100 Subject: [PATCH 028/119] FIX: allows more precise placement strategy on mobile (#15171) * FIX: allows more precise placement strategy on mobile - default to absolute on mobile, fixed on desktop - allows to set a global `placementStrategy` or a specific to each view `mobilePlacementStrategy` `desktopPlacementStrategy` This is mainly used to allow a proper composer-actions positioning in mobile. Note this commit also fixes a mouseDown event which could propagate quote-button event and cause the composer to close full screen on mobile * mobile only --- .../components/composer-action-title.hbs | 3 ++ .../select-kit/addon/components/select-kit.js | 43 +++++++++++++------ .../select-kit/select-kit-header.js | 4 ++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/components/composer-action-title.hbs b/app/assets/javascripts/discourse/app/templates/components/composer-action-title.hbs index 2924226408..220f9dd497 100644 --- a/app/assets/javascripts/discourse/app/templates/components/composer-action-title.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/composer-action-title.hbs @@ -10,6 +10,9 @@ post=model.post whisper=model.whisper noBump=model.noBump + options=(hash + mobilePlacementStrategy="fixed" + ) }} diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit.js b/app/assets/javascripts/select-kit/addon/components/select-kit.js index db9a181310..937628b4de 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -289,6 +289,9 @@ export default Component.extend( focusAfterOnChange: true, triggerOnChangeOnTab: true, autofocus: false, + placementStrategy: null, + mobilePlacementStrategy: null, + desktopPlacementStrategy: null, }, autoFilterable: computed("content.[]", "selectKit.filter", function () { @@ -849,36 +852,28 @@ export default Component.extend( } this.selectKit.mainElement().open = true; - this.clearErrors(); - - const inModal = this.element.closest("#discourse-modal"); - this.selectKit.onOpen(event); if (!this.popper) { + const inModal = this.element.closest("#discourse-modal"); const anchor = document.querySelector( `#${this.selectKit.uniqueID}-header` ); const popper = document.querySelector( `#${this.selectKit.uniqueID}-body` ); - - const placementStrategy = - this.capabilities?.isIpadOS || this.site?.mobileView - ? "absolute" - : "fixed"; - const verticalOffset = 3; + const strategy = this._computePlacementStrategy(); this.popper = createPopper(anchor, popper, { eventsEnabled: false, - strategy: placementStrategy, + strategy, placement: this.selectKit.options.placement, modifiers: [ { name: "offset", options: { - offset: [0, verticalOffset], + offset: [0, 3], }, }, { @@ -888,7 +883,11 @@ export default Component.extend( fn({ state }) { if (!inModal) { let { x } = state.elements.reference.getBoundingClientRect(); - state.modifiersData.popperOffsets.x = -x + 10; + if (strategy === "fixed") { + state.modifiersData.popperOffsets.x = 0 + 10; + } else { + state.modifiersData.popperOffsets.x = -x + 10; + } } }, }, @@ -1036,6 +1035,24 @@ export default Component.extend( this._deprecateOptions(); }, + _computePlacementStrategy() { + let placementStrategy = this.selectKit.options.placementStrategy; + + if (placementStrategy) { + return placementStrategy; + } + + if (this.capabilities?.isIpadOS || this.site?.mobileView) { + placementStrategy = + this.selectKit.options.mobilePlacementStrategy || "absolute"; + } else { + placementStrategy = + this.selectKit.options.desktopPlacementStrategy || "fixed"; + } + + return placementStrategy; + }, + _deprecated(text) { const discourseSetup = document.getElementById("data-discourse-setup"); if ( diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js index 8b62948824..689da33de0 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js @@ -56,6 +56,10 @@ export default Component.extend(UtilsMixin, { } }, + mouseDown() { + return false; + }, + click(event) { event.preventDefault(); event.stopPropagation(); From 75dbc488d9e8e3609d8c9d0eb1b54755aac2d8ff Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 3 Dec 2021 12:05:05 +0000 Subject: [PATCH 029/119] DEV: Update official plugin list (#15180) - Remove Discord plugin (it has now been merged into core) - Rename discourse-plugin-office365-auth to discourse-microsoft-auth --- lib/plugin/metadata.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index 05d00fdba7..9f045e3261 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -52,9 +52,8 @@ class Plugin::Metadata "discourse-openid-connect", "discourse-patreon", "discourse-perspective-api", - "discourse-plugin-discord-auth", "discourse-plugin-linkedin-auth", - "discourse-plugin-office365-auth", + "discourse-microsoft-auth", "discourse-policy", "discourse-presence", "discourse-prometheus", From 643f82d8d61caef326d3d5f2d74df05b0f8cd3be Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Fri, 3 Dec 2021 08:03:58 -0700 Subject: [PATCH 030/119] DEV: Update email responses in api docs (#15178) Documenting the `/u/:username:/emails.json` endpoint. Also removing some email fields from user api responses because they aren't actually included in the response unless you are querying yourself. --- .../api/schemas/json/admin_user_response.json | 7 ----- .../schemas/json/user_emails_response.json | 26 +++++++++++++++++++ .../api/schemas/json/user_get_response.json | 12 --------- spec/requests/api/users_spec.rb | 26 ++++++++++++++++++- 4 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 spec/requests/api/schemas/json/user_emails_response.json diff --git a/spec/requests/api/schemas/json/admin_user_response.json b/spec/requests/api/schemas/json/admin_user_response.json index 4fdaba2bb0..db38d9f99d 100644 --- a/spec/requests/api/schemas/json/admin_user_response.json +++ b/spec/requests/api/schemas/json/admin_user_response.json @@ -13,13 +13,6 @@ "avatar_template": { "type": "string" }, - "email": { - "type": "string" - }, - "secondary_emails": { - "type": "array", - "items": [] - }, "active": { "type": "boolean" }, diff --git a/spec/requests/api/schemas/json/user_emails_response.json b/spec/requests/api/schemas/json/user_emails_response.json new file mode 100644 index 0000000000..103ed6d038 --- /dev/null +++ b/spec/requests/api/schemas/json/user_emails_response.json @@ -0,0 +1,26 @@ +{ + "additionalProperties": false, + "properties": { + "email": { + "type": "string" + }, + "secondary_emails": { + "type": "array", + "items": [] + }, + "unconfirmed_emails": { + "type": "array", + "items": [] + }, + "associated_accounts": { + "type": "array", + "items": [] + } + }, + "required": [ + "email", + "secondary_emails", + "unconfirmed_emails", + "associated_accounts" + ] +} diff --git a/spec/requests/api/schemas/json/user_get_response.json b/spec/requests/api/schemas/json/user_get_response.json index 45fbbf0eae..8673da017f 100644 --- a/spec/requests/api/schemas/json/user_get_response.json +++ b/spec/requests/api/schemas/json/user_get_response.json @@ -74,18 +74,6 @@ "badge_count": { "type": "integer" }, - "email": { - "type": "string" - }, - "secondary_emails": { - "type": "array" - }, - "unconfirmed_emails": { - "type": "array" - }, - "associated_accounts": { - "type": "array" - }, "second_factor_backup_enabled": { "type": "boolean" }, diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e651df4c1f..a50be753a5 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -63,7 +63,8 @@ describe 'users' do expected_response_schema = load_spec_schema('user_get_response') schema expected_response_schema - let(:username) { 'system' } + let(:username) { Fabricate(:user).username } + it_behaves_like "a JSON endpoint", 200 do let(:expected_response_schema) { expected_response_schema } let(:expected_request_schema) { expected_request_schema } @@ -583,4 +584,27 @@ describe 'users' do end end + path '/u/{username}/emails.json' do + get 'Get email addresses belonging to a user' do + tags 'Users' + operationId 'getUserEmails' + consumes 'application/json' + expected_request_schema = nil + parameter name: :username, in: :path, type: :string, required: true + + produces 'application/json' + response '200', 'success response' do + expected_response_schema = load_spec_schema('user_emails_response') + schema expected_response_schema + + let(:username) { Fabricate(:user).username } + + it_behaves_like "a JSON endpoint", 200 do + let(:expected_response_schema) { expected_response_schema } + let(:expected_request_schema) { expected_request_schema } + end + end + end + end + end From b01ded9c896ee248b1140b08a56e0d1f6de43933 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 3 Dec 2021 10:22:05 -0500 Subject: [PATCH 031/119] UX: Improve tag info style (#15177) --- .../app/templates/components/d-navigation.hbs | 2 +- .../app/templates/components/tag-info.hbs | 61 +++++++-------- .../discourse/tests/acceptance/tags-test.js | 4 +- .../stylesheets/common/base/tagging.scss | 76 +++++++++++++------ config/locales/client.en.yml | 5 +- 5 files changed, 91 insertions(+), 57 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs b/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs index acec052510..bd309747db 100644 --- a/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs @@ -31,7 +31,7 @@ {{#if tag}} {{#if showToggleInfo}} - {{d-button icon="wrench" class="btn-default" ariaLabel="tagging.info" action=toggleInfo id="show-tag-info"}} + {{d-button icon=(if currentUser.staff "wrench" "info-circle") class="btn-default" ariaLabel="tagging.info" action=toggleInfo id="show-tag-info"}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs b/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs index 92aa9d2e9c..ca465bf1a4 100644 --- a/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs @@ -17,24 +17,11 @@
    {{discourse-tag tagInfo.name tagName="div" size="large"}} {{#if canAdminTag}} - {{d-icon "pencil-alt"}} + {{d-button action=(action "edit") class="btn-flat edit-tag" title=(i18n "tagging.edit_tag") icon="pencil-alt" }} {{/if}}
    - {{#if canAdminTag}} -
    - {{tagInfo.description}} -
    - {{/if}} - {{/if}} - - {{#if canAdminTag}} - {{plugin-outlet name="tag-custom-settings" args=(hash tag=tagInfo) connectorTagName="" tagName="section"}} - -
    - {{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}} - {{#if deleteAction}} - {{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}} - {{/if}} +
    + {{tagInfo.description}}
    {{/if}}
    @@ -53,7 +40,10 @@ {{#if tagInfo.category_restricted}} {{i18n "tagging.category_restricted"}} {{else}} - {{html-safe (i18n "tagging.default_info" basePath=(base-path))}} + {{html-safe (i18n "tagging.default_info")}} + {{#if canAdminTag}} + {{html-safe (i18n "tagging.staff_info" basePath=(base-path))}} + {{/if}} {{/if}} {{/if}} @@ -81,20 +71,31 @@ {{#if editSynonymsMode}}
    - {{tag-chooser - id="add-synonyms" - tags=newSynonyms - blockedTags=(array tagInfo.name) - everyTag=true - excludeSynonyms=true - excludeHasSynonyms=true - unlimitedTagCount=true}} +
    + {{tag-chooser + id="add-synonyms" + tags=newSynonyms + blockedTags=(array tagInfo.name) + everyTag=true + excludeSynonyms=true + excludeHasSynonyms=true + unlimitedTagCount=true}} + {{d-button + class="ok" + action=(action "addSynonyms") + disabled=addSynonymsDisabled + icon="check"}} +
    - {{d-button - class="btn-default" - action=(action "addSynonyms") - disabled=addSynonymsDisabled - label="tagging.add_synonyms"}} + {{/if}} + {{#if canAdminTag}} + {{plugin-outlet name="tag-custom-settings" args=(hash tag=tagInfo) connectorTagName="" tagName="section"}} +
    + {{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}} + {{#if deleteAction}} + {{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}} + {{/if}} +
    {{/if}} {{/if}} {{#if loading}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js index 42dfe9e3d0..357c26c759 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js @@ -441,7 +441,7 @@ acceptance("Tag info", function (needs) { await click("#show-tag-info"); assert.ok(exists(".tag-info .tag-name"), "show tag"); - await click("#edit-tag"); + await click(".edit-tag"); assert.strictEqual( query("#edit-name").value, "happy-monkey", @@ -470,7 +470,7 @@ acceptance("Tag info", function (needs) { assert.strictEqual(count("#show-tag-info"), 1); await click("#show-tag-info"); - assert.ok(exists("#edit-tag"), "can rename tag"); + assert.ok(exists(".edit-tag"), "can rename tag"); assert.ok(exists("#edit-synonyms"), "can edit synonyms"); assert.ok(exists("#delete-tag"), "can delete tag"); diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 9d8a9223fb..d40df6de86 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -340,51 +340,83 @@ section.tag-info { .edit-tag-wrapper { display: flex; - - input { + flex-wrap: wrap; + margin-bottom: 1em; + #edit-name { + flex: 1 1 auto; margin-right: 0.5em; } + #edit-description { + flex: 10 1 auto; + } + .edit-controls { + width: 100%; + } } - .tag-name-wrapper, + + .tag-name-wrapper { + display: flex; + font-size: var(--font-up-4); + align-items: baseline; + button { + font-size: var(--font-down-1); + .d-icon { + color: var(--primary-high); + } + .discourse-no-touch & { + &:hover { + background: transparent; + } + } + } + } + .tag-description-wrapper { display: flex; - } - .tag-name-wrapper a { - color: var(--primary-high); - margin-left: 0.5em; + font-size: var(--font-up-1); } - .tag-name-wrapper a { - font-size: var(--font-up-3); - } - - .tag-name .discourse-tag { - display: block; + .tag-box { + display: flex; + align-items: center; + margin-bottom: 0.25em; + a { + margin-left: 0.5em; + } } .tag-description-wrapper { margin-bottom: 1em; } - .synonyms-list, - .add-synonyms, .tag-associations { - margin-top: 1em; - @include clearfix; + margin-bottom: 1em; } - .synonyms-list { - margin: 2em 0 0; + .tag-actions { + margin-top: 2em; + } + + .add-synonyms { + margin-top: 1em; + div { + display: flex; + } + .ok { + margin-left: 0.5em; + display: none; + } + .has-selection + .ok { + display: flex; + } } .tag-list { - margin: 1em 0 0 0; + margin: 0.5em 0 1em; padding: 0; border: none; - @include clearfix; a { color: var(--primary-medium); - margin-left: 0.25em; } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4895e58255..3f6b5e59d4 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3792,7 +3792,8 @@ en: one: "select at least %{count} tag..." other: "select at least %{count} tags..." info: "Info" - default_info: "This tag isn't restricted to any categories, and has no synonyms. To add restrictions, put this tag in a tag group." + default_info: "This tag isn't restricted to any categories, and has no synonyms." + staff_info: "To add restrictions, put this tag in a tag group." category_restricted: "This tag is restricted to categories you don't have permission to access." synonyms: "Synonyms" synonyms_description: "When the following tags are used, they will be replaced with %{base_tag_name}." @@ -3803,7 +3804,7 @@ en: category_restrictions: one: "It can only be used in this category:" other: "It can only be used in these categories:" - edit_synonyms: "Manage Synonyms" + edit_synonyms: "Edit Synonyms" add_synonyms_label: "Add synonyms:" add_synonyms: "Add" add_synonyms_explanation: From 93860fd29b9e81dd7d8439abf3a23e180b956570 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 3 Dec 2021 17:42:14 +0000 Subject: [PATCH 032/119] DEV: Update discourse-plugin-linkedin-auth to discourse-linkedin-auth (#15181) --- lib/plugin/metadata.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index 9f045e3261..13c4267e61 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -52,7 +52,7 @@ class Plugin::Metadata "discourse-openid-connect", "discourse-patreon", "discourse-perspective-api", - "discourse-plugin-linkedin-auth", + "discourse-linkedin-auth", "discourse-microsoft-auth", "discourse-policy", "discourse-presence", From 657c137384b8c674e8ce87510cb2d60e4c1e6743 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 3 Dec 2021 13:48:26 -0500 Subject: [PATCH 033/119] UX: Prevent overflow on mobile timeline dates (#15182) --- app/assets/stylesheets/common/topic-timeline.scss | 2 -- app/assets/stylesheets/mobile/topic.scss | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index 159e82097c..ca1380b3cf 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -72,7 +72,6 @@ touch-action: none; .timeline-date-wrapper { - float: right; text-align: right; } .post-excerpt { @@ -142,7 +141,6 @@ border-right-style: solid; border-right-width: 1px; max-width: 120px; - margin-top: 2em; .timeline-scroller { position: relative; diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index ec5b6202dd..9ad066ee28 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -115,8 +115,10 @@ sub sub { .topic-timeline { .start-date, .now-date { - font-size: $font-up-1; - padding: 5px; + padding: 0.5em 0; + } + .timeline-scrollarea-wrapper .timeline-date-wrapper { + @include ellipsis; } .topic-category { margin-bottom: 0.5rem; From 63112f89a3d98a5fa058c55462567b2824920fa7 Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Fri, 3 Dec 2021 14:54:07 -0600 Subject: [PATCH 034/119] PERF: Shave off some test-suite time (#15183) --- spec/components/post_action_creator_spec.rb | 4 +- spec/models/post_action_spec.rb | 10 +---- spec/models/topic_spec.rb | 6 +-- spec/requests/groups_controller_spec.rb | 42 ++++++++------------- spec/requests/users_controller_spec.rb | 2 +- spec/services/user_destroyer_spec.rb | 2 - spec/services/user_merger_spec.rb | 34 +++++++---------- 7 files changed, 33 insertions(+), 67 deletions(-) diff --git a/spec/components/post_action_creator_spec.rb b/spec/components/post_action_creator_spec.rb index dd4ef73f94..488b47effa 100644 --- a/spec/components/post_action_creator_spec.rb +++ b/spec/components/post_action_creator_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' describe PostActionCreator do + fab!(:admin) { Fabricate(:admin) } fab!(:user) { Fabricate(:user) } fab!(:post) { Fabricate(:post) } let(:like_type_id) { PostActionType.types[:like] } @@ -147,8 +148,6 @@ describe PostActionCreator do end describe "When the post was already reviewed by staff" do - fab!(:admin) { Fabricate(:admin) } - before { reviewable.perform(admin, :ignore) } it "fails because the post was recently reviewed" do @@ -221,7 +220,6 @@ describe PostActionCreator do end context "queue_for_review" do - fab!(:admin) { Fabricate(:admin) } it 'fails if the user is not a staff member' do creator = PostActionCreator.new( diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 82f7ea92f5..f4a258dd6a 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -492,7 +492,7 @@ describe PostAction do it "shouldn't change given_likes unless likes are given or removed" do freeze_time(Time.zone.now) - PostActionCreator.like(codinghorror, Fabricate(:post)) + PostActionCreator.like(codinghorror, post) expect(value_for(codinghorror.id, Date.today)).to eq(1) PostActionType.types.each do |type_name, type_id| @@ -513,20 +513,17 @@ describe PostAction do describe 'flagging' do it 'does not allow you to flag stuff twice, even if the reason is different' do - post = Fabricate(:post) expect(PostActionCreator.spam(eviltrout, post)).to be_success expect(PostActionCreator.off_topic(eviltrout, post)).to be_failed end it 'allows you to flag stuff again if your previous flag was removed' do - post = Fabricate(:post) PostActionCreator.spam(eviltrout, post) PostActionDestroyer.destroy(eviltrout, post, :spam) expect(PostActionCreator.spam(eviltrout, post)).to be_success end it 'should update counts when you clear flags' do - post = Fabricate(:post) reviewable = PostActionCreator.spam(eviltrout, post).reviewable expect(post.reload.spam_count).to eq(1) @@ -874,7 +871,6 @@ describe PostAction do describe ".lookup_for" do it "returns the correct map" do user = Fabricate(:user) - post = Fabricate(:post) post_action = PostActionCreator.create(user, post, :bookmark).post_action map = PostAction.lookup_for(user, [post.topic], post_action.post_action_type_id) @@ -898,7 +894,6 @@ describe PostAction do it "should create a notification in the related topic" do Jobs.run_immediately! - post = Fabricate(:post) user = Fabricate(:user) stub_image_size result = PostActionCreator.create(user, post, :spam, message: "WAT") @@ -915,7 +910,6 @@ describe PostAction do skip "should not add a moderator post when post is flagged via private message" do Jobs.run_immediately! - post = Fabricate(:post) user = Fabricate(:user) result = PostActionCreator.create(user, post, :notify_user, message: "WAT") action = result.post_action @@ -986,8 +980,6 @@ describe PostAction do end describe "triggers Discourse events" do - fab!(:post) { Fabricate(:post) } - it 'triggers a flag_created event' do event = DiscourseEvent.track(:flag_created) { PostActionCreator.spam(eviltrout, post) } expect(event).to be_present diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index de3086ed46..7513128391 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -6,6 +6,7 @@ require 'rails_helper' describe Topic do let(:now) { Time.zone.local(2013, 11, 20, 8, 0) } fab!(:user) { Fabricate(:user) } + fab!(:admin) { Fabricate(:admin) } fab!(:another_user) { Fabricate(:user) } fab!(:trust_level_2) { Fabricate(:user, trust_level: SiteSetting.min_trust_level_to_allow_invite) } @@ -249,8 +250,6 @@ describe Topic do end context 'admin topic title' do - let(:admin) { Fabricate(:admin) } - it 'allows really short titles' do pm = Fabricate.build(:private_message_topic, user: admin, title: 'a') expect(pm).to be_valid @@ -747,7 +746,6 @@ describe Topic do context "when invited_user has enabled allow_list" do fab!(:user2) { Fabricate(:user) } - fab!(:admin) { Fabricate(:admin) } fab!(:pm) { Fabricate(:private_message_topic, user: user, topic_allowed_users: [ Fabricate.build(:topic_allowed_user, user: user), Fabricate.build(:topic_allowed_user, user: user2) @@ -1864,7 +1862,6 @@ describe Topic do Fabricate(:topic_timer, execute_at: 5.hours.from_now).topic end - fab!(:admin) { Fabricate(:admin) } fab!(:trust_level_4) { Fabricate(:trust_level_4) } it 'can take a number of hours as an integer' do @@ -2955,7 +2952,6 @@ describe Topic do describe "#cannot_permanently_delete_reason" do fab!(:post) { Fabricate(:post) } let!(:topic) { post.topic } - fab!(:admin) { Fabricate(:admin) } before do freeze_time diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index ff7b00a4a7..7dc0d1cb59 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' describe GroupsController do fab!(:user) { Fabricate(:user) } - let(:other_user) { Fabricate(:user) } + fab!(:other_user) { Fabricate(:user) } let(:group) { Fabricate(:group, users: [user]) } let(:moderator_group_id) { Group::AUTO_GROUPS[:moderators] } fab!(:admin) { Fabricate(:admin) } @@ -92,7 +92,7 @@ describe GroupsController do sign_in(user) end - let!(:other_group) { Fabricate(:group, name: "other_group", users: [user, other_user]) } + fab!(:other_group) { Fabricate(:group, name: "other_group", users: [user, other_user]) } context "with default (descending) order" do it "sorts by name" do @@ -213,7 +213,7 @@ describe GroupsController do expect(group_names).to contain_exactly("0_0") # logged in user - sign_in(Fabricate(:user)) + sign_in(user) get "/groups.json", params: { username: u.username } expect(response.status).to eq(200) @@ -256,8 +256,6 @@ describe GroupsController do end context 'viewing as an admin' do - fab!(:admin) { Fabricate(:admin) } - before do sign_in(admin) group.add(admin) @@ -356,7 +354,7 @@ describe GroupsController do describe '#show' do it "ensures the group can be seen" do - sign_in(Fabricate(:user)) + sign_in(user) group.update!(visibility_level: Group.visibility_levels[:owners]) get "/groups/#{group.name}.json" @@ -397,7 +395,7 @@ describe GroupsController do context 'as an admin' do it "returns the right response" do - sign_in(Fabricate(:admin)) + sign_in(admin) get "/groups/#{group.name}.json" expect(response.status).to eq(200) @@ -448,7 +446,7 @@ describe GroupsController do describe "#posts" do it "ensures the group can be seen" do - sign_in(Fabricate(:user)) + sign_in(user) group.update!(visibility_level: Group.visibility_levels[:owners]) get "/groups/#{group.name}/posts.json" @@ -457,7 +455,7 @@ describe GroupsController do end it "ensures the group members can be seen" do - sign_in(Fabricate(:user)) + sign_in(user) group.update!(members_visibility_level: Group.visibility_levels[:owners]) get "/groups/#{group.name}/posts.json" @@ -487,7 +485,7 @@ describe GroupsController do describe "#members" do it "returns correct error code with invalid params" do - sign_in(Fabricate(:user)) + sign_in(user) get "/groups/#{group.name}/members.json?limit=-1" expect(response.status).to eq(400) @@ -500,7 +498,7 @@ describe GroupsController do end it "ensures the group can be seen" do - sign_in(Fabricate(:user)) + sign_in(user) group.update!(visibility_level: Group.visibility_levels[:owners]) get "/groups/#{group.name}/members.json" @@ -1188,7 +1186,7 @@ describe GroupsController do end it "can show group requests" do - sign_in(Fabricate(:admin)) + sign_in(admin) user4 = Fabricate(:user) request4 = Fabricate(:group_request, user: user4, group: group) @@ -1217,7 +1215,7 @@ describe GroupsController do describe 'as an admin' do before do - sign_in(Fabricate(:admin)) + sign_in(admin) end it "should allow members to be filterable by username" do @@ -1296,11 +1294,10 @@ describe GroupsController do end context 'when user is an admin' do - fab!(:user) { Fabricate(:admin) } - let(:group) { Fabricate(:group, users: [user], automatic: true) } + fab!(:group) { Fabricate(:group, users: [admin], automatic: true) } before do - sign_in(user) + sign_in(admin) end it "cannot add members to automatic groups" do @@ -1314,8 +1311,6 @@ describe GroupsController do end describe "membership edits" do - fab!(:admin) { Fabricate(:admin) } - context '#add_members' do before do sign_in(admin) @@ -1370,7 +1365,7 @@ describe GroupsController do group.add_owner(user) sign_in(user) - put "/groups/#{group.id}/members.json", params: { usernames: Fabricate(:user).username } + put "/groups/#{group.id}/members.json", params: { usernames: other_user.username } expect(response.status).to eq(200) end @@ -1564,8 +1559,6 @@ describe GroupsController do end context 'public group' do - fab!(:other_user) { Fabricate(:user) } - before do group.update!( public_admission: true, @@ -1713,7 +1706,6 @@ describe GroupsController do end context 'public group' do - fab!(:other_user) { Fabricate(:user) } let(:group) { Fabricate(:public_group, users: [other_user]) } context "admin" do @@ -1923,8 +1915,6 @@ describe GroupsController do end context 'when user is an admin' do - fab!(:admin) { Fabricate(:admin) } - before do sign_in(admin) end @@ -2106,7 +2096,7 @@ describe GroupsController do context 'as an admin' do it "returns the right response" do - sign_in(Fabricate(:admin)) + sign_in(admin) get '/groups/search.json?ignore_automatic=true' @@ -2141,7 +2131,7 @@ describe GroupsController do end describe 'for an admin user' do - before { sign_in(Fabricate(:admin)) } + before { sign_in(admin) } it 'should return 200' do get '/groups/custom/new' diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 637cbe399d..096629df97 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -202,7 +202,7 @@ describe UsersController do end context 'valid token' do - let!(:user) { Fabricate(:user) } + fab!(:user) { Fabricate(:user) } let!(:user_auth_token) { UserAuthToken.generate!(user_id: user.id) } let!(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:password_reset]) } diff --git a/spec/services/user_destroyer_spec.rb b/spec/services/user_destroyer_spec.rb index 3bcafc505c..a376177fa5 100644 --- a/spec/services/user_destroyer_spec.rb +++ b/spec/services/user_destroyer_spec.rb @@ -371,7 +371,6 @@ describe UserDestroyer do describe "Destroying a user with security key" do let!(:security_key) { Fabricate(:user_security_key_with_random_credential, user: user) } - fab!(:admin) { Fabricate(:admin) } it "removes the security key" do UserDestroyer.new(admin).destroy(user) @@ -381,7 +380,6 @@ describe UserDestroyer do describe "Destroying a user with a bookmark" do let!(:bookmark) { Fabricate(:bookmark, user: user) } - fab!(:admin) { Fabricate(:admin) } it "removes the bookmark" do UserDestroyer.new(admin).destroy(user) diff --git a/spec/services/user_merger_spec.rb b/spec/services/user_merger_spec.rb index 06d3a51931..3ffb7ef847 100644 --- a/spec/services/user_merger_spec.rb +++ b/spec/services/user_merger_spec.rb @@ -7,6 +7,13 @@ describe UserMerger do fab!(:source_user) { Fabricate(:user, username: 'alice1', email: 'alice@work.com') } fab!(:walter) { Fabricate(:walter_white) } + fab!(:p1) { Fabricate(:post) } + fab!(:p2) { Fabricate(:post) } + fab!(:p3) { Fabricate(:post) } + fab!(:p4) { Fabricate(:post) } + fab!(:p5) { Fabricate(:post) } + fab!(:p6) { Fabricate(:post) } + def merge_users!(source = nil, target = nil) source ||= source_user target ||= target_user @@ -154,13 +161,6 @@ describe UserMerger do end it "merges likes" do - p1 = Fabricate(:post) - p2 = Fabricate(:post) - p3 = Fabricate(:post) - p4 = Fabricate(:post) - p5 = Fabricate(:post) - p6 = Fabricate(:post) - now = Time.zone.now freeze_time(now - 1.day) @@ -334,9 +334,6 @@ describe UserMerger do context "post actions" do it "merges post actions" do - p1 = Fabricate(:post) - p2 = Fabricate(:post) - p3 = Fabricate(:post) type_ids = PostActionType.public_type_ids + [PostActionType.flag_types.values.first] type_ids.each do |type| @@ -357,11 +354,6 @@ describe UserMerger do end it "updates post actions" do - p1 = Fabricate(:post) - p2 = Fabricate(:post) - p3 = Fabricate(:post) - p4 = Fabricate(:post) - action1 = PostActionCreator.create(source_user, p1, :off_topic).post_action action1.update_attribute(:deleted_by_id, source_user.id) @@ -384,7 +376,7 @@ describe UserMerger do end it "updates post revisions" do - post = Fabricate(:post) + post = p1 post_revision = Fabricate(:post_revision, post: post, user: source_user) merge_users! @@ -410,9 +402,9 @@ describe UserMerger do end it "merges post timings" do - post1 = Fabricate(:post) - post2 = Fabricate(:post) - post3 = Fabricate(:post) + post1 = p1 + post2 = p2 + post3 = p3 create_post_timing(post1, source_user, 12345) create_post_timing(post2, source_user, 9876) @@ -678,8 +670,8 @@ describe UserMerger do # action_type and user_id are not nullable # target_topic_id and acting_user_id are nullable, but always have a value - fab!(:post1) { Fabricate(:post) } - fab!(:post2) { Fabricate(:post) } + fab!(:post1) { p1 } + fab!(:post2) { p2 } def log_like_action(acting_user, user, post) UserAction.log_action!(action_type: UserAction::LIKE, From 6d2eae27a64b92f9013222a32b00d2c1525ee471 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 3 Dec 2021 17:02:22 -0500 Subject: [PATCH 035/119] UX: reduce composer jumpiness on android (#15184) --- app/assets/stylesheets/mobile/compose.scss | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index afbf39cc09..e69acc6218 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -23,11 +23,14 @@ } .keyboard-visible &.open { - height: calc(var(--composer-vh, 1vh) * 100); + height: 100%; // Android: Reduces composer jumpiness when the keyboard toggles } - .keyboard-visible body.ios-safari-composer-hacks &.open .reply-area { - padding-bottom: 0px; + .keyboard-visible body.ios-safari-composer-hacks &.open { + height: calc(var(--composer-vh, 1vh) * 100); + .reply-area { + padding-bottom: 0px; + } } .reply-to { From 959923d3cf0d058630ef4c84070eddbf32a1161b Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Fri, 3 Dec 2021 16:05:27 -0600 Subject: [PATCH 036/119] FIX: Match for indeterminate depth in URL during upload tests (#15186) Since the uploads id sequence counter isn't reset before test runs, the URL might not be /1X/ --- spec/support/uploads_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/uploads_helpers.rb b/spec/support/uploads_helpers.rb index 35c57fc940..b0cfe5452c 100644 --- a/spec/support/uploads_helpers.rb +++ b/spec/support/uploads_helpers.rb @@ -19,7 +19,7 @@ module UploadsHelpers end def stub_upload(upload) - url = "https://#{SiteSetting.s3_upload_bucket}.s3.#{SiteSetting.s3_region}.amazonaws.com/original/1X/#{upload.sha1}.#{upload.extension}?acl" + url = %r{https://#{SiteSetting.s3_upload_bucket}.s3.#{SiteSetting.s3_region}.amazonaws.com/original/\d+X.*#{upload.sha1}.#{upload.extension}\?acl} stub_request(:put, url) end From 03f3d793886117a0040066d77b482e054a292a88 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 3 Dec 2021 21:56:37 -0500 Subject: [PATCH 037/119] UX: remove style that breaks composer on pm page (#15189) --- app/assets/stylesheets/desktop/user.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 9bc4fd2d8d..e836483766 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -275,11 +275,6 @@ table.user-invite-list { .show-mores { position: absolute; } - - #reply-control .mini-tag-chooser { - width: 100%; - margin: 0; - } } .user-messages { From d3912075b64c98cc55ca8bc383265b8cecae9b5e Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Sun, 5 Dec 2021 00:15:51 +0100 Subject: [PATCH 038/119] FIX: PWA badges were not updating (#15191) That regressed in #7714, over two years ago. :P --- .../discourse/app/initializers/badging.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/discourse/app/initializers/badging.js b/app/assets/javascripts/discourse/app/initializers/badging.js index 73c90b0aa1..a347e30f2a 100644 --- a/app/assets/javascripts/discourse/app/initializers/badging.js +++ b/app/assets/javascripts/discourse/app/initializers/badging.js @@ -13,15 +13,12 @@ export default { return; } // must be logged in - this.notifications = - user.unread_notifications + user.unread_high_priority_notifications; + const appEvents = container.lookup("service:app-events"); + appEvents.on("notifications:changed", () => { + const notifications = + user.unread_notifications + user.unread_high_priority_notifications; - container - .lookup("service:app-events") - .on("notifications:changed", this, "_updateBadge"); - }, - - _updateBadge() { - navigator.setAppBadge(this.notifications); + navigator.setAppBadge(notifications); + }); }, }; From 972d7cb1d68571945f8506beb8128b87c7f7f9d0 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 4 Dec 2021 23:33:07 +0000 Subject: [PATCH 039/119] DEV: Fix mini-profiler location for custom (or missing) d-headers (#15192) --- app/assets/stylesheets/common/base/discourse.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 50158919df..a25132718e 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -372,7 +372,7 @@ table { } .profiler-results.profiler-left { - top: 60px !important; + top: var(--header-offset) !important; } .flex-center-align { From 11d1c520ff96ef01f44f223d0fe8a6167d63c475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Dec 2021 22:24:35 +0100 Subject: [PATCH 040/119] Build(deps): Bump regexp_parser from 2.1.1 to 2.2.0 (#15194) Bumps [regexp_parser](https://github.com/ammar/regexp_parser) from 2.1.1 to 2.2.0. - [Release notes](https://github.com/ammar/regexp_parser/releases) - [Changelog](https://github.com/ammar/regexp_parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/ammar/regexp_parser/compare/v2.1.1...v2.2.0) --- updated-dependencies: - dependency-name: regexp_parser dependency-type: indirect update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e640206bfc..43efd41d84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -348,7 +348,7 @@ GEM redis (4.5.1) redis-namespace (1.8.1) redis (>= 3.0.4) - regexp_parser (2.1.1) + regexp_parser (2.2.0) request_store (1.5.0) rack (>= 1.4) rexml (3.2.5) From 3b13f1146b2a406238c50d6b45bc9aa721094f46 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Mon, 6 Dec 2021 10:34:39 +1000 Subject: [PATCH 041/119] FIX: Add random suffix to outbound Message-ID for email (#15179) Currently the Message-IDs we send out for outbound email are not unique; for a post they look like: topic/TOPIC_ID/POST_ID@HOST And for a topic they look like: topic/TOPIC_ID@HOST This commit changes the outbound Message-IDs to also have a random suffix before the host, so the new format is like this: topic/TOPIC_ID/POST_ID.RANDOM_SUFFIX@HOST Or: topic/TOPIC_ID.RANDOM_SUFFIX@HOST This should help with email deliverability. This change is backwards-compatible, the old Message-ID format will still be recognized in the mail receiver flow, so people will still be able to reply using Message-IDs, In-Reply-To, and References headers that have already been sent. This commit also refactors Message-ID related logic to a central location, and adds judicious amounts of tests and documentation. --- app/controllers/webhooks_controller.rb | 4 +- lib/email.rb | 13 --- lib/email/message_id_service.rb | 103 ++++++++++++++++++ lib/email/receiver.rb | 36 +------ lib/email/sender.rb | 16 +-- lib/imap/providers/generic.rb | 6 +- lib/imap/sync.rb | 2 +- spec/components/email/email_spec.rb | 26 ----- spec/components/email/receiver_spec.rb | 49 ++++++++- spec/components/email/sender_spec.rb | 24 +++-- spec/jobs/regular/group_smtp_email_spec.rb | 12 ++- spec/lib/message_id_service_spec.rb | 117 +++++++++++++++++++++ 12 files changed, 304 insertions(+), 104 deletions(-) create mode 100644 lib/email/message_id_service.rb create mode 100644 spec/lib/message_id_service_spec.rb diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 93995df098..befa6ea22e 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -13,7 +13,7 @@ class WebhooksController < ActionController::Base def sendgrid events = params["_json"] || [params] events.each do |event| - message_id = Email.message_id_clean((event["smtp-id"] || "")) + message_id = Email::MessageIdService.message_id_clean((event["smtp-id"] || "")) to_address = event["email"] if event["event"] == "bounce" if event["status"]["4."] @@ -150,7 +150,7 @@ class WebhooksController < ActionController::Base return mailgun_failure unless valid_mailgun_signature?(params["token"], params["timestamp"], params["signature"]) event = params["event"] - message_id = Email.message_id_clean(params["Message-Id"]) + message_id = Email::MessageIdService.message_id_clean(params["Message-Id"]) to_address = params["recipient"] # only handle soft bounces, because hard bounces are also handled diff --git a/lib/email.rb b/lib/email.rb index f1b8329a7b..91c48dda43 100644 --- a/lib/email.rb +++ b/lib/email.rb @@ -52,21 +52,8 @@ module Email SiteSetting.email_site_title.presence || SiteSetting.title end - # https://tools.ietf.org/html/rfc850#section-2.1.7 - def self.message_id_rfc_format(message_id) - message_id.present? && !is_message_id_rfc?(message_id) ? "<#{message_id}>" : message_id - end - - def self.message_id_clean(message_id) - message_id.present? && is_message_id_rfc?(message_id) ? message_id.gsub(/^<|>$/, "") : message_id - end - private - def self.is_message_id_rfc?(message_id) - message_id.start_with?('<') && message_id.include?('@') && message_id.end_with?('>') - end - def self.obfuscate_part(part) if part.size < 3 "*" * part.size diff --git a/lib/email/message_id_service.rb b/lib/email/message_id_service.rb new file mode 100644 index 0000000000..744d51990f --- /dev/null +++ b/lib/email/message_id_service.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Email + ## + # Email Message-IDs are used in both our outbound and inbound email + # flow. For the outbound flow via Email::Sender, we assign a unique + # Message-ID for any emails sent out from the application. + # If we are sending an email related to a topic, such as through the + # PostAlerter class, then the Message-ID will contain references to + # the topic ID, and if it is for a specific post, the post ID, + # along with a random suffix to make the Message-ID truly unique. + # The host must also be included on the Message-IDs. + # + # For the inbound email flow via Email::Receiver, we use Message-IDs + # to discern which topic or post the inbound email reply should be + # in response to. In this case, the Message-ID is extracted from the + # References and/or In-Reply-To headers, and compared with either + # the IncomingEmail table, the Post table, or the IncomingEmail to + # determine where to send the reply. + # + # See https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.4 for + # more specific information around Message-IDs in email. + # + # See https://tools.ietf.org/html/rfc850#section-2.1.7 for the + # Message-ID format specification. + class MessageIdService + class << self + def generate_default + "<#{SecureRandom.uuid}@#{host}>" + end + + def generate_for_post(post, use_incoming_email_if_present: false) + if use_incoming_email_if_present && post.incoming_email&.message_id.present? + return "<#{post.incoming_email.message_id}>" + end + + "" + end + + def generate_for_topic(topic, use_incoming_email_if_present: false) + first_post = topic.ordered_posts.first + + if use_incoming_email_if_present && first_post.incoming_email&.message_id.present? + return "<#{first_post.incoming_email.message_id}>" + end + + "" + end + + def find_post_from_message_ids(message_ids) + message_ids = message_ids.map { |message_id| message_id_clean(message_id) } + post_ids = message_ids.map { |message_id| message_id[message_id_post_id_regexp, 1] }.compact.map(&:to_i) + post_ids << Post.where( + topic_id: message_ids.map { |message_id| message_id[message_id_topic_id_regexp, 1] }.compact, + post_number: 1 + ).pluck(:id) + post_ids << EmailLog.where(message_id: message_ids).pluck(:post_id) + post_ids << IncomingEmail.where(message_id: message_ids).pluck(:post_id) + + post_ids.flatten! + post_ids.compact! + post_ids.uniq! + + return if post_ids.empty? + + Post.where(id: post_ids).order(:created_at).last + end + + def random_suffix + SecureRandom.hex(12) + end + + def discourse_generated_message_id?(message_id) + !!(message_id =~ message_id_post_id_regexp) || + !!(message_id =~ message_id_topic_id_regexp) + end + + def message_id_post_id_regexp + @message_id_post_id_regexp ||= Regexp.new "topic/\\d+/(\\d+|\\d+\.\\w+)@#{Regexp.escape(host)}" + end + + def message_id_topic_id_regexp + @message_id_topic_id_regexp ||= Regexp.new "topic/(\\d+|\\d+\.\\w+)@#{Regexp.escape(host)}" + end + + def message_id_rfc_format(message_id) + message_id.present? && !is_message_id_rfc?(message_id) ? "<#{message_id}>" : message_id + end + + def message_id_clean(message_id) + message_id.present? && is_message_id_rfc?(message_id) ? message_id.gsub(/^<|>$/, "") : message_id + end + + def is_message_id_rfc?(message_id) + message_id.start_with?('<') && message_id.include?('@') && message_id.end_with?('>') + end + + def host + @host ||= Email::Sender.host_for(Discourse.base_url) + end + end + end +end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 7c76c44d61..2f8c403191 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -107,7 +107,7 @@ module Email # server (e.g. a message_id generated by Gmail) and does not need to # be updated, because message_ids from the IMAP server are not guaranteed # to be unique. - return unless discourse_generated_message_id?(@message_id) + return unless Email::MessageIdService.discourse_generated_message_id?(@message_id) incoming_email.update( imap_uid_validity: @opts[:imap_uid_validity], @@ -801,7 +801,7 @@ module Email # if the user is directly replying to an email send to them from discourse, # there will be a corresponding EmailLog record, so we can use that as the # reply post if it exists - if discourse_generated_message_id?(mail.in_reply_to) + if Email::MessageIdService.discourse_generated_message_id?(mail.in_reply_to) post_id_from_email_log = EmailLog.where(message_id: mail.in_reply_to) .addressed_to_user(user) .order(created_at: :desc) @@ -1056,35 +1056,7 @@ module Email message_ids = Email::Receiver.extract_reply_message_ids(@mail, max_message_id_count: 5) return if message_ids.empty? - post_ids = message_ids.map { |message_id| message_id[message_id_post_id_regexp, 1] }.compact.map(&:to_i) - post_ids << Post.where(topic_id: message_ids.map { |message_id| message_id[message_id_topic_id_regexp, 1] }.compact, post_number: 1).pluck(:id) - post_ids << EmailLog.where(message_id: message_ids).pluck(:post_id) - post_ids << IncomingEmail.where(message_id: message_ids).pluck(:post_id) - - post_ids.flatten! - post_ids.compact! - post_ids.uniq! - - return if post_ids.empty? - - Post.where(id: post_ids).order(:created_at).last - end - - def host - @host ||= Email::Sender.host_for(Discourse.base_url) - end - - def discourse_generated_message_id?(message_id) - !!(message_id =~ message_id_post_id_regexp) || - !!(message_id =~ message_id_topic_id_regexp) - end - - def message_id_post_id_regexp - @message_id_post_id_regexp ||= Regexp.new "topic/\\d+/(\\d+)@#{Regexp.escape(host)}" - end - - def message_id_topic_id_regexp - @message_id_topic_id_regexp ||= Regexp.new "topic/(\\d+)@#{Regexp.escape(host)}" + Email::MessageIdService.find_post_from_message_ids(message_ids) end def self.extract_reply_message_ids(mail, max_message_id_count:) @@ -1100,7 +1072,7 @@ module Email references elsif references.present? references.split(/[\s,]/).map do |r| - Email.message_id_clean(r) + Email::MessageIdService.message_id_clean(r) end end end diff --git a/lib/email/sender.rb b/lib/email/sender.rb index cf2c98b2c0..14a538b4c6 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -109,7 +109,7 @@ module Email ).pluck_first(:id) # always set a default Message ID from the host - @message.header['Message-ID'] = "<#{SecureRandom.uuid}@#{host}>" + @message.header['Message-ID'] = Email::MessageIdService.generate_default if topic_id.present? && post_id.present? post = Post.find_by(id: post_id, topic_id: topic_id) @@ -121,15 +121,9 @@ module Email return skip(SkippedEmailLog.reason_types[:sender_topic_deleted]) if topic.blank? add_attachments(post) - first_post = topic.ordered_posts.first - topic_message_id = first_post.incoming_email&.message_id.present? ? - "<#{first_post.incoming_email.message_id}>" : - "" - - post_message_id = post.incoming_email&.message_id.present? ? - "<#{post.incoming_email.message_id}>" : - "" + topic_message_id = Email::MessageIdService.generate_for_topic(topic, use_incoming_email_if_present: true) + post_message_id = Email::MessageIdService.generate_for_post(post, use_incoming_email_if_present: true) referenced_posts = Post.includes(:incoming_email) .joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ") @@ -141,9 +135,9 @@ module Email "<#{referenced_post.incoming_email.message_id}>" else if referenced_post.post_number == 1 - "" + Email::MessageIdService.generate_for_topic(topic) else - "" + Email::MessageIdService.generate_for_post(referenced_post) end end end diff --git a/lib/imap/providers/generic.rb b/lib/imap/providers/generic.rb index ac18ca9f46..53ec57459d 100644 --- a/lib/imap/providers/generic.rb +++ b/lib/imap/providers/generic.rb @@ -236,7 +236,7 @@ module Imap trashed_email_uids = find_uids_by_message_ids(message_ids) if trashed_email_uids.any? trashed_emails = emails(trashed_email_uids, ["UID", "ENVELOPE"]).map do |e| - BasicMail.new(message_id: Email.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) + BasicMail.new(message_id: Email::MessageIdService.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) end end end @@ -253,7 +253,7 @@ module Imap spam_email_uids = find_uids_by_message_ids(message_ids) if spam_email_uids.any? spam_emails = emails(spam_email_uids, ["UID", "ENVELOPE"]).map do |e| - BasicMail.new(message_id: Email.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) + BasicMail.new(message_id: Email::MessageIdService.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) end end end @@ -266,7 +266,7 @@ module Imap def find_uids_by_message_ids(message_ids) header_message_id_terms = message_ids.map do |msgid| - "HEADER Message-ID '#{Email.message_id_rfc_format(msgid)}'" + "HEADER Message-ID '#{Email::MessageIdService.message_id_rfc_format(msgid)}'" end # OR clauses are written in Polish notation...so the query looks like this: diff --git a/lib/imap/sync.rb b/lib/imap/sync.rb index 040910cbc8..f60338064c 100644 --- a/lib/imap/sync.rb +++ b/lib/imap/sync.rb @@ -138,7 +138,7 @@ module Imap else # try finding email by message-id instead, we may be able to set the uid etc. incoming_email = IncomingEmail.where( - message_id: Email.message_id_clean(email['ENVELOPE'].message_id), + message_id: Email::MessageIdService.message_id_clean(email['ENVELOPE'].message_id), imap_uid: nil, imap_uid_validity: nil ).where("to_addresses LIKE ?", "%#{@group.email_username}%").first diff --git a/spec/components/email/email_spec.rb b/spec/components/email/email_spec.rb index b2127041e3..382c321fe1 100644 --- a/spec/components/email/email_spec.rb +++ b/spec/components/email/email_spec.rb @@ -64,30 +64,4 @@ describe Email do end end - - describe "message_id_rfc_format" do - - it "returns message ID in RFC format" do - expect(Email.message_id_rfc_format("test@test")).to eq("") - end - - it "returns input if already in RFC format" do - expect(Email.message_id_rfc_format("")).to eq("") - end - - end - - describe "message_id_clean" do - - it "returns message ID if in RFC format" do - expect(Email.message_id_clean("")).to eq("test@test") - end - - it "returns input if a clean message ID is not in RFC format" do - message_id = "<" + "@" * 50 - expect(Email.message_id_clean(message_id)).to eq(message_id) - end - - end - end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index a7bbd40650..4b7480e5c5 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -947,6 +947,52 @@ describe Email::Receiver do ordered_posts[1..-1].each(&:trash!) expect { process(:email_reply_4) }.to change { topic.posts.count }.by(1) end + + describe "replying with various message-id formats" do + let!(:topic) do + process(:email_reply_1) + Topic.last + end + let!(:post) { Fabricate(:post, topic: topic) } + + def process_mail_with_message_id(message_id) + mail_string = <<~REPLY + Return-Path: + From: Two + To: one@foo.com + Subject: RE: Testing email threading + Date: Fri, 15 Jan 2016 00:12:43 +0100 + Message-ID: <44@foo.bar.mail> + In-Reply-To: <#{message_id}> + Mime-Version: 1.0 + Content-Type: text/plain + Content-Transfer-Encoding: 7bit + + This is email reply testing with Message-ID formats. + REPLY + Email::Receiver.new(mail_string).process! + end + + it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID@HOST" do + expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}@test.localhost") }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + end + + it "posts a reply using a message-id in the format topic/TOPIC_ID@HOST" do + expect { process_mail_with_message_id("topic/#{topic.id}@test.localhost") }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + end + + it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID.RANDOM_SUFFIX@HOST" do + expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.rjc3yr79834y@test.localhost") }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + end + + it "posts a reply using a message-id in the format topic/TOPIC_ID.RANDOM_SUFFIX@HOST" do + expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.x3487nxy877843x@test.localhost") }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + end + end end it "supports any kind of attachments when 'allow_all_attachments_for_group_messages' is enabled" do @@ -1161,6 +1207,7 @@ describe Email::Receiver do NotificationEmailer.enable SiteSetting.disallow_reply_by_email_after_days = 10000 Jobs.run_immediately! + Email::MessageIdService.stubs(:random_suffix).returns("blah123") end def reply_as_group_user @@ -1185,7 +1232,7 @@ describe Email::Receiver do it "creates an EmailLog when someone from the group replies, and does not create an IncomingEmail record for the reply" do email_log, group_post = reply_as_group_user - expect(email_log.message_id).to eq("topic/#{original_inbound_email_topic.id}/#{group_post.id}@test.localhost") + expect(email_log.message_id).to eq("topic/#{original_inbound_email_topic.id}/#{group_post.id}.blah123@test.localhost") expect(email_log.to_address).to eq("two@foo.com") expect(email_log.email_type).to eq("user_private_message") expect(email_log.post_id).to eq(group_post.id) diff --git a/spec/components/email/sender_spec.rb b/spec/components/email/sender_spec.rb index eedb7e8ef4..a2ab5f6c51 100644 --- a/spec/components/email/sender_spec.rb +++ b/spec/components/email/sender_spec.rb @@ -260,6 +260,7 @@ describe Email::Sender do end context "email threading" do + let(:random_message_id_suffix) { "5f1330cfd941f323d7f99b9e" } fab!(:topic) { Fabricate(:topic) } fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) } @@ -271,14 +272,17 @@ describe Email::Sender do let!(:post_reply_2_4) { PostReply.create(post: post_2, reply: post_4) } let!(:post_reply_3_4) { PostReply.create(post: post_3, reply: post_4) } - before { message.header['X-Discourse-Topic-Id'] = topic.id } + before do + message.header['X-Discourse-Topic-Id'] = topic.id + Email::MessageIdService.stubs(:random_suffix).returns(random_message_id_suffix) + end it "doesn't set the 'In-Reply-To' and 'References' headers on the first post" do message.header['X-Discourse-Post-Id'] = post_1.id email_sender.send - expect(message.header['Message-Id'].to_s).to eq("") + expect(message.header['Message-Id'].to_s).to eq("") expect(message.header['In-Reply-To'].to_s).to be_blank expect(message.header['References'].to_s).to be_blank end @@ -288,8 +292,8 @@ describe Email::Sender do email_sender.send - expect(message.header['Message-Id'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to eq("") + expect(message.header['Message-Id'].to_s).to eq("") + expect(message.header['In-Reply-To'].to_s).to eq("") end it "sets the 'In-Reply-To' header to the newest replied post" do @@ -297,8 +301,8 @@ describe Email::Sender do email_sender.send - expect(message.header['Message-Id'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to eq("") + expect(message.header['Message-Id'].to_s).to eq("") + expect(message.header['In-Reply-To'].to_s).to eq("") end it "sets the 'References' header to the topic and all replied posts" do @@ -307,9 +311,9 @@ describe Email::Sender do email_sender.send references = [ - "", - "", - "", + "", + "", + "", ] expect(message.header['References'].to_s).to eq(references.join(" ")) @@ -328,7 +332,7 @@ describe Email::Sender do references = [ "<#{topic_incoming_email.message_id}>", - "", + "", "<#{post_2_incoming_email.message_id}>", ] diff --git a/spec/jobs/regular/group_smtp_email_spec.rb b/spec/jobs/regular/group_smtp_email_spec.rb index 81600bedad..88872a9394 100644 --- a/spec/jobs/regular/group_smtp_email_spec.rb +++ b/spec/jobs/regular/group_smtp_email_spec.rb @@ -23,6 +23,7 @@ RSpec.describe Jobs::GroupSmtpEmail do let(:staged1) { Fabricate(:staged, email: "otherguy@test.com") } let(:staged2) { Fabricate(:staged, email: "cormac@lit.com") } let(:normaluser) { Fabricate(:user, email: "justanormalguy@test.com", username: "normaluser") } + let(:random_message_id_suffix) { "5f1330cfd941f323d7f99b9e" } before do SiteSetting.enable_smtp = true @@ -34,6 +35,7 @@ RSpec.describe Jobs::GroupSmtpEmail do TopicAllowedUser.create(user: staged1, topic: topic) TopicAllowedUser.create(user: staged2, topic: topic) TopicAllowedUser.create(user: normaluser, topic: topic) + Email::MessageIdService.stubs(:random_suffix).returns(random_message_id_suffix) end it "sends an email using the GroupSmtpMailer and Email::Sender" do @@ -61,7 +63,7 @@ RSpec.describe Jobs::GroupSmtpEmail do PostReply.create(post: second_post, reply: post) subject.execute(args) email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) - expect(email_log.raw_headers).to include("In-Reply-To: ") + expect(email_log.raw_headers).to include("In-Reply-To: ") expect(email_log.as_mail_message.html_part.to_s).not_to include(I18n.t("user_notifications.in_reply_to")) end @@ -82,7 +84,7 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log).not_to eq(nil) - expect(email_log.message_id).to eq("topic/#{post.topic_id}/#{post.id}@test.localhost") + expect(email_log.message_id).to eq("topic/#{post.topic_id}/#{post.id}.#{random_message_id_suffix}@test.localhost") end it "creates an IncomingEmail record with the correct details to avoid double processing IMAP" do @@ -91,7 +93,7 @@ RSpec.describe Jobs::GroupSmtpEmail do expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") incoming_email = IncomingEmail.find_by(post_id: post.id, topic_id: post.topic_id, user_id: post.user.id) expect(incoming_email).not_to eq(nil) - expect(incoming_email.message_id).to eq("topic/#{post.topic_id}/#{post.id}@test.localhost") + expect(incoming_email.message_id).to eq("topic/#{post.topic_id}/#{post.id}.#{random_message_id_suffix}@test.localhost") expect(incoming_email.created_via).to eq(IncomingEmail.created_via_types[:group_smtp]) expect(incoming_email.to_addresses).to eq("test@test.com") expect(incoming_email.cc_addresses).to eq("otherguy@test.com;cormac@lit.com") @@ -115,7 +117,7 @@ RSpec.describe Jobs::GroupSmtpEmail do expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log).not_to eq(nil) - expect(email_log.message_id).to eq("topic/#{post.topic_id}/#{post.id}@test.localhost") + expect(email_log.message_id).to eq("topic/#{post.topic_id}/#{post.id}.#{random_message_id_suffix}@test.localhost") end it "creates an IncomingEmail record with the correct details to avoid double processing IMAP" do @@ -124,7 +126,7 @@ RSpec.describe Jobs::GroupSmtpEmail do expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") incoming_email = IncomingEmail.find_by(post_id: post.id, topic_id: post.topic_id, user_id: post.user.id) expect(incoming_email).not_to eq(nil) - expect(incoming_email.message_id).to eq("topic/#{post.topic_id}/#{post.id}@test.localhost") + expect(incoming_email.message_id).to eq("topic/#{post.topic_id}/#{post.id}.#{random_message_id_suffix}@test.localhost") expect(incoming_email.created_via).to eq(IncomingEmail.created_via_types[:group_smtp]) expect(incoming_email.to_addresses).to eq("test@test.com") expect(incoming_email.cc_addresses).to eq("otherguy@test.com;cormac@lit.com") diff --git a/spec/lib/message_id_service_spec.rb b/spec/lib/message_id_service_spec.rb new file mode 100644 index 0000000000..151b55bff6 --- /dev/null +++ b/spec/lib/message_id_service_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Email::MessageIdService do + fab!(:topic) { Fabricate(:topic) } + fab!(:post) { Fabricate(:post, topic: topic) } + fab!(:second_post) { Fabricate(:post, topic: topic) } + + subject { described_class } + + describe "#generate_for_post" do + it "generates for the post using the message_id on the post's incoming_email" do + Fabricate(:incoming_email, message_id: "test@test.localhost", post: post) + post.reload + expect(subject.generate_for_post(post, use_incoming_email_if_present: true)).to eq("") + end + + it "generates for the post without an incoming_email record" do + expect(subject.generate_for_post(post)).to match(subject.message_id_post_id_regexp) + expect(subject.generate_for_post(post, use_incoming_email_if_present: true)).to match(subject.message_id_post_id_regexp) + end + end + + describe "#generate_for_topic" do + it "generates for the topic using the message_id on the first post's incoming_email" do + Fabricate(:incoming_email, message_id: "test@test.localhost", post: post) + post.reload + expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to eq("") + end + + it "generates for the topic without an incoming_email record" do + expect(subject.generate_for_topic(topic)).to match(subject.message_id_topic_id_regexp) + expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to match(subject.message_id_topic_id_regexp) + end + end + + describe "find_post_from_message_ids" do + let(:post_format_message_id) { "" } + let(:topic_format_message_id) { "" } + let(:default_format_message_id) { "<36ac1ddd-5083-461d-b72c-6372fb0e7f33@test.localhost>" } + let(:gmail_format_message_id) { "" } + + it "finds a post based only on a post-format message id" do + expect(subject.find_post_from_message_ids([post_format_message_id])).to eq(post) + end + + it "finds a post based only on a topic-format message id" do + expect(subject.find_post_from_message_ids([topic_format_message_id])).to eq(post) + end + + it "finds a post from the email log" do + email_log = Fabricate(:email_log, message_id: subject.message_id_clean(default_format_message_id)) + expect(subject.find_post_from_message_ids([default_format_message_id])).to eq(email_log.post) + end + + it "finds a post from the incoming email log" do + incoming_email = Fabricate( + :incoming_email, + message_id: subject.message_id_clean(gmail_format_message_id), + post: Fabricate(:post) + ) + expect(subject.find_post_from_message_ids([gmail_format_message_id])).to eq(incoming_email.post) + end + + it "gets the last created post if multiple are returned" do + incoming_email = Fabricate( + :incoming_email, + message_id: subject.message_id_clean(post_format_message_id), + post: Fabricate(:post, created_at: 10.days.ago) + ) + expect(subject.find_post_from_message_ids([post_format_message_id])).to eq(post) + end + end + + describe "#discourse_generated_message_id?" do + def check_format(message_id) + subject.discourse_generated_message_id?(message_id) + end + + it "works correctly for the different possible formats" do + expect(check_format("topic/1223/4525.3c4f8n9@test.localhost")).to eq(true) + expect(check_format("")).to eq(true) + expect(check_format("topic/1223.fc3j4843@test.localhost")).to eq(true) + expect(check_format("")).to eq(true) + expect(check_format("topic/1223/4525@test.localhost")).to eq(true) + expect(check_format("")).to eq(true) + expect(check_format("topic/1223@test.localhost")).to eq(true) + expect(check_format("")).to eq(true) + + expect(check_format("topic/1223@blah")).to eq(false) + expect(check_format("")).to eq(false) + expect(check_format("t/1223@test.localhost")).to eq(false) + end + end + + describe "#message_id_rfc_format" do + it "returns message ID in RFC format" do + expect(Email::MessageIdService.message_id_rfc_format("test@test")).to eq("") + end + + it "returns input if already in RFC format" do + expect(Email::MessageIdService.message_id_rfc_format("")).to eq("") + end + end + + describe "#message_id_clean" do + it "returns message ID if in RFC format" do + expect(Email::MessageIdService.message_id_clean("")).to eq("test@test") + end + + it "returns input if a clean message ID is not in RFC format" do + message_id = "<" + "@" * 50 + expect(Email::MessageIdService.message_id_clean(message_id)).to eq(message_id) + end + end +end From 4bb91754ad43540efd9c34e093545d4caffcb0ef Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Mon, 6 Dec 2021 01:55:34 +0100 Subject: [PATCH 042/119] FIX: Make user themes sort order case insensitive (#15193) That's the order they appear in a dropdown in user preferences. --- app/serializers/site_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index c7d8d5d66c..29972ae8f5 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -42,7 +42,7 @@ class SiteSerializer < ApplicationSerializer cache_fragment("user_themes") do Theme.where('id = :default OR user_selectable', default: SiteSetting.default_theme_id) - .order(:name) + .order("lower(name)") .pluck(:id, :name, :color_scheme_id) .map { |id, n, cs| { theme_id: id, name: n, default: id == SiteSetting.default_theme_id, color_scheme_id: cs } } .as_json From 28bf9599f5c7066ce3210a60d27b69704ac01015 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Mon, 6 Dec 2021 02:08:21 +0100 Subject: [PATCH 043/119] FEATURE: Pre-setting user locale via bulk invite (#15195) --- app/jobs/regular/bulk_invite.rb | 22 ++++++++++++++++++---- spec/fixtures/csv/invites_with_locales.csv | 4 ++++ spec/requests/invites_controller_spec.rb | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 spec/fixtures/csv/invites_with_locales.csv diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb index 3f22282c25..abef2ee196 100644 --- a/app/jobs/regular/bulk_invite.rb +++ b/app/jobs/regular/bulk_invite.rb @@ -111,7 +111,8 @@ module Jobs email = invite[:email] groups = get_groups(invite[:groups]) topic = get_topic(invite[:topic_id]) - user_fields = get_user_fields(invite.except(:email, :groups, :topic_id)) + locale = invite[:locale] + user_fields = get_user_fields(invite.except(:email, :groups, :topic_id, :locale)) begin if user = Invite.find_user_by_email(email) @@ -133,13 +134,26 @@ module Jobs end user.save_custom_fields end + + if locale.present? + user.locale = locale + user.save! + end else - if user_fields.present? + if user_fields.present? || locale.present? user = User.where(staged: true).find_by_email(email) user ||= User.new(username: UserNameSuggester.suggest(email), email: email, staged: true) - user_fields.each do |user_field, value| - user.set_user_field(user_field, value) + + if user_fields.present? + user_fields.each do |user_field, value| + user.set_user_field(user_field, value) + end end + + if locale.present? + user.locale = locale + end + user.save! end diff --git a/spec/fixtures/csv/invites_with_locales.csv b/spec/fixtures/csv/invites_with_locales.csv new file mode 100644 index 0000000000..d5ed6ffa0b --- /dev/null +++ b/spec/fixtures/csv/invites_with_locales.csv @@ -0,0 +1,4 @@ +email,locale,team +test@example.com,de,red +test2@example.com,pl,blue +test3@example.com,,red diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb index ac4f784ae9..4727df2dd1 100644 --- a/spec/requests/invites_controller_spec.rb +++ b/spec/requests/invites_controller_spec.rb @@ -896,6 +896,8 @@ describe InvitesController do let(:csv_file_with_headers) { File.new("#{Rails.root}/spec/fixtures/csv/discourse_headers.csv") } let(:file_with_headers) { Rack::Test::UploadedFile.new(File.open(csv_file_with_headers)) } + let(:csv_file_with_locales) { File.new("#{Rails.root}/spec/fixtures/csv/invites_with_locales.csv") } + let(:file_with_locales) { Rack::Test::UploadedFile.new(File.open(csv_file_with_locales)) } it 'fails if you cannot bulk invite to the forum' do sign_in(Fabricate(:user)) @@ -947,6 +949,20 @@ describe InvitesController do user2 = User.where(staged: true).find_by_email('test2@example.com') expect(user2.user_fields[user_field.id.to_s]).to eq('europe') end + + it 'can pre-set user locales' do + Jobs.run_immediately! + sign_in(admin) + + post '/invites/upload_csv.json', params: { file: file_with_locales, name: 'discourse_headers.csv' } + expect(response.status).to eq(200) + + user = User.where(staged: true).find_by_email('test@example.com') + expect(user.locale).to eq('de') + + user2 = User.where(staged: true).find_by_email('test2@example.com') + expect(user2.locale).to eq('pl') + end end end end From ce074d118e7a9bb0c285eb56490c038a46af6859 Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Mon, 6 Dec 2021 02:37:54 +0100 Subject: [PATCH 044/119] DEV: drop unused method (#15190) There are no usages in Core and plugins. The last usage was removed in https://github.com/discourse/discourse/pull/9369. --- .../discourse/app/widgets/quick-access-bookmarks.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/assets/javascripts/discourse/app/widgets/quick-access-bookmarks.js b/app/assets/javascripts/discourse/app/widgets/quick-access-bookmarks.js index f315114ca6..4980748435 100644 --- a/app/assets/javascripts/discourse/app/widgets/quick-access-bookmarks.js +++ b/app/assets/javascripts/discourse/app/widgets/quick-access-bookmarks.js @@ -1,7 +1,6 @@ import RawHtml from "discourse/widgets/raw-html"; import { iconHTML } from "discourse-common/lib/icon-library"; import QuickAccessPanel from "discourse/widgets/quick-access-panel"; -import UserAction from "discourse/models/user-action"; import { ajax } from "discourse/lib/ajax"; import { createWidget, createWidgetFrom } from "discourse/widgets/widget"; import { h } from "virtual-dom"; @@ -72,14 +71,4 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", { ({ user_bookmark_list }) => user_bookmark_list.bookmarks ); }, - - loadUserActivityBookmarks() { - return ajax("/user_actions.json", { - data: { - username: this.currentUser.username, - filter: UserAction.TYPES.bookmarks, - no_results_help_key: "user_activity.no_bookmarks", - }, - }).then(({ user_actions }) => user_actions); - }, }); From 0b364140ec21f6a0e5d38c79c185fc15fd97ad63 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 6 Dec 2021 01:38:37 +0000 Subject: [PATCH 045/119] DEV: Add :before_email_login event for plugins (#15187) --- app/controllers/users_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index efc2b4984d..46112fa274 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -928,6 +928,8 @@ class UsersController < ApplicationController RateLimiter.new(nil, "email-login-min-#{user.id}", 3, 1.minute).performed! if user_presence + DiscourseEvent.trigger(:before_email_login, user) + email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) Jobs.enqueue(:critical_user_email, From 10cc082560a040288654ea1731024489c5d4eeeb Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Mon, 6 Dec 2021 12:06:35 +0100 Subject: [PATCH 046/119] FIX: when using external auth disallowed characters weren't removed from username (#15185) --- lib/auth/result.rb | 28 +++++--- .../omniauth_callbacks_controller_spec.rb | 66 +++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/lib/auth/result.rb b/lib/auth/result.rb index a5732810d1..2b0ba30c2f 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -135,18 +135,9 @@ class Auth::Result return result end - suggested_username = UserNameSuggester.suggest(username_suggester_attributes) - if email_valid && email.present? - if username.present? && User.username_available?(username, email) - suggested_username = username - elsif staged_user = User.where(staged: true).find_by_email(email) - suggested_username = staged_user.username - end - end - result = { email: email, - username: suggested_username, + username: resolve_username, auth_provider: authenticator_name, email_valid: !!email_valid, can_edit_username: can_edit_username, @@ -165,7 +156,24 @@ class Auth::Result private + def staged_user + return @staged_user if defined?(@staged_user) + if email.present? && email_valid + @staged_user = User.where(staged: true).find_by_email(email) + end + end + def username_suggester_attributes username || name || email end + + def resolve_username + if staged_user + if !username.present? || UserNameSuggester.fix_username(username) == staged_user.username + return staged_user.username + end + end + + UserNameSuggester.suggest(username_suggester_attributes) + end end diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index 5e774196c3..47754a28a8 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -729,6 +729,18 @@ RSpec.describe Users::OmniauthCallbacksController do expect(cookies['authentication_data']).to be_nil end + + it "removes disallowed characters from username" do + username = "strange_name*&^" + fixed_username = "strange_name" + + mock_auth("user.with.strange.username@gmail.com", username) + + get "/auth/google_oauth2/callback.json" + data = JSON.parse(cookies[:authentication_data]) + + expect(data["username"]).to eq(fixed_username) + end end context 'when attempting reconnect' do @@ -881,5 +893,59 @@ RSpec.describe Users::OmniauthCallbacksController do expect(response['email']).to eq(new_email) end end + + context "when user is staged" do + fab!(:staged_user) { Fabricate( + :user, + username: "staged_user", + email: "staged.user@gmail.com", + staged: true + ) + } + + it "should use username of the staged user if username is not present in payload" do + mock_auth(staged_user.email, nil) + + get "/auth/google_oauth2/callback.json" + data = JSON.parse(cookies[:authentication_data]) + + expect(data["username"]).to eq(staged_user.username) + end + + it "should use username of the staged user if username in payload is the same" do + mock_auth(staged_user.email, staged_user.username) + + get "/auth/google_oauth2/callback.json" + data = JSON.parse(cookies[:authentication_data]) + + expect(data["username"]).to eq(staged_user.username) + end + + it "should override username of the staged user if payload contains a new username" do + new_username = "new_username" + mock_auth(staged_user.email, new_username) + + get "/auth/google_oauth2/callback.json" + data = JSON.parse(cookies[:authentication_data]) + + expect(data["username"]).to eq(new_username) + end + end + + def mock_auth(email, nickname) + OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( + provider: 'google_oauth2', + uid: '123545', + info: OmniAuth::AuthHash::InfoHash.new( + email: email, + nickname: nickname + ), + extra: { + raw_info: OmniAuth::AuthHash.new(email_verified: true) + } + ) + + Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] + end end end From 3ebce550fe7b60c5de966eec1efd9d585910e57e Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Mon, 6 Dec 2021 09:10:14 -0600 Subject: [PATCH 047/119] DEV: Make add_api_parameter_route parameter deprecations errors (#15198) Since we said we would remove support in 2.7, this is overdue. --- lib/plugin/instance.rb | 4 ++-- spec/integration/api_keys_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 71b92a42de..902aaa4a00 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -907,14 +907,14 @@ class Plugin::Instance format: nil, formats: nil) if Array(format).include?("*") - Discourse.deprecate("* is no longer a valid api_parameter_route format matcher. Use `nil` instead", drop_from: "2.7") + Discourse.deprecate("* is no longer a valid api_parameter_route format matcher. Use `nil` instead", drop_from: "2.7", raise_error: true) # Old API used * as wildcard. New api uses `nil` format = nil end # Backwards compatibility with old parameter names: if method || route || format - Discourse.deprecate("method, route and format parameters for api_parameter_routes are deprecated. Use methods, actions and formats instead.", drop_from: "2.7") + Discourse.deprecate("method, route and format parameters for api_parameter_routes are deprecated. Use methods, actions and formats instead.", drop_from: "2.7", raise_error: true) methods ||= method actions ||= route formats ||= format diff --git a/spec/integration/api_keys_spec.rb b/spec/integration/api_keys_spec.rb index 60598cfbcf..39b6ffb3ff 100644 --- a/spec/integration/api_keys_spec.rb +++ b/spec/integration/api_keys_spec.rb @@ -51,7 +51,7 @@ describe 'api keys' do context "with a plugin registered filter" do before do plugin = Plugin::Instance.new - plugin.add_api_parameter_route method: :get, route: "session#current", format: "*" + plugin.add_api_parameter_route methods: [:get], actions: ["session#current"] end it 'allows parameter access to the registered route' do From 43903f8dfe76617c858a32208983b2148b6993ab Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Mon, 6 Dec 2021 12:31:44 -0300 Subject: [PATCH 048/119] FIX: Updating a consolidated notification should bump it to the top. (#15199) In the future, it would be better to have a consolidated_at timestamp instead of updating created_at. --- .../consolidate_notifications.rb | 21 ++++++++++++---- spec/services/post_alerter_spec.rb | 25 +++++++++++-------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/services/notifications/consolidate_notifications.rb b/app/services/notifications/consolidate_notifications.rb index 83d5f7e901..93da4a6af3 100644 --- a/app/services/notifications/consolidate_notifications.rb +++ b/app/services/notifications/consolidate_notifications.rb @@ -14,6 +14,7 @@ # - consolidation_window: Only consolidate notifications created since this value (Pass a ActiveSupport::Duration instance, and we'll call #ago on it). # - unconsolidated_query_blk: A block with additional queries to apply when fetching for unconsolidated notifications. # - consolidated_query_blk: A block with additional queries to apply when fetching for a consolidated notification. +# - bump_notification: Bump the consolidated notification to the top after updating it. # # Need to call #set_precondition to configure this: # @@ -25,7 +26,7 @@ module Notifications class ConsolidateNotifications - def initialize(from:, to:, consolidation_window: nil, unconsolidated_query_blk: nil, consolidated_query_blk: nil, threshold:) + def initialize(from:, to:, consolidation_window: nil, unconsolidated_query_blk: nil, consolidated_query_blk: nil, threshold:, bump_notification: true) @from = from @to = to @threshold = threshold @@ -34,6 +35,7 @@ module Notifications @unconsolidated_query_blk = unconsolidated_query_blk @precondition_blk = nil @set_data_blk = nil + @bump_notification = bump_notification end def set_precondition(precondition_blk: nil) @@ -69,7 +71,10 @@ module Notifications private - attr_reader :notification, :from, :to, :data, :threshold, :consolidated_query_blk, :unconsolidated_query_blk, :consolidation_window + attr_reader( + :notification, :from, :to, :data, :threshold, :consolidated_query_blk, + :unconsolidated_query_blk, :consolidation_window, :bump_notification + ) def consolidated_data(notification) return notification.data_hash if @set_data_blk.nil? @@ -91,11 +96,17 @@ module Notifications # Hack: We don't want to cache the old data if we're about to update it. consolidated.instance_variable_set(:@data_hash, nil) - consolidated.update!( + attrs = { data: data_hash.to_json, read: false, - updated_at: timestamp - ) + updated_at: timestamp, + } + + # Updating created_at may seem wrong, but it's the only way of bumping the notification. + # We cannot order by updated_at because marking them as read will move them to the top. + attrs[:created_at] = timestamp if bump_notification + + consolidated.update!(attrs) consolidated end diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index d8a2c7722f..cd24804977 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -103,7 +103,8 @@ describe PostAlerter do expect(notification_payload["group_name"]).to eq(group.name) end - it 'consolidates group summary notifications by bumping an existing one' do + it 'updates the consolidated group summary inbox count and bumps the notification' do + user2.update!(last_seen_at: 5.minutes.ago) TopicUser.change(user2.id, pm.id, notification_level: TopicUser.notification_levels[:tracking]) PostAlerter.post_created(op) @@ -113,21 +114,25 @@ describe PostAlerter do ).last starting_count = group_summary_notification.data_hash[:inbox_count] - expect(starting_count).to eq(1) + # Create another notification to ensure summary is correctly bumped + user2_post = Fabricate(:post, topic: pm, user: user2) + PostAlerter.new.create_notification( + user2, Notification.types[:liked], user2_post, user_id: pm.user, display_username: pm.user.username + ) + Notification.where(user: user2).update_all('read = true') another_pm = Fabricate(:topic, archetype: 'private_message', category_id: nil, allowed_groups: [group]) another_post = Fabricate(:post, user: another_pm.user, topic: another_pm) TopicUser.change(user2.id, another_pm.id, notification_level: TopicUser.notification_levels[:tracking]) - PostAlerter.post_created(another_post) - consolidated_summary = Notification.where( - user_id: user2.id, - notification_type: Notification.types[:group_message_summary] - ).last - updated_inbox_count = consolidated_summary.data_hash[:inbox_count] + message_data = MessageBus.track_publish("/notification/#{user2.id}") do + PostAlerter.post_created(another_post) + end.first.data - expect(group_summary_notification.id).to eq(consolidated_summary.id) - expect(updated_inbox_count).to eq(starting_count + 1) + expect(Notification.recent_report(user2, 1).first.notification_type).to eq(Notification.types[:group_message_summary]) + expect(message_data.dig(:last_notification, :notification, :id)).to eq(group_summary_notification.id) + expect(message_data.dig(:last_notification, :notification, :data, :inbox_count)).to eq(starting_count + 1) + expect(message_data[:unread_notifications]).to eq(1) end end end From bf18145e70c80266105abcf30e39594bc94920b8 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Mon, 6 Dec 2021 11:28:10 -0500 Subject: [PATCH 049/119] UX: Fix flair dropdown styling in user account (#15201) --- .../common/select-kit/flair-row.scss | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/common/select-kit/flair-row.scss b/app/assets/stylesheets/common/select-kit/flair-row.scss index 90d5d47c84..224ce3836d 100644 --- a/app/assets/stylesheets/common/select-kit/flair-row.scss +++ b/app/assets/stylesheets/common/select-kit/flair-row.scss @@ -1,3 +1,5 @@ +$flair-size: 18px; + .select-kit.flair-chooser { .select-kit-header, .flair-row { @@ -5,22 +7,22 @@ align-items: center; background-position: center; background-repeat: no-repeat; - background-size: 30px 30px; + background-size: $flair-size $flair-size; display: flex; justify-content: center; margin-right: 5px; - height: 30px; - width: 30px; + height: $flair-size; + width: $flair-size; &.rounded { - background-size: (30px / 1.4) (30px / 1.4); + background-size: ($flair-size / 1.4) ($flair-size / 1.4); border-radius: 50%; } .d-icon { display: block; - height: (30px / 1.8); - width: (30px / 1.8); + height: ($flair-size / 1.8); + width: ($flair-size / 1.8); } } @@ -28,8 +30,4 @@ white-space: nowrap; } } - - .select-kit-header { - padding: 2px 4px; - } } From f3508065a3313735654def72872a08e825555bef Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Mon, 6 Dec 2021 17:49:04 +0100 Subject: [PATCH 050/119] FIX: auth incorrectly handles duplicate usernames (#15197) --- app/services/username_changer.rb | 2 +- lib/user_name_suggester.rb | 38 ++++++++++++++----- spec/components/user_name_suggester_spec.rb | 15 ++++++++ spec/models/discourse_single_sign_on_spec.rb | 24 ++++++++++++ .../omniauth_callbacks_controller_spec.rb | 28 +++++++++++++- 5 files changed, 95 insertions(+), 12 deletions(-) diff --git a/app/services/username_changer.rb b/app/services/username_changer.rb index fff781ab97..8cef5901b9 100644 --- a/app/services/username_changer.rb +++ b/app/services/username_changer.rb @@ -19,7 +19,7 @@ class UsernameChanger UsernameChanger.change(user, new_username, user) true elsif user.username != UserNameSuggester.fix_username(new_username) - suggested_username = UserNameSuggester.suggest(new_username) + suggested_username = UserNameSuggester.suggest(new_username, user.username) UsernameChanger.change(user, suggested_username, user) true else diff --git a/lib/user_name_suggester.rb b/lib/user_name_suggester.rb index dcb7877339..ada89f0503 100644 --- a/lib/user_name_suggester.rb +++ b/lib/user_name_suggester.rb @@ -4,9 +4,10 @@ module UserNameSuggester GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team'] LAST_RESORT_USERNAME = "user" - def self.suggest(name_or_email) + def self.suggest(name_or_email, current_username = nil) name = parse_name_from_email(name_or_email) - find_available_username_based_on(name) + name = fix_username(name) + find_available_username_based_on(name, current_username) end def self.parse_name_from_email(name_or_email) @@ -20,13 +21,21 @@ module UserNameSuggester name end - def self.find_available_username_based_on(name) - name = fix_username(name) + def self.find_available_username_based_on(name, current_username = nil) offset = nil i = 1 attempt = name - until User.username_available?(attempt) || i > 100 + normalized_attempt = User.normalize_username(attempt) + + original_allowed_username = current_username + current_username = User.normalize_username(current_username) if current_username + + until ( + normalized_attempt == current_username || + User.username_available?(attempt) || + i > 100 + ) if offset.nil? normalized = User.normalize_username(name) @@ -42,7 +51,8 @@ module UserNameSuggester params = { count: count + 10, - name: normalized + name: normalized, + allowed_normalized: current_username || '' } # increasing the search space a bit to allow for some extra noise @@ -50,7 +60,11 @@ module UserNameSuggester WITH numbers AS (SELECT generate_series(1, :count) AS n) SELECT n FROM numbers - LEFT JOIN users ON (username_lower = :name || n::varchar) + LEFT JOIN users ON ( + username_lower = :name || n::varchar + ) AND ( + username_lower <> :allowed_normalized + ) WHERE users.id IS NULL ORDER by n ASC LIMIT 1 @@ -68,15 +82,21 @@ module UserNameSuggester max_length = User.username_length.end - suffix.length attempt = "#{truncate(name, max_length)}#{suffix}" + normalized_attempt = User.normalize_username(attempt) i += 1 end - until User.username_available?(attempt) || i > 200 + until normalized_attempt == current_username || User.username_available?(attempt) || i > 200 attempt = SecureRandom.hex[1..SiteSetting.max_username_length] + normalized_attempt = User.normalize_username(attempt) i += 1 end - attempt + if current_username == normalized_attempt + original_allowed_username + else + attempt + end end def self.fix_username(name) diff --git a/spec/components/user_name_suggester_spec.rb b/spec/components/user_name_suggester_spec.rb index 21a44b2c10..7fb9a186be 100644 --- a/spec/components/user_name_suggester_spec.rb +++ b/spec/components/user_name_suggester_spec.rb @@ -118,6 +118,21 @@ describe UserNameSuggester do expect(UserNameSuggester.suggest('uuuuuuu_u')).to eq('uuuuuuu1') end + it 'preserves current username' do + # if several users have username "bill" on the external site, + # they will have usernames bill, bill1, bill2 etc in Discourse: + Fabricate(:user, username: "bill") + Fabricate(:user, username: "bill1") + Fabricate(:user, username: "bill2") + Fabricate(:user, username: "bill3") + Fabricate(:user, username: "bill4") + + # the number should be preserved, bill3 should remain bill3 + suggestion = UserNameSuggester.suggest("bill", "bill3") + + expect(suggestion).to eq "bill3" + end + context "with Unicode usernames disabled" do before { SiteSetting.unicode_usernames = false } diff --git a/spec/models/discourse_single_sign_on_spec.rb b/spec/models/discourse_single_sign_on_spec.rb index fdd34c579e..51fdaff791 100644 --- a/spec/models/discourse_single_sign_on_spec.rb +++ b/spec/models/discourse_single_sign_on_spec.rb @@ -366,6 +366,30 @@ describe DiscourseSingleSignOn do expect(user.username).to eq "testuser" end + it 'should preserve username when several users login with the same username' do + SiteSetting.auth_overrides_username = true + + # if several users have username "bill" on the external site, + # they will have usernames bill, bill1, bill2 etc in Discourse: + Fabricate(:user, username: "bill") + Fabricate(:user, username: "bill1") + Fabricate(:user, username: "bill2") + Fabricate(:user, username: "bill4") + + # the number should be preserved during subsequent logins + # bill3 should remain bill3 + sso = new_discourse_sso + sso.username = "bill3" + sso.email = "test@test.com" + sso.external_id = "100" + sso.lookup_or_create_user(ip_address) + + sso.username = "bill" + user = sso.lookup_or_create_user(ip_address) + + expect(user.username).to eq "bill3" + end + it "doesn't use email as a source for username suggestions by default" do sso = new_discourse_sso sso.external_id = "100" diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index 47754a28a8..eae07abf9a 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -467,6 +467,30 @@ RSpec.describe Users::OmniauthCallbacksController do expect(user.name).to eq('Some name') end + it "should preserve username when several users login with the same username" do + SiteSetting.auth_overrides_username = true + + # if several users have username "bill" on the external site, + # they will have usernames bill, bill1, bill2 etc in Discourse: + Fabricate(:user, username: "bill") + Fabricate(:user, username: "bill1") + Fabricate(:user, username: "bill2") + Fabricate(:user, username: "bill4") + + # the number should be preserved during subsequent logins + # bill3 should remain bill3 + user.update!(username: 'bill3') + + uid = "12345" + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user.id, provider_uid: uid) + mock_auth(user.email, "bill", uid) + + get "/auth/google_oauth2/callback.json" + + user.reload + expect(user.username).to eq('bill3') + end + it "will not update email if not verified" do SiteSetting.email_editable = false SiteSetting.auth_overrides_email = true @@ -932,10 +956,10 @@ RSpec.describe Users::OmniauthCallbacksController do end end - def mock_auth(email, nickname) + def mock_auth(email, nickname, uid = '12345') OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( provider: 'google_oauth2', - uid: '123545', + uid: uid, info: OmniAuth::AuthHash::InfoHash.new( email: email, nickname: nickname From 44588255fc37ac1d76a484bdd8c1b1c6b37777a2 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Mon, 6 Dec 2021 10:27:25 +0800 Subject: [PATCH 051/119] FEATURE: Introduce API scopes for badges. --- app/models/api_key_scope.rb | 9 +++++++++ config/locales/client.en.yml | 8 ++++++++ spec/requests/admin/api_controller_spec.rb | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/models/api_key_scope.rb b/app/models/api_key_scope.rb index 08cf32a9dd..ba57361fa9 100644 --- a/app/models/api_key_scope.rb +++ b/app/models/api_key_scope.rb @@ -51,6 +51,15 @@ class ApiKeyScope < ActiveRecord::Base }, email: { receive_emails: { actions: %w[admin/email#handle_mail] } + }, + badges: { + create: { actions: %w[admin/badges#create] }, + show: { actions: %w[badges#show] }, + update: { actions: %w[admin/badges#update] }, + delete: { actions: %w[admin/badges#destroy] }, + list_user_badges: { actions: %w[user_badges#username], params: %i[username] }, + assign_badge_to_user: { actions: %w[user_badges#create], params: %i[username] }, + revoke_badge_from_user: { actions: %w[user_badges#destroy] }, } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3f6b5e59d4..02761797b7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4219,6 +4219,14 @@ en: list: Get a list of users. email: receive_emails: Combine this scope with the mail-receiver to process incoming emails. + badges: + create: Create a new badge. + show: Obtain information about a badge. + update: Update a badge. + delete: Delete a badge. + list_user_badges: List user badges. + assign_badge_to_user: Assign a badge to a user. + revoke_badge_from_user: Revoke a badge from a user. web_hooks: title: "Webhooks" diff --git a/spec/requests/admin/api_controller_spec.rb b/spec/requests/admin/api_controller_spec.rb index 2be52a0a97..8902472069 100644 --- a/spec/requests/admin/api_controller_spec.rb +++ b/spec/requests/admin/api_controller_spec.rb @@ -237,7 +237,7 @@ describe Admin::ApiController do scopes = response.parsed_body['scopes'] - expect(scopes.keys).to contain_exactly('topics', 'users', 'email', 'posts', 'uploads', 'global') + expect(scopes.keys).to contain_exactly('topics', 'users', 'email', 'posts', 'uploads', 'global', 'badges') end end end From 4e67297a7c08c4548667150db00ffbc233f894d2 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Mon, 6 Dec 2021 10:51:47 +0800 Subject: [PATCH 052/119] FIX: Missing allowed urls when displaying granualar API key scopes. Follow-up to 3791fbd9196c5a10d2723e3c46e7cf8f008caa4c --- app/models/api_key_scope.rb | 15 ++++++--------- spec/models/api_key_scope_spec.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 spec/models/api_key_scope_spec.rb diff --git a/app/models/api_key_scope.rb b/app/models/api_key_scope.rb index ba57361fa9..f0dcc91415 100644 --- a/app/models/api_key_scope.rb +++ b/app/models/api_key_scope.rb @@ -88,32 +88,29 @@ class ApiKeyScope < ActiveRecord::Base end def find_urls(actions:, methods:) - action_urls = [] - method_urls = [] + urls = [] if actions.present? - Rails.application.routes.routes.reduce([]) do |memo, route| + Rails.application.routes.routes.each do |route| defaults = route.defaults action = "#{defaults[:controller].to_s}##{defaults[:action]}" path = route.path.spec.to_s.gsub(/\(\.:format\)/, '') api_supported_path = path.end_with?('.rss') || route.path.requirements[:format]&.match?('json') excluded_paths = %w[/new-topic /new-message /exception] - memo.tap do |m| - if actions.include?(action) && api_supported_path && !excluded_paths.include?(path) - m << "#{path} (#{route.verb})" - end + if actions.include?(action) && api_supported_path && !excluded_paths.include?(path) + urls << "#{path} (#{route.verb})" end end end if methods.present? methods.each do |method| - method_urls << "* (#{method})" + urls << "* (#{method})" end end - action_urls + method_urls + urls end end diff --git a/spec/models/api_key_scope_spec.rb b/spec/models/api_key_scope_spec.rb new file mode 100644 index 0000000000..48ad48d4e9 --- /dev/null +++ b/spec/models/api_key_scope_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ApiKeyScope do + describe '.find_urls' do + it 'should return the right urls' do + expect(ApiKeyScope.find_urls(actions: ["posts#create"], methods: [])) + .to contain_exactly("/posts (POST)") + end + end +end From 9a6ec1d0c61b98b763c06ba6ae67809de29f22eb Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Mon, 6 Dec 2021 19:17:32 +0200 Subject: [PATCH 053/119] PERF: Add index on email_tokens.token_hash --- app/models/email_token.rb | 5 +++-- ...211206160211_create_index_on_email_tokens_token_hash.rb | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20211206160211_create_index_on_email_tokens_token_hash.rb diff --git a/app/models/email_token.rb b/app/models/email_token.rb index 020a011284..2d3dee9b00 100644 --- a/app/models/email_token.rb +++ b/app/models/email_token.rb @@ -121,6 +121,7 @@ end # # Indexes # -# index_email_tokens_on_token (token) UNIQUE -# index_email_tokens_on_user_id (user_id) +# index_email_tokens_on_token (token) UNIQUE +# index_email_tokens_on_token_hash (token_hash) UNIQUE +# index_email_tokens_on_user_id (user_id) # diff --git a/db/migrate/20211206160211_create_index_on_email_tokens_token_hash.rb b/db/migrate/20211206160211_create_index_on_email_tokens_token_hash.rb new file mode 100644 index 0000000000..1fdffdddb1 --- /dev/null +++ b/db/migrate/20211206160211_create_index_on_email_tokens_token_hash.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CreateIndexOnEmailTokensTokenHash < ActiveRecord::Migration[6.1] + def change + add_index :email_tokens, :token_hash, unique: true + end +end From a616bc296aecd1f671fa7c9e09bfe2ce4317945c Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Tue, 7 Dec 2021 05:44:55 +0100 Subject: [PATCH 054/119] FIX: tag transition only if tag name changed (#15149) We need to change path only if tag name is changed. If a description is added, we don't need to reload. --- .../discourse/app/components/tag-info.js | 8 +++++-- .../discourse/tests/acceptance/tags-test.js | 23 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/tag-info.js b/app/assets/javascripts/discourse/app/components/tag-info.js index 60ddb3ba60..e1402a2327 100644 --- a/app/assets/javascripts/discourse/app/components/tag-info.js +++ b/app/assets/javascripts/discourse/app/components/tag-info.js @@ -93,13 +93,17 @@ export default Component.extend({ }, finishedEditing() { + const oldTagName = this.tag.id; this.tag .update({ id: this.newTagName, description: this.newTagDescription }) .then((result) => { this.set("editing", false); this.tagInfo.set("description", this.newTagDescription); - if (result.payload) { - this.router.transitionTo("tag.show", result.payload.id); + if ( + result.responseJson.tag && + oldTagName !== result.responseJson.tag.id + ) { + this.router.transitionTo("tag.show", result.responseJson.tag.id); } }) .catch(popupAjaxError); diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js index 357c26c759..b658bf8d36 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js @@ -8,7 +8,7 @@ import { queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; -import { click, currentURL, visit } from "@ember/test-helpers"; +import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; import { test } from "qunit"; acceptance("Tags", function (needs) { @@ -350,6 +350,10 @@ acceptance("Tag info", function (needs) { ], }); }); + server.put("/tag/happy-monkey", (request) => { + const data = helper.parsePostData(request.requestBody); + return helper.response({ tag: { id: data.tag.id } }); + }); server.get("/tag/happy-monkey/info", () => { return helper.response({ @@ -452,6 +456,23 @@ acceptance("Tag info", function (needs) { "happy monkey description", "it displays original tag description" ); + + await fillIn("#edit-description", "new description"); + await click(".submit-edit"); + assert.strictEqual( + currentURL(), + "/tag/happy-monkey", + "it doesn't change URL" + ); + + await click("#edit-tag"); + await fillIn("#edit-name", "happy-monkey2"); + await click(".submit-edit"); + assert.strictEqual( + currentURL(), + "/tag/happy-monkey2", + "it changes URL to new tag path" + ); }); test("can filter tags page by category", async function (assert) { From 412a6c0e8c5df02b5a5911864f9b07dfe9765f18 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Tue, 7 Dec 2021 07:24:55 +0100 Subject: [PATCH 055/119] FIX: edit tag test (#15207) Broken with PR https://github.com/discourse/discourse/pull/15149 --- .../javascripts/discourse/app/templates/components/tag-info.hbs | 2 +- app/assets/javascripts/discourse/tests/acceptance/tags-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs b/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs index ca465bf1a4..f598d6d939 100644 --- a/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/tag-info.hbs @@ -17,7 +17,7 @@
    {{discourse-tag tagInfo.name tagName="div" size="large"}} {{#if canAdminTag}} - {{d-button action=(action "edit") class="btn-flat edit-tag" title=(i18n "tagging.edit_tag") icon="pencil-alt" }} + {{d-button action=(action "edit") class="btn-flat edit-tag" title="tagging.edit_tag" icon="pencil-alt" }} {{/if}}
    diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js index b658bf8d36..896225349b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js @@ -465,7 +465,7 @@ acceptance("Tag info", function (needs) { "it doesn't change URL" ); - await click("#edit-tag"); + await click(".edit-tag"); await fillIn("#edit-name", "happy-monkey2"); await click(".submit-edit"); assert.strictEqual( From d0888c190e503cc9ec623ea16dc2f8a8d80cd548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Guitaut?= Date: Thu, 2 Dec 2021 18:03:43 +0100 Subject: [PATCH 056/119] FIX: Display pending posts in a moderated category MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently we display pending posts in topics (both for author and staff members) but the feature is only enabled when there’s an enabled global site setting related to moderation. This patch allows to have the same behavior for a site where there’s nothing enabled globally but where a moderated category exists. So when browsing a topic of a moderated category, the presence of pending posts will be checked whereas nothing will happen in a normal category. --- app/serializers/topic_view_serializer.rb | 4 +-- lib/topic_view.rb | 6 +++- spec/components/topic_view_spec.rb | 36 +++++++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index b300b36802..0d717fa2b5 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -244,7 +244,7 @@ class TopicViewSerializer < ApplicationSerializer alias_method :include_is_shared_draft?, :include_destination_category_id? def include_pending_posts? - scope.authenticated? && object.queued_posts_enabled + scope.authenticated? && object.queued_posts_enabled? end def queued_posts_count @@ -252,7 +252,7 @@ class TopicViewSerializer < ApplicationSerializer end def include_queued_posts_count? - scope.is_staff? && object.queued_posts_enabled + scope.is_staff? && object.queued_posts_enabled? end def show_read_indicator diff --git a/lib/topic_view.rb b/lib/topic_view.rb index d5014768ef..4c8d4ac233 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -35,6 +35,7 @@ class TopicView :personal_message, :can_review_topic ) + alias queued_posts_enabled? queued_posts_enabled attr_accessor( :draft, @@ -45,6 +46,9 @@ class TopicView :post_number ) + delegate :category, to: :topic, allow_nil: true, private: true + delegate :require_reply_approval?, to: :category, prefix: true, allow_nil: true, private: true + def self.print_chunk_size 1000 end @@ -146,7 +150,7 @@ class TopicView @draft_sequence = DraftSequence.current(@user, @draft_key) @can_review_topic = @guardian.can_review_topic?(@topic) - @queued_posts_enabled = NewPostManager.queue_enabled? + @queued_posts_enabled = NewPostManager.queue_enabled? || category_require_reply_approval? @personal_message = @topic.private_message? end diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb index 2a209e47fa..d29a9f213b 100644 --- a/spec/components/topic_view_spec.rb +++ b/spec/components/topic_view_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' require 'topic_view' -describe TopicView do +RSpec.describe TopicView do fab!(:user) { Fabricate(:user) } fab!(:moderator) { Fabricate(:moderator) } fab!(:admin) { Fabricate(:admin) } @@ -991,4 +991,38 @@ describe TopicView do expect(topic_view.filtered_post_ids).to eq([post_2.id, post.id]) end end + + describe "#queued_posts_enabled?" do + subject(:topic_view) { described_class.new(topic, user) } + + let(:topic) { Fabricate.build(:topic) } + let(:user) { Fabricate.build(:user, id: 1) } + let(:category) { topic.category } + + before do + NewPostManager.stubs(:queue_enabled?).returns(queue_enabled) + end + + context "when queue is enabled globally" do + let(:queue_enabled) { true } + + it { is_expected.to be_queued_posts_enabled } + end + + context "when queue is not enabled globally" do + let(:queue_enabled) { false } + + context "when category is moderated" do + before do + category.custom_fields[Category::REQUIRE_REPLY_APPROVAL] = true + end + + it { is_expected.to be_queued_posts_enabled } + end + + context "when category is not moderated" do + it { is_expected.not_to be_queued_posts_enabled } + end + end + end end From 3fec579ededd69fb94f5f389e323ab957595f525 Mon Sep 17 00:00:00 2001 From: Discourse Translator Bot Date: Tue, 7 Dec 2021 10:19:44 -0500 Subject: [PATCH 057/119] Update translations (#15210) --- config/locales/client.ar.yml | 6 +- config/locales/client.be.yml | 1 + config/locales/client.bg.yml | 1 + config/locales/client.bs_BA.yml | 5 +- config/locales/client.ca.yml | 42 ++- config/locales/client.cs.yml | 3 +- config/locales/client.da.yml | 13 +- config/locales/client.de.yml | 12 +- config/locales/client.el.yml | 5 +- config/locales/client.es.yml | 6 +- config/locales/client.et.yml | 3 +- config/locales/client.fa_IR.yml | 12 +- config/locales/client.fi.yml | 6 +- config/locales/client.fr.yml | 6 +- config/locales/client.gl.yml | 5 +- config/locales/client.he.yml | 35 +- config/locales/client.hu.yml | 36 +- config/locales/client.hy.yml | 5 +- config/locales/client.it.yml | 18 +- config/locales/client.ja.yml | 5 +- config/locales/client.ko.yml | 14 +- config/locales/client.lt.yml | 148 +++++++- config/locales/client.lv.yml | 3 +- config/locales/client.nb_NO.yml | 5 +- config/locales/client.nl.yml | 5 +- config/locales/client.pl_PL.yml | 8 +- config/locales/client.pt.yml | 5 +- config/locales/client.pt_BR.yml | 6 +- config/locales/client.ro.yml | 3 +- config/locales/client.ru.yml | 18 +- config/locales/client.sk.yml | 3 +- config/locales/client.sl.yml | 5 +- config/locales/client.sq.yml | 3 +- config/locales/client.sr.yml | 1 + config/locales/client.sv.yml | 16 +- config/locales/client.sw.yml | 3 +- config/locales/client.te.yml | 1 + config/locales/client.th.yml | 3 +- config/locales/client.tr_TR.yml | 6 +- config/locales/client.uk.yml | 6 +- config/locales/client.ur.yml | 3 +- config/locales/client.vi.yml | 5 +- config/locales/client.zh_CN.yml | 6 +- config/locales/client.zh_TW.yml | 3 +- config/locales/server.ar.yml | 12 + config/locales/server.be.yml | 12 + config/locales/server.bg.yml | 12 + config/locales/server.bs_BA.yml | 12 + config/locales/server.ca.yml | 12 + config/locales/server.cs.yml | 12 + config/locales/server.da.yml | 12 + config/locales/server.de.yml | 23 ++ config/locales/server.el.yml | 12 + config/locales/server.es.yml | 12 + config/locales/server.et.yml | 12 + config/locales/server.fa_IR.yml | 12 + config/locales/server.fi.yml | 12 + config/locales/server.fr.yml | 12 + config/locales/server.gl.yml | 12 + config/locales/server.he.yml | 69 ++++ config/locales/server.hu.yml | 22 ++ config/locales/server.hy.yml | 12 + config/locales/server.id.yml | 12 + config/locales/server.it.yml | 19 + config/locales/server.ja.yml | 12 + config/locales/server.ko.yml | 13 + config/locales/server.lt.yml | 348 ++++++++++++++++++ config/locales/server.lv.yml | 12 + config/locales/server.nb_NO.yml | 12 + config/locales/server.nl.yml | 12 + config/locales/server.pl_PL.yml | 13 + config/locales/server.pt.yml | 12 + config/locales/server.pt_BR.yml | 12 + config/locales/server.ro.yml | 12 + config/locales/server.ru.yml | 19 + config/locales/server.sk.yml | 12 + config/locales/server.sl.yml | 12 + config/locales/server.sq.yml | 12 + config/locales/server.sr.yml | 12 + config/locales/server.sv.yml | 46 +++ config/locales/server.sw.yml | 12 + config/locales/server.te.yml | 12 + config/locales/server.th.yml | 12 + config/locales/server.tr_TR.yml | 12 + config/locales/server.uk.yml | 12 + config/locales/server.ur.yml | 12 + config/locales/server.vi.yml | 12 + config/locales/server.zh_CN.yml | 12 + config/locales/server.zh_TW.yml | 12 + .../config/locales/client.da.yml | 1 + .../config/locales/server.lt.yml | 114 ++++++ plugins/poll/config/locales/client.lt.yml | 2 + plugins/poll/config/locales/server.lt.yml | 6 + .../styleguide/config/locales/client.lt.yml | 1 + 94 files changed, 1494 insertions(+), 138 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 32039770d0..85e202d801 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -3965,7 +3965,7 @@ ar: tags: "الوسوم" choose_for_topic: "الوسوم الاختيارية" info: "المعلومات" - default_info: "هذا الوسم ليس مقصورًا على أي فئات وليس له أي مرادفات. لإضافة قيود، ضع هذا الوسم في مجموعة وسوم." + default_info: "هذ الوسم ليس مقصورًا على أي تصنيف، وليس له مرادفات." category_restricted: "هذا الوسم مقيَّد بالفئات التي ليس لديك إذن بالوصول إليها." synonyms: "المرادفات" synonyms_description: "عند استخدام الوسوم التالية، سيتم استبدالها بالوسم %{base_tag_name}." @@ -3983,7 +3983,6 @@ ar: few: "لا يمكن استخدامه إلا في هذه الفئات:" many: "لا يمكن استخدامه إلا في هذه الفئات:" other: "لا يمكن استخدامه إلا في هذه الفئات:" - edit_synonyms: "إدارة المرادفات" add_synonyms_label: "إضافة المرادفات:" add_synonyms: "إضافة" add_synonyms_explanation: @@ -4012,8 +4011,7 @@ ar: few: "سيتم حذف مرادفاته (%{count}) أيضًا." many: "سيتم حذف مرادفاته (%{count}) أيضًا." other: "سيتم حذف مرادفاته (%{count}) أيضًا." - rename_tag: "إعادة تسمية الوسم" - rename_instructions: "اختر اسمًا جديدًا للوسم:" + description: "الوصف" sort_by: "الترتيب حسب:" sort_by_count: "العدد" sort_by_name: "الاسم" diff --git a/config/locales/client.be.yml b/config/locales/client.be.yml index c01ec2bb99..3cb2b23a18 100644 --- a/config/locales/client.be.yml +++ b/config/locales/client.be.yml @@ -1320,6 +1320,7 @@ be: tags: "тэгі" choose_for_topic: "дадатковыя тэгі" add_synonyms: "дадаць" + description: "апісанне" sort_by: "Сартаваць па:" sort_by_count: "падлічваць" sort_by_name: "Імя" diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml index f13060932b..c0491e163a 100644 --- a/config/locales/client.bg.yml +++ b/config/locales/client.bg.yml @@ -2970,6 +2970,7 @@ bg: choose_for_topic: "етикети по желание" add_synonyms: "Добави" delete_tag: "Изтрийте таг" + description: "Описание" sort_by: "Сортирай по:" sort_by_count: "брой" sort_by_name: "име" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 06eebd2cca..7b3b186ff9 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -2974,6 +2974,7 @@ bs_BA: tags: "Oznake" choose_for_topic: "neobavezne oznake" info: "Informacija" + default_info: "Ovaj oznaka nije predodređena ni za jednu kategoriju i nema sinonime." category_restricted: "Ova oznaka je predodređena za kategorije na koje nemate pravo pristupa." synonyms: "Sinonimi" synonyms_description: "U slučaju da se sljedeće oznake koriste, iste će biti zamijenjene sa %{base_tag_name}." @@ -2985,7 +2986,6 @@ bs_BA: one: "Moguće je koristiti samo u sljedećoj kategoriji:" few: "Moguće je koristiti samo u sljedećim kategorijama:" other: "Moguće je koristiti samo u sljedećim kategorijama:" - edit_synonyms: "Uredi sinonime" add_synonyms_label: "Dodaj sinonime:" add_synonyms: "Dodaj" add_synonyms_explanation: @@ -3005,8 +3005,7 @@ bs_BA: one: "Pripadajući sinonim će također biti obrisan." few: "Pripadajućih %{count} sinonima će također biti obrisano." other: "Pripadajućih %{count} sinonima će također biti obrisano." - rename_tag: "Promjeni ime Oznake" - rename_instructions: "Odaberite novo ime za oznaku:" + description: "Description" sort_by: "Sortiraj po:" sort_by_count: "brojenje" sort_by_name: "ime" diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index eef0172405..c5d77894a5 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -158,9 +158,11 @@ ca: disabled: "ha suprimit aquest bàner %{when}. No apareixerà més a dalt de cada pàgina." forwarded: "el missatge de dalt s'ha reenviat" topic_admin_menu: "accions del tema" + skip_to_main_content: "Vés al contingut principal" wizard_required: "Us donem la benvinguda al vostre nou Discourse! Comencem amb l'assistent de configuració ✨" emails_are_disabled: "Tots els correus sortints han estat globalment inhabilitats per un administrador. No s'enviarà cap notificació de cap mena per correu electrònic." software_update_prompt: + message: "Hem actualitzat aquest lloc, si us plau actualitzeu, o podeu experimentar un comportament inesperat." dismiss: "Descarta-ho" bootstrap_mode_disabled: "El mode d'arrencada serà desactivat d'aquí a 24 hores." themes: @@ -170,6 +172,7 @@ ca: regions: ap_northeast_1: "Àsia Pacífic (Tòquio)" ap_northeast_2: "Àsia Pacífic (Seül)" + ap_east_1: "Àsia Pacífic (Hong Kong)" ap_south_1: "Àsia Pacífic (Mumbai)" ap_southeast_1: "Àsia Pacífic (Singapur)" ap_southeast_2: "Àsia Pacífic (Sydney)" @@ -178,6 +181,7 @@ ca: cn_northwest_1: "la Xina (Ningxia)" eu_central_1: "UE (Frankfurt)" eu_north_1: "UE (Stockholm)" + eu_south_1: "UE (Milà)" eu_west_1: "UE (Irlanda)" eu_west_2: "UE (Londres)" eu_west_3: "UE (París)" @@ -235,6 +239,8 @@ ca: character_count: one: "%{count} caràcter" other: "%{count} caràcters" + period_chooser: + aria_label: "Filtra per període" related_messages: title: "Missatges relacionats" see_all: 'Mostra tots els missatges de @%{username}...' @@ -267,19 +273,25 @@ ca: help: bookmark: "Feu clic per a marcar com a preferit la primera publicació d'aquest tema" edit_bookmark: "Feu clic per editar el marcador sobre aquest tema" + edit_bookmark_for_topic: "Cliqueu per editar el marcador d'aquest tema" unbookmark: "Feu clic per a eliminar tots els preferits d'aquest tema" unbookmark_with_reminder: "Feu clic per eliminar tots els marcadors i recordatoris d’aquest tema." bookmarks: created: "Heu marcat com a preferida aquesta publicació. %{name}" not_bookmarked: "marca aquesta publicació com a preferit" + remove_reminder_keep_bookmark: "Elimina el recordatori i mantén el marcador" created_with_reminder: "Heu marcat aquesta publicació amb un recordatori de %{date}. %{name}" remove: "Elimina preferit" delete: "Suprimeix el marcador" confirm_delete: "Esteu segur que voleu suprimir aquest marcador? El recordatori també se suprimirà." confirm_clear: "Esteu segur que voleu eliminar tots els preferits d'aquest tema?" save: "Desa" + no_timezone: 'Encara no heu establert una zona horària. No podreu establir recordatoris. Configureu una al vostre perfil.' + invalid_custom_datetime: "La data i l'hora que heu proporcionat no són vàlides, torneu-ho a provar." + list_permission_denied: "No teniu permís per veure els marcadors d'aquest usuari." no_user_bookmarks: "No teniu publicacions en els preferits. Els preferits us permeten referir-vos fàcilment a publicacions específiques. " auto_delete_preference: + label: "Esborra automàticament" never: "Mai" when_reminder_sent: "Una vegada enviat el recordatori" on_owner_reply: "Després de respondre a aquest tema" @@ -294,12 +306,17 @@ ca: copied: "copiat!" drafts: label: "Esborranys" + label_with_count: "Esborranys (%{count})" resume: "Reprèn" remove: "Elimina" + remove_confirmation: "Esteu segur que voleu suprimir aquest esborrany?" new_topic: "Esborrany de tema nou" + new_private_message: "Nou esborrany de missatge personal" topic_reply: "Esborrany de resposta" abandon: + confirm: "Teniu un esborrany en curs per a aquest tema. Què t'agradaria fer-hi?" yes_value: "Descarta" + no_value: "Reprèn l'edició" topic_count_categories: one: "Vegeu %{count} tema nou o actualitzat" other: "Vegeu %{count} temes nous o actualitzats" @@ -317,12 +334,14 @@ ca: other: "Vegeu %{count} temes nous" preview: "previsualitza" cancel: "cancel·la" + deleting: "Suprimint..." save: "Desa els canvis" saving: "Desant..." saved: "Desat!" upload: "Carrega" uploading: "Carregant..." uploading_filename: "Pujant: %{filename}..." + processing_filename: "Processant: %{filename}..." clipboard: "porta-retalls" uploaded: "Carregat!" pasting: "Enganxant..." @@ -351,6 +370,7 @@ ca: placeholder: "escriviu aquí el títol, l'URL o l'identificador del missatge" review: order_by: "Ordena per" + date_filter: "Publicat entre" in_reply_to: "en resposta a" explain: why: "expliqueu per què aquest element ha acabat a la cua" @@ -505,20 +525,29 @@ ca: other: "Teniu %{count} publicacions pendents." ok: "D'acord" example_username: "nom d'usuari" + reject_reason: + title: "Per què rebutgeu aquest usuari?" + send_email: "Envia un correu electrònic de rebuig" relative_time_picker: days: one: "dia" other: "dies" + relative: "Relatiu" time_shortcut: later_today: "Més tard avui" next_business_day: "El pròxim dia feiner" tomorrow: "Demà" + post_local_date: "Data en la publicació" later_this_week: "Més avant aquesta setmana" this_weekend: "Aquest cap de setmana" start_of_next_business_week: "Dilluns" start_of_next_business_week_alt: "Dilluns vinent" + two_weeks: "Dues setmanes" next_month: "El mes que ve" + six_months: "Sis mesos" custom: "Data i hora personalitzades" + relative: "Temps relatiu" + none: "No cal cap" user_action: user_posted_topic: "%{user} ha publicat el tema" you_posted_topic: "Vós heu publicat el tema" @@ -571,9 +600,12 @@ ca: member_added: "Afegit" member_requested: "Sol·licitat" add_members: + title: "Afegeix usuaris a %{group_name}" + description: "Introduïu una llista d'usuaris que voleu convidar al grup o enganxeu-la en una llista separada per comes:" usernames_placeholder: "noms d'usuari" usernames_or_emails_placeholder: "noms d’usuari o correus electrònics" notify_users: "Notifiqueu els usuaris" + set_owner: "Estableix usuaris com a propietaris d'aquest grup" requests: title: "Sol·licituds" reason: "Motiu" @@ -587,6 +619,7 @@ ca: title: "Gestiona" name: "Nom" full_name: "Nom complet" + add_members: "Afegeix usuaris" invite_members: "Convida" delete_member_confirm: "Voleu eliminar '%{username}' del grup '%{group}'?" profile: @@ -597,7 +630,13 @@ ca: notification: Notificació email: title: "Correu electrònic" + status: "S'han sincronitzat %{old_emails} / %{total_emails} correus electrònics via IMAP." + enable_smtp: "Activa SMTP" + enable_imap: "Activa IMAP" + save_settings: "Desa la configuració" + last_updated: "Darrera actualització:" last_updated_by: "per" + settings_required: "Tots els paràmetres són obligatoris, si us plau ompli tots els camps abans de la validació." credentials: title: "Credencials" smtp_server: "Servidor SMTP" @@ -2792,8 +2831,7 @@ ca: one: "Esteu segur que voleu suprimir aquesta etiqueta i eliminar-la del tema %{count} al qual és assignada?" other: "Esteu segur que voleu suprimir aquesta etiqueta i eliminar-la dels %{count} temes als quals és assignada?" delete_confirm_no_topics: "Esteu segur que voleu suprimir aquesta etiqueta?" - rename_tag: "Reanomena l'etiqueta" - rename_instructions: "Trieu un nom nou per a l'etiqueta:" + description: "Descripció" sort_by: "Ordena per:" sort_by_count: "comptabilitza" sort_by_name: "nom" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 6084f2e53e..973b62ad71 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -2771,8 +2771,7 @@ cs: many: "Jsi si jist, že chceš smazat tento štítek a odstranit ho z %{count} témat, kterým je přiřazen?" other: "Jsi si jist, že chceš smazat tento štítek a odstranit ho z %{count} témat, kterým je přiřazen?" delete_confirm_no_topics: "Opravdu chceš smazat tento štítek? " - rename_tag: "Přejmenovat štítek" - rename_instructions: "Vyber název nového štítku:" + description: "Popis" sort_by: "Seřadit dle:" sort_by_count: "počtu" sort_by_name: "jména" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index aec3716e2f..350362363f 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -3343,6 +3343,7 @@ da: tags: "Mærker" choose_for_topic: "valgfrie mærker" info: "Info" + default_info: "Dette mærke er ikke begrænset til nogen kategorier og har ingen synonymer." category_restricted: "Dette mærke er begrænset til kategorier, som du ikke har adgang til." synonyms: "Synonymer" synonyms_description: "Når følgende mærker bruges, vil de blive erstattet med %{base_tag_name}." @@ -3352,7 +3353,6 @@ da: category_restrictions: one: "Den kan kun bruges i denne kategori:" other: "Det kan kun bruges i disse kategorier:" - edit_synonyms: "Administrer Synonymer" add_synonyms_label: "Tilføj synonymer:" add_synonyms: "Tilføj" add_synonyms_explanation: @@ -3369,8 +3369,7 @@ da: delete_confirm_synonyms: one: "Dets synonym slettes også." other: "Dets %{count} synonymer slettes også." - rename_tag: "Omdøb Mærke" - rename_instructions: "Vælg et nyt navn for det mærke:" + description: "Beskrivelse" sort_by: "Sortér efter:" sort_by_count: "antal" sort_by_name: "navn" @@ -3709,6 +3708,14 @@ da: list: Hent en liste over brugere. email: receive_emails: Kombiner dette anvendelsesområde med mail-modtageren til at behandle indgående e-mails. + badges: + create: Opret et nyt emblem. + show: Hent oplysninger om et emblem. + update: Opdatér et emblem + delete: Slet et emblem + list_user_badges: Se liste over bruger emblemer. + assign_badge_to_user: Tildel et emblem til en bruger. + revoke_badge_from_user: Tilbagekald et emblem fra en bruger. web_hooks: title: "Webhooks" none: "Der er ingen webhooks i øjeblikket" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index dee9e45462..92bd6d1196 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -2178,6 +2178,8 @@ de: in_posts_by: "in Beiträgen von %{username}" browser_tip: "%{modifier} + f" browser_tip_description: "nochmal um die native Browsersuche zu verwenden" + recent: "Letzte Suchanfragen" + clear_recent: "Letzte Suchanfragen löschen" type: default: "Themen/Beiträge" users: "Benutzer" @@ -3488,17 +3490,19 @@ de: one: "mindestens %{count} Schlagwort auswählen..." other: "mindestens %{count} Schlagwörter auswählen..." info: "Info" - default_info: "Dieses Schlagwort ist nicht auf bestimmte Kategorien beschränkt und hat keine Synonyme. Um Einschränkungen hinzuzufügen, füge dieses Schlagwort einer Schlagwortgruppe hinzu." + default_info: "Dieses Schlagwort ist nicht auf Kategorien beschränkt und hat keine Synonyme." + staff_info: "Um Einschränkungen hinzuzufügen, füge dieses Schlagwort in eine Tag-Gruppeein." category_restricted: "Dieses Schlagwort ist auf Kategorien beschränkt, für die du keine Zugriffsberechtigung hast." synonyms: "Synonyme" synonyms_description: "Wenn die folgenden Schlagwörter verwendet werden, werden sie durch %{base_tag_name} ersetzt." + save: "Name und Beschreibung des Schlagwortes speichern" tag_groups_info: one: 'Dieses Schlagwort gehört zur Gruppe „%{tag_groups}“.' other: "Dieses Schlagwort gehört zu diesen Gruppen: %{tag_groups}" category_restrictions: one: "Es kann nur in dieser Kategorie verwendet werden:" other: "Es kann nur in folgenden Kategorien verwendet werden:" - edit_synonyms: "Synonyme Verwalten" + edit_synonyms: "Synonyme bearbeiten" add_synonyms_label: "Synonyme hinzufügen:" add_synonyms: "Hinzufügen" add_synonyms_explanation: @@ -3515,8 +3519,8 @@ de: delete_confirm_synonyms: one: "Das Synonym wird ebenfalls gelöscht." other: "Es werden %{count} weitere Synonyme ebenfalls gelöscht." - rename_tag: "Schlagwort umbenennen" - rename_instructions: "Neuen Namen für das Schlagwort wählen:" + edit_tag: "Schlagwortname und Beschreibung bearbeiten" + description: "Beschreibung" sort_by: "Sortieren nach:" sort_by_count: "Anzahl" sort_by_name: "Name" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index f171d82100..8ecdefb257 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -2976,6 +2976,7 @@ el: tags: "Ετικέτες" choose_for_topic: "προαιρετικές ετικέτες" info: "Πληροφορίες" + default_info: "Αυτή η ετικέτα δεν περιορίζεται σε καμία κατηγορία και δεν έχει συνώνυμα." category_restricted: "Αυτή η ετικέτα περιορίζεται σε κατηγορίες στις οποίες δεν έχετε άδεια πρόσβασης." synonyms: "Συνώνυμα" synonyms_description: "Όταν χρησιμοποιούνται οι ακόλουθες ετικέτες, θα αντικατασταθούν με %{base_tag_name}." @@ -2985,7 +2986,6 @@ el: category_restrictions: one: "Μπορεί να χρησιμοποιηθεί μόνο σε αυτήν την κατηγορία:" other: "Μπορεί να χρησιμοποιηθεί μόνο σε αυτές τις κατηγορίες:" - edit_synonyms: "Διαχείριση συνωνύμων" add_synonyms_label: "Προσθήκη συνωνύμων:" add_synonyms: "Προσθήκη" add_synonyms_explanation: @@ -3002,8 +3002,7 @@ el: delete_confirm_synonyms: one: "Το συνώνυμό του θα διαγραφεί επίσης." other: "Τα %{count} συνώνυμά του θα διαγραφούν επίσης." - rename_tag: "Μετονομασία Ετικέτας" - rename_instructions: "Επίλεξε ένα καινούριο όνομα για την ετικέτα:" + description: "Περιγραφή" sort_by: "Ταξινόμηση κατά:" sort_by_count: "άθροισμα" sort_by_name: "όνομα" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 609e7bc559..3e58912187 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -3482,7 +3482,7 @@ es: one: "selecciona al menos %{count} etiqueta…" other: "selecciona al menos %{count} etiquetas…" info: "Info" - default_info: "Esta etiqueta no está restringida a ninguna categoría y no tiene sinónimos. Para añadir restricciones, añádela a un grupo de etiquetas." + default_info: "Esta etiqueta no está restringida a ninguna categoría, y no tiene sinónimos." category_restricted: "Esta etiqueta está restringida para las categorías a las que no tienes permiso de acceso." synonyms: "Sinónimos" synonyms_description: "Cuando las siguientes etiquetas sean usadas, serán reemplazadas por %{base_tag_name}." @@ -3492,7 +3492,6 @@ es: category_restrictions: one: "Solo se puede utilizar en esta categoría:" other: "Solo se puede utilizar en estas categorías:" - edit_synonyms: "Gestionar sinónimos" add_synonyms_label: "Añadir sinónimos:" add_synonyms: "Añadir" add_synonyms_explanation: @@ -3509,8 +3508,7 @@ es: delete_confirm_synonyms: one: "Su sinónimo también se eliminará" other: "Sus %{count} sinónimos también se eliminarán." - rename_tag: "Cambiar nombre de etiqueta" - rename_instructions: "Elige un nuevo nombre para la etiqueta:" + description: "Descripción" sort_by: "Ordenar por:" sort_by_count: "cantidad" sort_by_name: "nombre" diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index f27b2e7480..74525881c6 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -2489,8 +2489,7 @@ et: choose_for_topic: "valikulised sildid" add_synonyms: "Lisa" delete_tag: "Kustuta silt" - rename_tag: "Nimeta silt ümber" - rename_instructions: "Vali sildile uus nimi:" + description: "Kirjeldus" sort_by: "Järjesta:" sort_by_count: "üldarv" sort_by_name: "nimi" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 9e0df0212c..84904a8575 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -2962,11 +2962,12 @@ fa_IR: one: "حداقل برچسب %{count} را انتخاب کنید..." other: "حداقل برچسب %{count} را انتخاب کنید..." info: "اطلاعات" + save: "ذخیره نام و توضیحات برچسب" add_synonyms: "افزودن" delete_tag: "حذف برچسب" delete_confirm_no_topics: "ایا از حذف این برچسب مطمعن هستید؟" - rename_tag: "تغییر نام برچسب" - rename_instructions: "انتخاب نام جدید برای برچسب:" + edit_tag: "ویرایش نام و توضیحات برچسب" + description: "توضیحات" sort_by: "مرتب سازی بر اساس:" sort_by_count: "تعداد" sort_by_name: "نام" @@ -3215,6 +3216,13 @@ fa_IR: descriptions: uploads: create: یک پرونده جدید بارگذاری کنید. + badges: + create: ایجاد نشان جدید + update: به‌روزرسانی نشان + delete: حذف نشان + list_user_badges: فهرست نشان‌های کاربر. + assign_badge_to_user: یک نشان را به یک کاربر اختصاص دهید. + revoke_badge_from_user: یک نشان را از کاربر بگیرید. web_hooks: title: "webhook ها" none: "در حال حاضر webhook‌ی وجود ندارد" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index fa4928b9af..69d1d0f24b 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -3364,7 +3364,7 @@ fi: tags: "Tunnisteet" choose_for_topic: "valinnaiset tunnisteet" info: "Tietoa" - default_info: "Tätä tunnistetta ei ole rajoitettu mihinkään alueeseen, eikä sillä ole synonyymejä. Lisää rajoituksia laittamalla tämä tunniste tunnisteryhmään." + default_info: "Tätä tunnistetta ei ole rajattu millekään alueelle eikä sillä ole synonyymejä." category_restricted: "Tämä tunniste on rajoitettu alueille, joille sinulla ei ole pääsyoikeutta." synonyms: "Synonyymit" synonyms_description: "Kun näitä tunnisteita käytetään, ne korvataan tunnisteella %{base_tag_name}." @@ -3374,7 +3374,6 @@ fi: category_restrictions: one: "Sitä voi käyttää vain tällä alueella:" other: "Sitä voi käyttää vain näillä alueilla:" - edit_synonyms: "Hallitse synonyymejä" add_synonyms_label: "Lisää synonyymejä:" add_synonyms: "Lisää" add_synonyms_explanation: @@ -3391,8 +3390,7 @@ fi: delete_confirm_synonyms: one: "Sen synonyymi poistetaan myös." other: "Sen %{count} synonyymiä poistetaan myös." - rename_tag: "Nimeä tunniste uudelleen" - rename_instructions: "Valitse uusi nimi tälle tunnisteelle:" + description: "Kuvaus" sort_by: "Järjestys:" sort_by_count: "lukumäärä" sort_by_name: "nimi" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 5da39f0b29..9b57b3db22 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -3361,7 +3361,7 @@ fr: tags: "Étiquettes" choose_for_topic: "étiquettes optionnelles" info: "Détails" - default_info: "Cette étiquette n'est restreinte à aucune catégorie et n'a aucun synonyme défini. Pour restreindre l'utilisation de cette étiquette, ajoutez-là à un groupe d'étiquettes." + default_info: "Cette étiquette n'est pas limitée à une catégorie et n'a aucun synonyme." category_restricted: "Cette étiquette est limitée à des catégories auxquelles vous n'avez pas la permission d'accéder." synonyms: "Synonymes" synonyms_description: "Quand les étiquettes suivantes sont utilisées, elles seront remplacées par %{base_tag_name}." @@ -3371,7 +3371,6 @@ fr: category_restrictions: one: "Elle ne peut être utilisée que dans cette catégorie :" other: "Elle ne peut être utilisée que dans ces catégories :" - edit_synonyms: "Gérer les synonymes" add_synonyms_label: "Ajoutez des synonymes :" add_synonyms: "Ajouter" add_synonyms_explanation: @@ -3388,8 +3387,7 @@ fr: delete_confirm_synonyms: one: "Son synonyme sera aussi supprimé." other: "Ses %{count} synonymes seront aussi supprimés." - rename_tag: "Renommer l'étiquette" - rename_instructions: "Choisissez un nouveau nom pour l'étiquette :" + description: "Description" sort_by: "Trier par :" sort_by_count: "nombre" sort_by_name: "nom" diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 196a87de83..98b429ee03 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -3261,6 +3261,7 @@ gl: tags: "Etiquetas" choose_for_topic: "etiquetas opcionais" info: "Información" + default_info: "Esta etiqueta non se restrinxe a ningunha categoría e non ten sinónimos." category_restricted: "Esta etiqueta restrínxese ás categorías ás que non ten permiso para acceder." synonyms: "Sinónimos" synonyms_description: "Cando se usen as seguintes etiquetas, substituílas por %{base_tag_name}." @@ -3270,7 +3271,6 @@ gl: category_restrictions: one: "Só pode usarse nesta categoría:" other: "Só pode usarse nestas categorías:" - edit_synonyms: "Xestionar sinónimos" add_synonyms_label: "Engadir sinónimos:" add_synonyms: "Engadir" add_synonyms_explanation: @@ -3287,8 +3287,7 @@ gl: delete_confirm_synonyms: one: "Tamén se eliminará o seu sinónimo." other: "Tamén se eliminarán os seus %{count} sinónimos." - rename_tag: "Renomear etiqueta" - rename_instructions: "Seleccione un novo nome para a etiqueta:" + description: "Descrición" sort_by: "Ordenar por:" sort_by_count: "número" sort_by_name: "nome" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index bf3f82054b..b0c140ef72 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -1117,7 +1117,11 @@ he: ניתן להתחיל להוסיף פוסטים לסימניות עם הכפתור %{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: @@ -1605,6 +1609,12 @@ he: bulk_invite: none: "אין הזמנות להצגה בעמוד הזה." text: "הזמנה כמותית" + instructions: | +

    אפשר להזמין רשימה של משתמשים כדי להניע את הקהילה שלך במהירות. יש להכין קובץ CSV שמכיל לפחות שורה אחת לכל כתובת דוא״ל של משתמשים שברצונך להזמין. את המידע הבאה, בהפרדה עם פסיקים, ניתן לספק כדי להוסיף אנשים לקבוצות או לשלוח אותם לנושא מסוים עם כניסתם הראשונה.

    +
    john@smith.com,first_group_name;second_group_name,topic_id
    + או +
    itzik@bitton.co.il,שם_קבוצה_ראשונה;שם_קבוצה_שנייה,מזהה_נושא
    +

    לכל כתובת דוא״ל בקובץ ה־CSV תישלח הזמנה ותהיה לך אפשרות לנהל אותה בהמשך.

    progress: "מתבצעת העלאה %{progress}%…" success: "הקובץ נשלח בהצלחה. תישלח אליך הודעה כשהתהליך יושלם." error: "הקובץ אמור להיות בתצורת CSV, עמך הסליחה." @@ -2251,6 +2261,11 @@ he: reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - הסתיים" dismiss_confirmation: + body: + one: "בוודאות? יש לך התראה %{count} חשובה." + two: "בוודאות? יש לך %{count} התראות חשובות." + many: "בוודאות? יש לך %{count} התראות חשובות." + other: "בוודאות? יש לך %{count} התראות חשובות." dismiss: "דחה" cancel: "ביטול" group_message_summary: @@ -3760,10 +3775,12 @@ he: many: "נא לבחור ב־%{count} תגיות לפחות…" other: "נא לבחור ב־%{count} תגיות לפחות…" info: "פרטים" - default_info: "תגית זו אינה מוגבלת לאף קטגוריה ואין לה תגיות נרדפות. כדי להוסיף הגבלות, יש להציב את התגית הזאת בקבוצת תגיות." + default_info: "תגית זו אינה מוגבלת לקטגוריות כלשהן ואין לה מילים נרדפות." + staff_info: "כדי להוסיף הגבלות, יש לשים את התגית הזאת בקבוצת תגיות." category_restricted: "תגית זו מוגבלת לקטגוריות שאין לך גישה אליהן." synonyms: "מילים נרדפות" synonyms_description: "תגיות אלו תוחלפנה בתגית %{base_tag_name}." + save: "שמירת השם ותיאור התגית" tag_groups_info: one: 'תגית זו שייכת לקבוצה הזאת: %{tag_groups}' two: "תגית זו שייכת לקבוצות האלו: %{tag_groups}" @@ -3774,7 +3791,7 @@ he: two: "ניתן להשתמש בה בקטגוריות אלו בלבד:" many: "ניתן להשתמש בה בקטגוריות אלו בלבד:" other: "ניתן להשתמש בה בקטגוריות אלו בלבד:" - edit_synonyms: "ניהול מילים נרדפות" + edit_synonyms: "עריכת מילים נרדפות" add_synonyms_label: "הוספת מילים נרדפות:" add_synonyms: "הוספה" add_synonyms_explanation: @@ -3797,8 +3814,8 @@ he: two: "%{count} המילים הנרדפות שקשורות אליה תימחקנה גם כן." many: "%{count} המילים הנרדפות שקשורות אליה תימחקנה גם כן." other: "%{count} המילים הנרדפות שקשורות אליה תימחקנה גם כן." - rename_tag: "שינוי שם לתגית" - rename_instructions: "בחרו שם חדש לתגית:" + edit_tag: "עריכת שם התגית ותיאורה" + description: "תיאור" sort_by: "סידור לפי:" sort_by_count: "ספירה" sort_by_name: "שם" @@ -3924,9 +3941,11 @@ he: no_drafts_title: "לא התחלת טיוטות" no_drafts_body: "לא סיימת לכתוב את הפוסט? אנו נשמור טיוטה חדשה אוטומטית ונציג אותה כאן עם כל כתיבה של נושא, תגובה או הודעה פרטית חדשים. יש לבחור בכפתור הביטול כדי להתעלם או לשמור את הטיוטה שלך ולהמשיך אחר כך." no_likes_title: "עדיין לא סימנת אף נושא בלייק" + no_likes_body: "דרך נפלאה לקפוץ ישירות פנימה ולהתחיל לתרום היא להתחיל לקרוא דיונים שכבר נערכו ולבחור בסמל %{heartIcon} על פוסטים שאהבת!" no_likes_others: "אין פוסטים שנעשה להם לייק." no_topics_title: "עדיין לא פתחת אף נושא" no_read_topics_title: "טרם קראת נושאים" + no_read_topics_body: "עם תחילת קריאת הדיונים, תופיע כאן רשימה. כדי להתחיל לקרוא, כדאי לחפש נושאים שמעניינים אותך תחת עליונים או קטגוריות או לחפש אחר מילת מפתח %{searchIcon}" no_group_messages_title: "לא נמצאו הודעות קבוצתיות" fullscreen_table: expand_btn: "הרחבת טבלה" @@ -4176,6 +4195,14 @@ he: list: קבלת רשימת משתמשים. email: receive_emails: לשלב את התחום הזה עם מקבל הודעות הדוא״ל כדי לעבד הודעות דוא״ל נכנסות. + badges: + create: יצירת עיטור חדש. + show: קבלת מידע על עיטור. + update: עדכון עיטור. + delete: מחיקת עיטור. + list_user_badges: הצגת עיטורי המשתמש. + assign_badge_to_user: הקצאת עיטור למשתמש. + revoke_badge_from_user: שלילת עיטור ממשתמש. web_hooks: title: "Webhooks" none: "אין כרגע webhooks." diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index f21b205b86..7e16838dc6 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -386,7 +386,7 @@ hu: score_to_hide: "Pontszám a bejegyzés elrejtéséhez" take_action_bonus: name: "intézkedett" - title: "Ha egy stábtag úgy dönt, hogy intézkedik, akkor a a jelzés bónuszt kap." + title: "Ha egy stábtag úgy dönt, hogy intézkedik, akkor a jelzés bónuszt kap." user_accuracy_bonus: name: "felhasználói pontosság" title: "Azok a felhasználók, akik előzőleg egyetértettek a jelzéssel, bónuszt kapnak." @@ -1639,14 +1639,14 @@ hu: hide_session: "Emlékeztessen holnap" hide_forever: "nem, köszönöm" hidden_for_session: "Rendben, holnap megkérdezzük. Bármikor használhatja a „Bejelentkezés” gombot, hogy fiókot készítsen magának." - intro: "Szia! Úgy tűnik, tetszik neked a fórumunk, de még nem regisztráltál fiókot." + intro: "Üdv! Úgy tűnik, hogy tetszik Önnek a fórumunk, de még nem regisztrált fiókot." value_prop: "Onnantól, hogy létrehozol egy fiókot, a rendszer emlékezni fog arra, hogy mit olvastál, így mindig oda térhetsz vissza, ahol korábban abbahagytad. Értesítéseket is kapsz, itt is és e-mailben is, valahányszor valaki válaszol neked. A bejegyzéseket pedig kedvelheted is. :heartpulse:" summary: - enabled_description: "A téma összefoglalását látod: a legérdekesebb bejegyzéseket a közösség határozta meg." + enabled_description: "A téma összefoglalását látja: a legérdekesebb bejegyzéseket a közösség határozta meg." description: one: "%{count} válasz van." other: "%{count} válasz van." - description_time_MF: "{replyCount, plural, one {is # válasz} other {are # válasz}} található, a becsült olvasási idő {readingTime, plural, one {# perc} other {# perc}}." + description_time_MF: "{replyCount, plural, one {# válasz} other {# válasz}} található, a becsült olvasási idő {readingTime, plural, one {# perc} other {# perc}}." enable: "Téma összefoglalása" disable: "Összes bejegyzés megjelenítése" short_label: "Összefoglalás" @@ -1659,7 +1659,7 @@ hu: private_message_info: title: "Üzenet" invite: "Mások meghívása…" - edit: "Hozzáadás és eltávolítás" + edit: "Hozzáadás vagy eltávolítás…" remove: "Eltávolítás…" add: "Hozzáadás…" leave_message: "Biztos, hogy elhagyja a beszélgetést?" @@ -1677,16 +1677,16 @@ hu: subheader_title: "Hozzuk létre a fiókját" disclaimer: "Csak akkor regisztráljon, ha elfogadja az adatvédelmi szabályzatot és a szolgáltatási feltételeket." title: "Regisztráció" - failed: "Valami hiba történt, talán ez az e-mail cím már regisztrálva van. Próbálta már a jelszó-emlékeztetőt?" + failed: "Valami hiba történt, talán ez az e-mail-cím már regisztrálva van. Próbálta már a jelszó-emlékeztetőt?" associate: "Már van fiókja? Jelentkezzen be hogy hozzákapcsolja a(z) %{provider}-fiókját." forgot_password: title: "Jelszó-visszaállítás" action: "Elfelejtettem a jelszavamat" - invite: "Adja meg a felhasználónevét vagy az e-mail címét és küldünk egy jelszó-visszaállító e-mailt." + invite: "Adja meg a felhasználónevét vagy az e-mail-címét, és küldünk egy jelszó-visszaállító levelet." reset: "Jelszó visszaállítása" - complete_username: "Amennyiben létezik fiók %{username} felhasználónévvel, hamarosan kapni fog egy levelet, amiben megtalálhatja a jelszó visszaállításához szükséges további lépéseket." - complete_email: "Amennyiben létezik fiók %{email} e-mail-címmel, hamarosan kapni fog egy levelet, amiben megtalálhatja a jelszava visszaállításához szükséges további lépéseket." - complete_username_found: "Találtunk egy fiókot, amelynek a felhasználóneve megegyezik ezzel: %{username}. Perceken belül egy e-mailt kell kapnod arról, hogy hogyan állíthatod vissza a jelszavadat." + complete_username: "Amennyiben létezik fiók %{username} felhasználónévvel, hamarosan kapni fog egy levelet, amelyben megtalálhatja a jelszó visszaállításához szükséges további lépéseket." + complete_email: "Amennyiben létezik fiók %{email} e-mail-címmel, hamarosan kapni fog egy levelet, amelyben megtalálhatja a jelszava visszaállításához szükséges további lépéseket." + complete_username_found: "Találtunk egy fiókot, amelynek a felhasználóneve megegyezik ezzel: %{username}. Perceken belül egy levelet kell kapnia arról, hogyan állíthatja vissza a jelszavát." complete_email_found: "Találtunk egy fiókot, amelynek a beállított e-mail címe megegyezik ezzel: %{email}. Perceken belül egy e-mailt kell kapnod arról, hogy hogyan állíthatod vissza a jelszavadat." complete_username_not_found: "Nincs fiók regisztrálva a következő felhasználónévvel: %{username}" complete_email_not_found: "Nincs fiók regisztrálva a következő e-mail-címmel: %{email}" @@ -3118,11 +3118,15 @@ hu: selector_no_tags: "címke nélküli" tags: "Címkék" choose_for_topic: "Megadható címke" + default_info: "Ez a címke nincs kategóriára korlátozva, és nincsenek szinonimái." + staff_info: "Korlátozások hozzáadásához tegye ezt a címkét egy címkecsoportba." + save: "Címke nevének és leírásának mentése" + edit_synonyms: "Szinonimák szerkesztése" add_synonyms: "Hozzáadás" delete_tag: "Címke törlése" delete_confirm_no_topics: "Biztos vagy benne hogy elakarod távolítani ezt a címkét?" - rename_tag: "Címke átnevezése" - rename_instructions: "Válassz egy úgy nevet a címkének:" + edit_tag: "Címke nevének és leírásának szerkesztése" + description: "Leírás" sort_by: "Rendezés" sort_by_count: "besorol" sort_by_name: "név" @@ -3341,6 +3345,14 @@ hu: create: Töltsön fel egy új fájlt. users: update: Felhasználói profil adatainak frissítése. + badges: + create: Új jelvény létrehozása. + show: Információ szerzése egy jelvényről. + update: Jelvény frissítése. + delete: Jelvény törlése. + list_user_badges: A felhasználó jelvényeinek felsorolása. + assign_badge_to_user: Jelvény kiosztása egy felhasználónak. + revoke_badge_from_user: Jelvény elvétele egy felhasználótól. web_hooks: title: "Webhooks" create: "Létrehoz" diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml index 222b161d9c..c8288b009e 100644 --- a/config/locales/client.hy.yml +++ b/config/locales/client.hy.yml @@ -2798,13 +2798,13 @@ hy: tags: "Թեգեր" choose_for_topic: "ընտրովի թեգեր" info: "Ինֆորմացիա" + default_info: "Այս տեգը չի սահմանափակվում որևէ կատեգորիայով և չունի հոմանիշ:" category_restricted: "Այս տեգը սահմանափակված է այն կատեգորիաներով, որոնցում մուտքի թույլտվություն չունեք:" synonyms: "Հոմանիշներ" synonyms_description: "Հետևյալ տեգերի օգտագործման դեպքում, դրանք կփոխարինվեն %{base_tag_name} -ով:" tag_groups_info: one: 'Այս պիտակը պատկանում է «%{tag_groups}» խմբին:' other: "Այս թեգը պատկանում է այս խմբերին՝ %{tag_groups}" - edit_synonyms: "Կարգավորել Հոմանիշները" add_synonyms_label: "Ավելացնել հոմանիշներ." add_synonyms: "Ավելացնել" add_synonyms_failed: "Այս տեգերը չեն կարող ավելացվել որպես հոմանիշներ՝ %{tag_names} : Համոզվեք, որ դրանք հոմանիշներ չունեն և մեկ այլ տեգի հոմանիշներ չեն:" @@ -2815,8 +2815,7 @@ hy: one: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել այս թեգը և հեռացնել այն %{count} թեմայից, որին այն վերագրված է:" other: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել այս թեգը և հեռացնել այն %{count} թեմայից, որոնց այն վերագրված է:" delete_confirm_no_topics: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել այս թեգը:" - rename_tag: "Վերանվանել Թեգը" - rename_instructions: "Ընտրեք նոր անուն թեգի համար՝ " + description: "Նկարագրույթուն" sort_by: "Դասավորել ըստ՝ " sort_by_count: "քանակի" sort_by_name: "անվան" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 37260bfc03..f668532eb6 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -3437,17 +3437,19 @@ it: tags: "Etichette" choose_for_topic: "etichette facoltative" info: "Info" - default_info: "Questa etichetta non è limitata ad alcuna categoria e non ha sinonimi. Per aggiungere restrizioni, inseriscila in un gruppo di etichette." + default_info: "Questa etichetta non è limitata a nessuna categoria e non ha sinonimi." + staff_info: "Per aggiungere restrizioni, metti questa etichetta in un gruppo di etichette" category_restricted: "Questa etichetta è limitata a categorie a cui non sei autorizzato ad accedere." synonyms: "Sinonimi" synonyms_description: "Quando vengono utilizzate le seguenti etichette, verranno sostituite con %{base_tag_name}." + save: "Salva il nome e la descrizione dell’etichetta" tag_groups_info: one: 'Questa etichetta appartiene al gruppo "%{tag_groups}".' other: "Questa etichetta appartiene a questi gruppi: %{tag_groups}." category_restrictions: one: "Può essere utilizzato solo in questa categoria:" other: "Può essere utilizzato solo in queste categorie:" - edit_synonyms: "Gestisci sinonimi" + edit_synonyms: "Modifica Sinonimi" add_synonyms_label: "Aggiungi sinonimi:" add_synonyms: "Aggiungi" add_synonyms_explanation: @@ -3464,8 +3466,8 @@ it: delete_confirm_synonyms: one: "Anche il suo sinonimo verrà eliminato." other: "I suoi %{count} sinonimi verranno a loro volta eliminati." - rename_tag: "Rinomina Etichetta" - rename_instructions: "Scegli un altro nome per l'etichetta:" + edit_tag: "Modifica il nome e la descrizione dell’etichetta" + description: "Descrizione" sort_by: "Ordina per:" sort_by_count: "conteggio" sort_by_name: "nome" @@ -3826,6 +3828,14 @@ it: list: Ottieni un elenco di utenti. email: receive_emails: Combina questo ambito con il destinatario della posta per elaborare le email in arrivo. + badges: + create: Crea un nuovo distintivo. + show: Informazioni su un distintivo. + update: Aggiorna un distintivo. + delete: Cancella un distintivo. + list_user_badges: Elenca i distintivi dell'utente. + assign_badge_to_user: Assegna un distintivo ad un utente. + revoke_badge_from_user: Revoca un distintivo ad un utente. web_hooks: title: "Webhook" none: "Non ci sono webhook disponibili adesso." diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index ef4ee0b3f6..69512470a0 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -3222,7 +3222,6 @@ ja: tags: "タグ" choose_for_topic: "オプションのタグ" info: "情報" - default_info: "このタグはどのカテゴリにも制限されておらず、同義語もありません。制限を追加するには、このタグをタググループに追加してください。" category_restricted: "このタグは、アクセス権限のないカテゴリに制限されています。" synonyms: "同義語" synonyms_description: "次のタグが使用されている場合、%{base_tag_name} に置き換えられます。" @@ -3230,7 +3229,6 @@ ja: other: "このタグは次のグループに属しています: %{tag_groups}。" category_restrictions: other: "次のカテゴリでのみ使用できます:" - edit_synonyms: "同義語を管理" add_synonyms_label: "同義語の追加:" add_synonyms: "追加" add_synonyms_explanation: @@ -3244,8 +3242,7 @@ ja: delete_confirm_no_topics: "このタグを削除してもよろしいですか?" delete_confirm_synonyms: other: "その %{count} 個の同義語も削除されます。" - rename_tag: "タグの名前を変更" - rename_instructions: "タグの新しい名前を選択:" + description: "説明" sort_by: "並べ替え:" sort_by_count: "件数" sort_by_name: "名前" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 275676f81f..220e88caf3 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -1988,6 +1988,8 @@ ko: reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - 완료됨" dismiss_confirmation: + body: + other: "확실한가요? %{count}개의 중요한 알림이 있습니다." dismiss: "읽음" cancel: "취소" group_message_summary: @@ -2070,6 +2072,8 @@ ko: in_topics_posts: "모든 글과 댓글에서" enter_hint: "또는 Enter 키를 누르십시오." in_posts_by: "%{username}님의 글에서" + recent: "최근 검색" + clear_recent: "최근 검색 지우기" type: default: "글/댓글" users: "사용자" @@ -3301,15 +3305,15 @@ ko: choose_for_topic_required: other: "최소한 %{count}개의 태그를 선택해야합니다..." info: "정보" - default_info: "이 태그는 카테고리로 제한되지 않으며 동의어가 없습니다. 제한을 추가하려면 이 태그를 태그 그룹에 넣으십시오." + default_info: "이 태그는 카테고리로 제한되지 않으며 동의어가 없습니다." category_restricted: "이 태그는 액세스 권한이없는 카테고리로 제한됩니다." synonyms: "동의어" synonyms_description: "다음 태그를 사용하면 %{base_tag_name} 으로 대체됩니다." + save: "태그 이름 및 설명 저장" tag_groups_info: other: "이 태그는 다음 그룹에 속합니다: %{tag_groups}." category_restrictions: other: "다음 카테고리에서만 사용할 수 있습니다:" - edit_synonyms: "동의어 관리" add_synonyms_label: "동의어 추가 :" add_synonyms: "추가" add_synonyms_explanation: @@ -3323,8 +3327,8 @@ ko: delete_confirm_no_topics: "정말로 이 태그를 삭제할까요?" delete_confirm_synonyms: other: "%{count}개의 동의어도 삭제됩니다." - rename_tag: "태그명 변경" - rename_instructions: "새로운 태그명을 입력하세요:" + edit_tag: "태그 이름 및 설명 수정" + description: "설명" sort_by: "정렬 기준:" sort_by_count: "개수" sort_by_name: "이름" @@ -3668,6 +3672,8 @@ ko: wordpress: 워드프레스 wp-discourse 플러그인이 작동하는데 필요합니다. posts: edit: 게시물 또는 특정 게시물을 편집합니다. + uploads: + create: 새 파일을 업로드합니다. users: bookmarks: 사용자 북마크를 나열합니다. ICS 형식을 사용하면 북마크 알림을 반환합니다. sync_sso: DiscourseConnect를 사용하여 사용자를 동기화합니다. diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index c275698cc6..9e99143360 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -414,6 +414,7 @@ lt: date_filter: "Paskelbta tarp" in_reply_to: "atsakant į" explain: + why: "paaiškinkite, kodėl šis elementas atsidūrė eilėje" title: "Peržiūrimas balas" formula: "Formulė" subtotal: "Tarpinė suma" @@ -429,6 +430,11 @@ lt: title: "Peržiūrimi elementai, sukurti aukštesnio pasitikėjimo lygio naudotojų, turi didesnį balą." type_bonus: title: "Tam tikriems peržiūrėtiems tipams darbuotojai gali priskirti premiją, kad jie taptų aukštesniu prioritetu." + stale_help: "Ši peržiūra buvo išspręsta %{username}." + claim_help: + optional: "Galite pareikšti teises į šį elementą, kad kiti negalėtų jo peržiūrėti." + required: "Turite pareikšti teises į elementus, kad galėtumėte juos peržiūrėti." + claimed_by_you: "Pareiškėte teises į šį elementą ir galite jį peržiūrėti." unclaim: help: "pašalinti šį teiginį" awaiting_approval: "Laukiama patvirtinimo" @@ -446,12 +452,14 @@ lt: view_pending: "laukiama peržiūros" title: "Peržiūra" topic: "Tema:" + filtered_topic: "Išfiltravote turinį, kurį galima peržiūrėti vienoje temoje." filtered_user: "Narys" filtered_reviewed_by: "Peržiūrėjo" show_all_topics: "rodyti visas temas" deleted_post: "(įrašas ištrintas)" deleted_user: "(naudotojas ištrintas)" user: + bio: "Biografija" website: "Interneto svetainė" username: "Slapyvardis" email: "Epaštas" @@ -566,6 +574,7 @@ lt: custom: "Pasirinkite datą ir laiką" relative: "Santykinis laikas" none: "Nė vienas reikalingas" + last_custom: "Paskutinė tinkinta data ir laikas" user_action: user_posted_topic: "%{user} paskelbė temą" you_posted_topic: "Tu paskelbei temą" @@ -619,6 +628,7 @@ lt: remove_user_as_group_owner: "Atšaukti valdytoją" groups: member_added: "Pridėta" + member_requested: "Paprašyta" add_members: title: "Pridėti vartotojus į %{group_name}" description: "Įveskite naudotojų, kuriuos norite pakviesti į grupę, sąrašą arba įklijuokite juos kableliais atskirtame sąraše:" @@ -665,7 +675,9 @@ lt: imap_settings_valid: "IMAP nustatymai galioja." smtp_disable_confirm: "Jei išjungsite SMTP, visi SMTP ir IMAP nustatymai bus iš naujo nustatyti, o susijusios funkcijos bus išjungtos. Ar tikrai norite tęsti?" imap_disable_confirm: "Jei išjungsite IMAP, visi IMAP parametrai bus iš naujo nustatyti ir susijusios funkcijos bus išjungtos. Ar tikrai norite tęsti?" + imap_mailbox_not_selected: "Šiai IMAP konfigūracijai turite pasirinkti pašto dėžutę arba pašto dėžutės nebus sinchronizuojamos!" prefill: + title: "Užpildykite nustatymus:" gmail: "Gmail" credentials: title: "Įgaliojimai" @@ -782,7 +794,9 @@ lt: remove_owner: "Pašalinti iš savininkų" remove_owner_description: "Atšaukti %{username}, iš grupės valdytojo posto" make_primary: "Padaryti pagrindiniu" + make_primary_description: "Padarykite tai pagrindine grupe %{username}" remove_primary: "Pašalinti kaip pagrindinį" + remove_primary_description: "Pašalinkite tai kaip pagrindinę grupę %{username}" remove_members: "Pašalinti narius" remove_members_description: "Pašalinti pasirinktus vartotojus iš šios grupės" make_owners: "Paversti savininku" @@ -801,6 +815,7 @@ lt: posts: "įrašai" mentions: "Paminėjimai" messages: "Žinutės" + notification_level: "Numatytasis pranešimo lygis grupės pranešimams" alias_levels: mentionable: "Kas gali @minėti šią grupę?" messageable: "Kas gali pranešti apie šią grupę?" @@ -825,6 +840,7 @@ lt: description: "Būsi perspėtas kai kažkas paminės tavo @vardą ar tau atsakys." muted: title: "Nutildytos" + description: "Jums nebus pranešta apie pranešimus šioje grupėje." flair_url: "Avatar Flair paveiksliukas" flair_upload_description: "Naudokite kvadratinius vaizdus ne mažesnius kaip 20 x 20 pikselių." flair_bg_color: "Avatar Flair fono splva" @@ -838,6 +854,7 @@ lt: image: "Įkelti paveikslėlį" default_notifications: modal_title: "Numatytieji vartotojo pranešimai" + modal_description: "Ar norėtumėte šį pakeitimą pritaikyti istoriškai? Tai pakeis %{count} esamų vartotojų nuostatas." modal_yes: "Taip" modal_no: "Ne, taikykite pakeitimus tik į priekį" user_action_groups: @@ -895,6 +912,7 @@ lt: topics_entered: "paskelbtos temos" post_count: "# įrašai" confirm_delete_other_accounts: "Ar tikrai nori ištrinti šias paskyras?" + powered_by: "naudojant MaxMinDDB" copied: "nukopijuota" user_fields: none: "(pasirink nustatymą)" @@ -986,13 +1004,18 @@ lt: dismiss_notifications: "Atmesti visus" dismiss_notifications_tooltip: "Pažymėkite visus pranešimus kaip Perskaitytus." no_messages_title: "Jūs neturite jokių pranešimų" + no_messages_body: > + Reikia tiesioginio asmeninio pokalbio su kuo nors už įprasto pokalbio srauto ribų? Nusiųskite jiems žinutę pasirinkdami jų avatarą ir naudodami pranešimo mygtuką %{icon}

    Jei jums reikia pagalbos, galite pranešti darbuotojui. no_bookmarks_title: "Jūs dar nieko nepažymėjote" + no_bookmarks_body: > + Pradėkite žymėti įrašus %{icon} mygtuku ir jie bus pateikti čia, kad būtų lengviau peržiūrėti. Taip pat galite suplanuoti priminimą! no_bookmarks_search: "Nerasta jokių žymių pagal pateiktą paieškos užklausą." no_notifications_title: "Jūs dar neturite jokių pranešimų" no_notifications_page_title: "Jūs dar neturite jokių pranešimų" first_notification: "Asmeniniai perspėjimai. Jūs galite atlikti jums reikiamą užklausą pasirinkdami atitinkamus nustatymus." dynamic_favicon: "Rodyti skaičius naršyklės piktogramoje" skip_new_user_tips: + description: "Praleiskite naujų naudotojų priėmimo patarimus ir ženklelius" not_first_time: "Ne pirmas kartas?" skip_link: "Praleisti šiuos patarimus" read_later: "Perskaitysiu vėliau." @@ -1001,6 +1024,7 @@ lt: color_scheme: "Spalvų schema" color_schemes: default_description: "Tema numatytoji" + disable_dark_scheme: "Toks pat kaip įprastas" dark_instructions: "Galite peržiūrėti tamsaus režimo spalvų schemą perjungdami įrenginio tamsų režimą." undo: "Atstatyti" regular: "Nuolatinės" @@ -1144,6 +1168,7 @@ lt: use: "Naudoti atsarginį kodą" codes: title: "Sukurta atsarginių kopijų kodai" + description: "Kiekvienas iš šių atsarginių kodų gali būti naudojamas tik vieną kartą. Laikykite juos saugioje, bet prieinamoje vietoje." second_factor: title: "Dviejų veiksnių autentifikavimas" enable: "Tvarkyti dviejų veiksnių autentifikavimą" @@ -1235,6 +1260,7 @@ lt: auth_override_instructions: "El. paštas gali būti atnaujintas iš autentifikavimo teikėjo." no_secondary: "Antrinių el. Paštų nėra" instructions: "Nebus rodomas viešai." + admin_note: "Pastaba: Administratoriaus naudotojas, pakeitęs kito neadministratoriaus vartotojo el. pašto adresą, rodo, kad vartotojas prarado prieigą prie savo pradinės el. pašto paskyros, todėl naujuoju adresu bus išsiųstas slaptažodžio nustatymo iš naujo el. laiškas. Naudotojo el. pašto adresas nepasikeis, kol jis neužbaigs slaptažodžio nustatymo iš naujo proceso." ok: "Mes jums atsiųsime patvirtinimo elektroninį laišką" required: "Prašome įvesti elektroninio pašto adresą" invalid: "Prašom įrašyti teisingą elektroninį adresą" @@ -1315,6 +1341,7 @@ lt: larger: "Didesnis" largest: "Didžiausias" title_count_mode: + title: "Fono puslapio pavadinime rodomas skaičius:" notifications: "Nauji pranešimai" contextual: "Naujas puslapio turinys" like_notification_frequency: @@ -1329,6 +1356,7 @@ lt: always: "visada" never: "niekada" email_digests: + title: "Kai čia nesilankau, atsiųskite man el. paštu populiarių temų ir atsakymų santrauką" every_30_minutes: "kas 30 minučių" every_hour: "kas valandą" daily: "kas dieną" @@ -1368,6 +1396,7 @@ lt: title: "Pakvietimai" pending_tab: "Laukiama" pending_tab_with_count: "Neperskaityti (%{count})" + expired_tab: "Baigėsi galiojimo laikas" expired_tab_with_count: "Galiojimo laikas baigėsi %{count})" redeemed_tab: "Atstatyta" redeemed_tab_with_count: "Atstatyta (%{count})" @@ -1497,6 +1526,7 @@ lt: none: "(nieko)" instructions: "pasirodo po jūsų vartotojo vardo" flair: + title: "Nuojauta" none: "(nieko)" instructions: "piktograma, rodoma šalia jūsų profilio nuotraukos" primary_group: @@ -1603,6 +1633,8 @@ lt: reset: "Keisti slaptažodį" complete_username: "Jei paskyra sutampa su vartotojo vardu %{username}, turėtum gauti el.laišką su slaptažodžio keitimo instrukcijomis." complete_email: "Jei paskyra sutampa su %{email}, turėtum gauti el.laišką su slaptažodžio keitimo instrukcijomis." + complete_username_found: "Mes radome paskyrą, kuri atitinka vartotojo vardą %{username}. Netrukus turėtumėte gauti el. laišką su instrukcijomis, kaip iš naujo nustatyti slaptažodį." + complete_email_found: "Mes radome paskyrą, atitinkančią %{email}. Netrukus turėtumėte gauti el. laišką su instrukcijomis, kaip iš naujo nustatyti slaptažodį." complete_username_not_found: "Jokia paskyra nesutampa su vartotojo vardu %{username}" complete_email_not_found: "Jokia paskyra nesutampa su %{email}" button_ok: "GERAI" @@ -1612,6 +1644,8 @@ lt: button_label: "su el. paštu" login_link: "Praleisti slaptažodį; atsiųskite man prisijungimo nuorodą el. paštu" emoji: "užrakinti jaustukus" + complete_username: "Jei paskyra sutampa su vartotojo vardu %{username}, turėtumėte gauti el.laišką su slaptažodžio keitimo instrukcijomis." + complete_email: "Jei paskyra atitinka %{email}, netrukus turėtumėte gauti el. laišką su prisijungimo nuoroda." complete_username_not_found: "Jokia paskyra nesutampa su vartotojo vardu %{username}" complete_email_not_found: "Jokia paskyra nesutampa su %{email}" confirm_title: Tęsti į %{site_name} @@ -1790,6 +1824,7 @@ lt: edit_conflict: "redaguoti konfliktą" cannot_see_mention: category: "Paminėjote %{username} bet jiems nebus pranešta, nes jie neturi prieigos prie šios kategorijos. Turėsite juos pridėti prie grupės, kuri turi prieigą prie šios kategorijos." + reference_topic_title: "RE: %{title}" error: title_missing: "Antraštė turi būti užpildyta" post_missing: "Įrašas negali būti tuščias" @@ -1835,6 +1870,7 @@ lt: link_description: "įveskite čia nuorodos aprašymą" link_dialog_title: "Įkelti nuorodą" link_optional_text: "papildoma antraštė" + link_url_placeholder: "Įklijuokite URL arba įveskite, kad ieškotumėte temų" blockquote_title: "Blokuoti citatą" blockquote_text: "Blokuoti citatą" code_title: "Tekstas kodui" @@ -1859,6 +1895,7 @@ lt: draft: Juodraštis edit: Redaguoti reply_to_post: + label: Atsakyti į įrašą pagal %{postUsername} desc: Atsakyti į konkretų įrašą reply_as_new_topic: label: Atsakykite kaip susieta tema @@ -1882,6 +1919,7 @@ lt: label: "Bendrinami juodraščiai" desc: "Sukurkite temą, kuri bus matoma tik leidžiamiems vartotojams" toggle_topic_bump: + label: "Perjungti temos iškilimą" desc: "Atsakykite nekeisdami paskutinės atsakymo datos" reload: "Įkelti iš naujo" ignore: "Ignoruoti" @@ -1946,8 +1984,10 @@ lt: posted: '%{username} paskelbė "%{topic}" - %{site_title}' private_message: '%{username} atsiuntė privačią žinutė "%{topic}" - %{site_title}' linked: '%{username} panaudojo tavo įrašą "%{topic}" - %{site_title}' + watching_first_post: '%{username} sukūrė naują temą "%{topic}" - %{site_title}' confirm_title: "Pranešimai įgalinti - %{site_title}" confirm_body: "Pavyko! Pranešimai buvo įgalinti." + custom: "Pranešimas nuo %{username} %{site_title}" titles: mentioned: "paminėjimai" replied: "naujas atsakymas" @@ -1972,6 +2012,7 @@ lt: post_approved: "įrašas patvirtintas" membership_request_consolidated: "nauji narystės prašymai" reaction: "nauja reakcija" + votes_released: "Balsavimas buvo išleistas" upload_selector: uploading: "Įkeliama" processing: "Apdorojamas įkėlimas" @@ -2018,6 +2059,7 @@ lt: enter_hint: "arba paspauskite Enter" in_posts_by: "pranešimuose %{username}" browser_tip: "%{modifier} + f" + browser_tip_description: "dar kartą norėdami naudoti savosios naršyklės paiešką" recent: "Naujausios paieškos" clear_recent: "Išvalyti naujausias paieškas" type: @@ -2076,6 +2118,7 @@ lt: public: yra viešas archived: archyvuota noreplies: neturi atsakymų + single_user: yra vienas vartotojas post: count: label: Įrašai @@ -2285,6 +2328,8 @@ lt: forever: "Am-inai" pick_date_and_time: "Pasirinkite datą ir laiką" set_based_on_last_post: "Uždaryti pagal paskutinį įrašą" + publish_to_category: + title: "Suplanuokite leidybą" temp_open: title: "Laikinai atidaryti" auto_reopen: @@ -2300,6 +2345,8 @@ lt: title: "Automatiškai uždaryti temą po paskutinio įrašo" auto_delete: title: "Automatiškai ištrinti temą" + auto_bump: + title: "Automatiškai iškylanti tema" reminder: title: "Man priminti" auto_delete_replies: @@ -2307,8 +2354,10 @@ lt: status_update_notice: auto_open: "Ši tema automatiškai atsidarys po %{timeLeft} ." auto_close: "Ši tema automatiškai užsidarys po %{timeLeft}." + auto_publish_to_category: "Ši tema bus paskelbta #%{categoryName} %{timeLeft}." auto_close_after_last_post: "Ši tema užsidarys po %{duration} po paskutinio atsakymo." auto_delete: "Ši tema bus automatiškai ištrinta %{timeLeft}." + auto_reminder: "Jums bus priminta apie šią temą %{timeLeft}." auto_close_title: "Automatinio uždarymo nustatymai" timeline: back: "Atgal" @@ -2343,7 +2392,7 @@ lt: "2_8_stale": "Matysite naujų atsakymų skaičių, nes anksčiau stebėjote šią kategoriją." "2_4": "Pamatysite naujų atsakymų skaičių, nes paskelbėte atsakymą į šią temą." "2_2": "Matysite naujų atsakymų skaičių, nes stebite šią temą." - "2": 'You will see a count of new replies because you read this topic.' + "2": 'Matysite naujų atsakymų skaičių, nes skaitėte šią temą.' "1_2": "Būsi perspėtas kai kažkas paminės tavo @vardą ar tau atsakys." "1": "Būsi perspėtas kai kažkas paminės tavo @vardą ar tau atsakys." "0_7": "Jūs ignoruojate visus šios temos pranešimus" @@ -2516,6 +2565,7 @@ lt: radio_label: "Nauja žinutė" participants: "Dalyviai" move_to_existing_message: + action: "pereiti prie esamo pranešimo" radio_label: "Esama žinutė" participants: "Dalyviai" merge_posts: @@ -2595,6 +2645,9 @@ lt: few: "peržiūrėti %{count} paslėptus atsakymus" many: "peržiūrėti %{count} paslėptus atsakymus" other: "peržiūrėti %{count} paslėptus atsakymus" + notice: + new_user: "Tai pirmas kartas, kai %{user} parašė – sveikinkime juos mūsų bendruomenėje!" + returning_user: "Jau kurį laiką nematėme %{user} – paskutinis jų įrašas buvo %{time}." unread: "Įrašas yra neperskaitytas" has_replies: one: "%{count} Atsakymas" @@ -2614,6 +2667,7 @@ lt: few: "tau ir %{count} žmonėms patiko šis įrašas" many: "tau ir %{count} žmonėms patiko šis įrašas" other: "tau ir %{count} žmonėms patiko šis įrašas" + in_reply_to: "Įkelti pirminį įrašą" view_all_posts: "Peržiūrėti visus įrašus" errors: create: "Atsiprašome, įvyko klaida kuriant įrašą. Prašome pamėginti dar kartą." @@ -2677,6 +2731,7 @@ lt: change_owner: "Keisti savininką ..." grant_badge: "Suteikti ženklelį ..." lock_post: "Užrakinti įrašą" + lock_post_description: "neleisti skelbėjui redaguoti šio įrašo" unlock_post: "Atrakinti įrašą" unlock_post_description: "leisti paskelbusiam asmeniui redaguoti šį įrašą" delete_topic_disallowed_modal: "Neturite leidimo ištrinti šios temos. Jei tikrai norite, kad jis būtų ištrintas, kartu su argumentais pateikite vėliavą moderatoriui." @@ -2820,8 +2875,10 @@ lt: create: "Sukurti" no_groups_selected: "Prieiga nebuvo suteikta jokiai grupei; šią kategoriją matys tik darbuotojai." everyone_has_access: 'Ši kategorija yra vieša, visi gali matyti, atsakyti ir kurti įrašus. Jei norite apriboti leidimus, pašalinkite vieną ar kelis grupei „visi“ suteiktus leidimus.' + toggle_reply: "Perjungti atsakymo leidimą" special_warning: "Įspėjimas: Ši kategorija yra iš anksto numatyta kategorija ir saugumo nustatymai negali būti keičiami. Jeigu nenorite naudoti šios kategorijos, ištrinkie ją vietoj pakartotino naudojimo." uncategorized_security_warning: "Ši kategorija yra ypatinga. Jis skirta temoms, kurios neturi kategorijos, laikyti; ji negali turėti saugos nustatymų." + pending_permission_change_alert: "Jūs nepridėjote %{group} į šią kategoriją; spustelėkite šį mygtuką, norėdami juos pridėti." images: "Paveiksliukai" email_in: " papildomas naujas el. pašto adresas:" email_in_allow_strangers: "Gauti el. laiškus iš anoniminių paskyrų neturinčių vartotojų " @@ -2829,7 +2886,9 @@ lt: email_in_disabled_click: 'nustatykite "email in" nustatymus.' mailinglist_mirror: "Kategorija atspindi adresatų sąrašą" show_subcategory_list: "Rodyti subkategorijų sąrašą virš šios kategorijos temų." + read_only_banner: "Reklamjuostės tekstas, kai vartotojas negali sukurti temos šioje kategorijoje:" num_featured_topics: "Temų, rodomų kategorijų puslapyje, skaičius:" + subcategory_num_featured_topics: "Panašių temų skaičius pagrindinės kategorijos puslapyje:" all_topics_wiki: "Pagal numatytuosius nustatymus kurti naujas temas wiki" allow_unlimited_owner_edits_on_first_post: "Leisti neribotą pirmojo įrašo savininko redagavimą" subcategory_list_style: "Subkategorijų sąrašo stilius:" @@ -2851,8 +2910,10 @@ lt: minimum_required_tags: "Minimalus temoje reikalingas žymų skaičius:" default_slow_mode: 'Įgalinkite „lėtąjį režimą“ naujoms šios kategorijos temoms.' parent: "Tėvinė kategoriją" + num_auto_bump_daily: "Atvirų temų, kurios bus automatiškai rodomos kasdien, skaičius:" navigate_to_first_post_after_read: "Perskaitykite temas, eikite į pirmąjį įrašą" notifications: + title: "pakeisti šios kategorijos pranešimų lygį" watching: title: "Stebimos" description: "Jūs automatiškai žiūrėsite visas šios kategorijos temas. Jums bus pranešta apie kiekvieną naują įrašą kiekvienoje temoje ir bus parodytas naujų atsakymų skaičius." @@ -2861,11 +2922,13 @@ lt: description: "Jums bus pranešta apie naujas temas šioje kategorijoje, bet ne atsakymus į temas." tracking: title: "Sekamos" + description: "Jūs automatiškai stebėsite visas šios kategorijos temas. Jums bus pranešta, jei kas nors paminės jūsų @vardą arba atsakys jums, ir bus rodomas naujų atsakymų skaičius." regular: title: "Įprastas" description: "Būsi perspėtas kai kažkas paminės tavo @vardą ar tau atsakys." muted: title: "Nutildytos" + description: "Niekada jums nebus pranešta apie naujas šios kategorijos temas ir jos nebus rodomos naujausiose." search_priority: label: "Paieškos prioritetas" options: @@ -3158,6 +3221,8 @@ lt: dismiss_new: "%{shortcut} Atmesti naują" dismiss_topics: "%{shortcut} Praleisti Temas" log_out: "%{shortcut} Atsijungti" + composing: + title: "Komponavimas" bookmarks: title: "Žymėjimas" enter: "%{shortcut} Išsaugoti ir uždaryti" @@ -3254,17 +3319,20 @@ lt: tags: "Žymos" choose_for_topic: "pasirenkamos žymos" info: "Informacija" + default_info: "Ši žyma neapsiriboja jokiomis kategorijomis ir neturi sinonimų." + staff_info: "Norėdami pridėti apribojimų, įtraukite šią žymą į žymų grupę." category_restricted: "Ši žyma skirta tik kategorijoms, prie kurių neturite prieigos teisės." synonyms: "Sinonimai" - edit_synonyms: "Tvarkyti sinonimus" + save: "Išsaugokite žymos pavadinimą ir aprašymą" + edit_synonyms: "Redaguoti sinonimus" add_synonyms_label: "Pridėti sinonimus:" add_synonyms: "Pridėti" remove_synonym: "Pašalinti sinonimą" delete_synonym_confirm: 'Ar tikrai norite ištrinti sinonimą "%{tag_name}“?' delete_tag: "Ištrinti žymą" delete_confirm_no_topics: "Ar tikrai norite ištrinti šią žymą?" - rename_tag: "Pervadinti žymą" - rename_instructions: "Pasirinkite naują žymos pavadinimą:" + edit_tag: "Redaguoti žymos pavadinimą ir aprašymą" + description: "Aprašymas" sort_by: "Rūšiuoti pagal:" sort_by_count: "skaičių" sort_by_name: "vardą" @@ -3399,6 +3467,7 @@ lt: title: "Galimų ataskaitų sąrašas" dashboard: title: "Apžvalga" + last_updated: "Informacijos suvestinė atnaujinta:" discourse_last_updated: "Discourse atnaujinta:" version: "Versija" up_to_date: "Jūs turite naujausią versiją!" @@ -3470,6 +3539,8 @@ lt: dates: "Datos (UTC)" groups: "Visos grupės" disabled: "Ši ataskaita išjungta" + average_for_sample: "Pavyzdžio vidurkis" + total: "Visas laikas iš viso" no_data: "Nėra rodomų duomenų." trending_search: more: 'Paieškos įrašai' @@ -3548,6 +3619,7 @@ lt: key: "Raktas" created: Sukurta updated: Atnaujinta + last_used: Paskutinis naudotas never_used: (niekada) generate: "Generuoti" undo_revoke: "Anuliuoti Atšaukti" @@ -3560,6 +3632,7 @@ lt: no_description: (Nėra aprašymo) all_api_keys: Visi API raktai user_mode: Vartotojo lygis + scope_mode: Apimtis impersonate_all_users: Apsimesti bet kokiu vartotoju single_user: "Vienas vartotojas" user_placeholder: Įveskite vartotojo vardą @@ -3568,10 +3641,12 @@ lt: new_key: Naujas API raktas revoked: Atšauktas delete: Ištrinti visam laikui + not_shown_again: Šis raktas daugiau nebus rodomas. Prieš tęsdami būtinai nukopijuokite. continue: Tęsti scopes: read_only: Tik skaityti global: Pasaulinis + global_description: API raktas neturi apribojimų ir visi galiniai taškai yra prieinami. resource: Ištekliai action: Veiksmas allowed_parameters: Leidžiami parametrai @@ -3579,6 +3654,8 @@ lt: any_parameter: (bet koks parametras) allowed_urls: Leidžiami URL descriptions: + global: + read: Apriboti API raktą iki tik skaitomų galinių taškų. topics: read: Perskaitykite temą ar konkretų įrašą. RSS taip pat palaikomas. write: Sukurkite naują temą arba paskelbkite esamą. @@ -3590,19 +3667,34 @@ lt: create: Įkelti naują failą. users: bookmarks: Sąrašas vartotojo žymių. Naudojant ICS formatą, jis grąžina priminimus apie žymes. + sync_sso: Sinchronizuokite vartotoją naudodami DiscourseConnect. show: Gauti informaciją apie vartotoją. check_emails: Vartotojų el. paštų sąrašas update: Atnaujinti vartotojo profilio informaciją. + log_out: Atjungti visus seansus vartotojui. anonymize: Anonimizuokite vartotojų paskyras. delete: Ištrinti vartotojų paskyras. list: Gaukite vartotojų sąrašą. + email: + receive_emails: Sujunkite šią taikymo sritį su pašto gavėju, kad apdorotumėte gaunamus el. laiškus. + badges: + create: Sukurkite naują ženklelį. + show: Gaukite informacijos apie ženklelį. + update: Atnaujinkite ženklelį. + delete: Ištrinti ženklelį. + list_user_badges: Pateikite vartotojų ženklelių sąrašą. + assign_badge_to_user: Priskirkite ženklelį vartotojui. + revoke_badge_from_user: Atšaukti vartotojo ženklelį. web_hooks: + detailed_instruction: "POST užklausa bus išsiųsta nurodytu URL, kai įvyks pasirinktas įvykis." create: "Sukurti" save: "Saugoti" destroy: "Pašalinti" description: "Aprašymas" controls: "Valdymai" go_back: "Atgal į sąrašą" + payload_url: "Naudingos apkrovos URL" + payload_url_placeholder: "https://example.com/postreceive" secret_invalid: "Paslaptyje neturi būti tuščių simbolių." content_type: "Turinio tipas" secret: "Slaptas" @@ -3611,8 +3703,15 @@ lt: verify_certificate: "Patikrinkite TLS naudingos apkrovos URL sertifikatą" active: "Aktyvus" active_notice: "Mes pristatysime renginio detales, kai tai įvyks." + categories_filter: "Suaktyvintos kategorijos" + tags_filter: "Suaktyvintos žymos" + groups_filter: "Suaktyvintos grupės" topic_event: name: "Temos įvykis" + details: "Kai yra nauja tema, peržiūrėta, pakeista arba ištrinta." + post_event: + name: "Paskelbti įvykį" + details: "Kai yra naujas atsakymas, redaguokite, ištrinkite arba atkurkite." user_event: name: "Vartotojo įvykis" group_event: @@ -3651,6 +3750,7 @@ lt: body: "Turinys" go_list: "Eiti į sąrašą" go_events: "Eiti į renginius" + status: "Būsenos kodas" event_id: "ID" timestamp: "Sukurta" completion: "Sukurimo laikas" @@ -3692,6 +3792,7 @@ lt: title: "Įkelti atsarginę kopiją šiam atskiram atvejui" uploading: "Įkeliama" uploading_progress: "Įkeliama: %{progress}%" + success: "“%{filename}” sėkmingai įkelta. Failas dabar yra apdorojamas ir užtruks iki minutės, kol pasirodys sąraše." error: "Įvyko klaida įkeliant '%{filename}': %{message}" operations: is_running: "Šiuo metu jau yra vykdoma operacija..." @@ -3704,9 +3805,11 @@ lt: label: "Kurti atsarginę kopiją" title: "Sukurti atsarginę kopiją" confirm: "Ar tikrai norite pradėti naują atsarginės kopijos darymą?" + without_uploads: "Taip (neįtraukti įkėlimų)" download: label: "Atsisiųsti" title: "Siųsti laišką su atsisiuntimo nuoroda" + alert: "Šios atsarginės kopijos atsisiuntimo nuoroda jums buvo išsiųsta el. paštu." destroy: title: "Ištrinti atsarginę kopiją" confirm: "Ar tikrai norite ištrinti šią atsarginę kopiją?" @@ -3802,6 +3905,7 @@ lt: color_scheme: "Spalvų paletė" default_light_scheme: "Šviesus (numatytasis)" color_scheme_select: "Pasirinkite spalvas, kurias norite naudoti pagal temą" + custom_sections: "Pasirinktiniai skyriai:" theme_components: "Temos komponentai" add_all_themes: "Pridėti visas temas" convert: "Paversti" @@ -3821,6 +3925,7 @@ lt: uploads: "Įkėlimai" no_uploads: "Galite įkelti su tema susietus išteklius, pvz., šriftus ir vaizdus" add_upload: "Pridėti įkėlimą" + variable_name: "SCSS var pavadinimas:" variable_name_invalid: "Neteisingas kintamojo pavadinimas. Leidžiama naudoti tik raidinius ir skaitmeninius. Turi prasidėti raide. Turi būti unikalus." variable_name_error: invalid_syntax: "Neteisingas kintamojo pavadinimas. Leidžiama naudoti tik raidinius ir skaitmeninius. Turi prasidėti raide." @@ -3832,6 +3937,7 @@ lt: unsaved_parent_themes: "Jūs nepriskyrėte komponento temoms, ar norite judėti toliau?" discard: "Išmesti" stay: "Likti" + css_html: "Pasirinktinis CSS/HTML" edit_css_html: "Redaguoti CSS/HTML" edit_css_html_help: "Jūs neredagavote jokio CSS ar HTML" delete_upload_confirm: "Ištrinti šį įkėlimą? (Tema CSS gali nustoti veikti!)" @@ -3839,9 +3945,12 @@ lt: included_components: "Įtraukti komponentai" add_all: "Pridėti visus" import_web_tip: "Saugykla, kurioje yra tema" + direct_install_tip: "Ar tikrai norite įdiegti %{name} iš toliau nurodytos saugyklos?" + import_web_advanced: "Išplėstinė..." import_file_tip: ".tar.gz, .zip arba .dcstyle.json failas, kuriame yra tema" is_private: "Tema yra privačioje „git“ saugykloje" remote_branch: "Filialo pavadinimas (neprivaloma)" + public_key_note: "Aukščiau įvedus galiojantį privačios saugyklos URL, bus sugeneruotas ir čia rodomas SSH raktas." install: "diegti" installed: "Įrašyta" install_popular: "Populiaros" @@ -3858,6 +3967,10 @@ lt: enable: "Įgalinti" disable: "Išjungti" disabled: "Šis komponentas buvo išjungtas." + disabled_by: "Šį komponentą išjungė" + required_version: + minimum: "Reikalinga Discourse %{version} arba naujesnė versija." + maximum: "Reikalinga Discourse %{version} arba senesnė versija." component_of: "Komponentas:" update_to_latest: "Atnaujinti iki paskutinės" check_for_updates: "Tikrinti atnaujinimus " @@ -3868,6 +3981,7 @@ lt: no_settings: "Ši tema neturi nustatymų." theme_translations: "Temos vertimai" empty: "Nėra elementų" + compare_commits: "(Žr. naujus įsipareigojimus)" repo_unreachable: "Nepavyko susisiekti su šios temos „Git“ saugykla. Klaidos pranešimas:" imported_from_archive: "Ši tema buvo importuota iš .zip failo" scss: @@ -3890,6 +4004,7 @@ lt: text: "Turinys" yaml: text: "YAML" + title: "Apibrėžkite temos nustatymus YAML formatu" colors: select_base: title: "Pasirinkite pagrindinę spalvų paletę" @@ -3955,6 +4070,7 @@ lt: preview_digest: "Parodyti Santrauką" advanced_test: title: "Išplėstinis testas" + desc: "Sužinokite, kaip Discourse apdoroja gautus el. laiškus. Kad galėtumėte tinkamai apdoroti el. laišką, įklijuokite žemiau visą pradinį el. pašto pranešimą." email: "Originali žinutė" run: "Vykdyti testą" sending_test: "Siunčiamas testinis el. laiškas..." @@ -4066,6 +4182,8 @@ lt: change_site_text: "keisti puslapio tekstą" suspend_user: "sustabdyti vartotoją" unsuspend_user: "atkurti vartotoją" + removed_suspend_user: "sustabdyti vartotoją (pašalintas)" + removed_unsuspend_user: "atšaukti naudotojo sustabdymą (pašalintas)" grant_badge: "suteikti trofėjų" revoke_badge: "atimti trofėjų" check_email: "patikrinti el. paštą" @@ -4078,7 +4196,10 @@ lt: change_category_settings: "keisti katetgorijos nustatymus" delete_category: "ištrinti kategoriją" create_category: "sukurti kategoriją" + silence_user: "nutildyti narį" + unsilence_user: "atšaukti nario nutildymą" removed_silence_user: "nutildyti naudotoją (pašalintas)" + removed_unsilence_user: "atšaukti naudotojo sustabdymą (pašalintas)" grant_admin: "suteikti administratoriaus teises" revoke_admin: "atšaukti administratoriaus teises" grant_moderation: "suteikti moderatoriaus teises" @@ -4087,6 +4208,7 @@ lt: deleted_tag: "pašalinti žymą" deleted_unused_tags: "ištrinkite nepanaudotas žymas" renamed_tag: "pervadinti žymą" + revoke_email: "atšaukti el. paštą" lock_trust_level: "užrakinti patikimumo lygį" unlock_trust_level: "atrakinti patikimumo lygį" activate_user: "aktyvuoti narį" @@ -4095,6 +4217,7 @@ lt: backup_download: "atsisiųsti atsarginę kopiją" backup_destroy: "sunaikinti atsarginę kopiją" reviewed_post: "peržiūrėtas įrašas" + post_locked: "įrašas užrakintas" post_edit: "įrašo redagavimas" post_unlocked: "įrašas atrakintas" check_personal_message: "patikrinti asmeninę žinutę" @@ -4106,14 +4229,19 @@ lt: change_badge: "pakeisti ženklelį" delete_badge: "ištrinti ženklelį" merge_user: "sujungti vartotoją" + entity_export: "eksporto subjektas" change_name: "pakeisti vardą" topic_timestamps_changed: "temos laiko žymos pakeistos" approve_user: "patvirtinti vartotoją" + change_theme_setting: "pakeisti temos nustatymą" + disable_theme_component: "išjungti temos komponentą" + enable_theme_component: "įgalinti temos komponentą" revoke_title: "atšaukti pavadinimą" change_title: "pakeisti pavadinimą" api_key_create: "api raktas sukurtas" api_key_update: "api rakto atnaujinimas" api_key_destroy: "api raktas sunaikintas" + override_upload_secure_status: "nepaisyti saugios įkėlimo būsenos" page_published: "puslapis paskelbtas" page_unpublished: "puslapis nepaskelbtas" add_email: "pridėti el. paštą" @@ -4195,6 +4323,7 @@ lt: tag: "Automatiškai žymėti temas pagal pirmąjį įrašą" form: label: "Turi žodį ar frazę" + placeholder_regexp: "Įprasta išraiška" replace_label: "Pakeitimas" replace_placeholder: "pavyzdys" tag_label: "Žymėti" @@ -4251,6 +4380,7 @@ lt: title: "Atskleisk šio vartotojo el. pašto adresą" text: "Rodyti" check_sso: + title: "Atskleisti SSO naudingąją apkrovą" text: "Rodyti" user: suspend_failed: "Įvyko klaida sustabdant vartotoją %{error}" @@ -4266,6 +4396,7 @@ lt: suspend_message_placeholder: "Pasirinktinai pateikite daugiau informacijos apie sustabdymą ir ji bus išsiųsta el. paštu vartotojui." suspended_by: "Sustabdė" silence_reason: "Priežastis" + silenced_by: "Nutildė" silence_modal_title: "Nutildyti narį" silence_duration: "Kiek laiko vartotojas bus nutildytas?" silence_reason_label: "Kodėl nutildote šį vartotoją?" @@ -4339,9 +4470,12 @@ lt: delete_posts: button: "Ištrinti visus pranešimus" progress: + title: "Pranešimų ištrynimo pažanga" description: "Ištrinami įrašai ..." confirmation: title: "Ištrinkite visus @%{username}" + text: "ištrinti pranešimus pagal @%{username}" + delete: "Ištrinti pranešimus pagal @%{username}" cancel: "Atšaukti" merge: button: "Sujungti" @@ -4397,6 +4531,8 @@ lt: suspended_explanation: "Sustabdytas vartotojas negali prisijungti." silence_explanation: "Nutildytas vartotojas negali skelbti ar pradėti temų." staged_explanation: "Inscenizuotas vartotojas gali skelbti įrašus tik specialiose temose." + bounce_score_explanation: + none: "Pastaruoju metu iš šio el. pašto negauta jokių atmetimų." trust_level_change_failed: "Įvyko klaida keičiant vartotojo pasitikėjimo lygį." suspend_modal_title: "Sustabdyti vartotoją" trust_level_2_users: "Vartotojai su 2 Pasitikėjimo Statusu " @@ -4440,6 +4576,7 @@ lt: external_name: "Vardas" external_email: "Epaštas" external_avatar_url: "Profilio Nuotraukos URL" + last_payload: "Paskutinė naudingoji apkrova" delete_sso_record: "Ištrinti SSO įrašą" confirm_delete: "Ar tikrai norite ištrinti šį „DiscourseConnect“ įrašą?" user_fields: @@ -4501,6 +4638,7 @@ lt: none: "nieko" site_settings: emoji_list: + invalid_input: "Jaustukų sąraše turi būti tik tinkami jaustukų pavadinimai, pvz.: apkabinimai" add_emoji_button: label: "Pridėti jaustukus" title: "Nustatymai" @@ -4638,10 +4776,12 @@ lt: no_badge_selected: Pasirinkite ženklelį, kad pradėtumėt. perform: "Apdovanojimo ženkliukas vartotojams" upload_csv: Įkelkite CSV su vartotojo el. laiškais arba vartotojo vardais + aborted: Įkelkite CSV failą su naudotojų el. pašto adresais arba naudotojų vardais replace_owners: Pašalinti ženklelį iš ankstesnių savininkų grant_existing_holders: Suteikite papildomų ženklelių esamiems ženklelių savininkams emoji: title: "Šypsenėlės" + help: "Pridėkite naujų jaustukų, kurie bus prieinami visiems. Vilkite ir numeskite kelis failus vienu metu neįvesdami pavadinimo, kad sukurtumėte jaustukus naudodami jų failų pavadinimus." add: "Pridėti Naują Šypsenėlę" uploading: "Įkeliama..." name: "Vardas" diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index 49bb3b5699..2065ba1b5e 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -2775,8 +2775,7 @@ lv: tags: "Tagi" add_synonyms: "Pievienot" delete_tag: "Dzēst tagu" - rename_tag: "Pārsaukt tagu" - rename_instructions: "Izvēlēties jaunu nosaukumu tagam:" + description: "Apraksts" sort_by: "Kārtot pēc:" sort_by_count: "skaita" sort_by_name: "vārda" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index b8b0b22da4..07f86438b5 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -3316,7 +3316,6 @@ nb_NO: tags: "Stikkord" choose_for_topic: "valgfrie stikkord" info: "Informasjon" - default_info: "Denne taggen er ikke begrenset til noen kategorier, og har ingen synonymer. For å legge til restriksjoner, legg til denne taggen i en tagg gruppe." category_restricted: "Denne taggen er begrenset til kategorier du ikke har tilgang til tilgang." synonyms: "Synonymer" synonyms_description: "Når følgende merkelapper brukes, vil de bli erstattet med %{base_tag_name}." @@ -3326,7 +3325,6 @@ nb_NO: category_restrictions: one: "Den kan bare brukes i denne kategorien:" other: "Den kan kun brukes i disse kategoriene:" - edit_synonyms: "Administrer synonymer" add_synonyms_label: "Legg til synonymer:" add_synonyms: "Legg til" add_synonyms_explanation: @@ -3343,8 +3341,7 @@ nb_NO: delete_confirm_synonyms: one: "Synonymen vil også bli slettet." other: "Dets %{count} synonymer vil også bli slettet." - rename_tag: "Gi stikkord nytt navn" - rename_instructions: "Velg et nytt navn for dette stikkordet:" + description: "Beskrivelse" sort_by: "Sorter etter:" sort_by_count: "antall" sort_by_name: "navn" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 4679086dcb..bc2d6c6de8 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -3352,6 +3352,7 @@ nl: tags: "Tags" choose_for_topic: "optionele tags" info: "Info" + default_info: "Deze tag is niet beperkt tot categorieën, en heeft geen synoniemen." category_restricted: "Deze tag is beperkt tot categorieën waartoe u geen toegang hebt." synonyms: "Synoniemen" synonyms_description: "Wanneer de volgende tags worden gebruikt, worden deze vervangen door %{base_tag_name}." @@ -3361,7 +3362,6 @@ nl: category_restrictions: one: "Deze kan alleen worden gebruikt in deze categorie:" other: "Deze kan alleen worden gebruikt in deze categorieën:" - edit_synonyms: "Synoniemen beheren" add_synonyms_label: "Synoniemen toevoegen:" add_synonyms: "Toevoegen" add_synonyms_explanation: @@ -3378,8 +3378,7 @@ nl: delete_confirm_synonyms: one: "Het synoniem ervan wordt ook verwijderd." other: "De %{count} synoniemen ervan worden ook verwijderd." - rename_tag: "Tag hernoemen" - rename_instructions: "Kies een nieuwe naam voor de tag:" + description: "Omschrijving" sort_by: "Sorteren op:" sort_by_count: "aantal" sort_by_name: "naam" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 59734067e9..558febd466 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -3767,7 +3767,8 @@ pl_PL: tags: "Tagi" choose_for_topic: "tagi opcjonalne" info: "Informacje" - default_info: "Ten tag nie jest ograniczony do żadnych kategorii i nie ma synonimów. Aby dodać ograniczenia, umieść ten tag w grupie tagów ." + 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}." @@ -3781,7 +3782,7 @@ pl_PL: few: "Można go używać tylko w tych kategoriach:" many: "Można go używać tylko w tych kategoriach:" other: "Można go używać tylko w tych kategoriach:" - edit_synonyms: "Zarządzaj synonimami" + edit_synonyms: "Edytuj synonimy" add_synonyms_label: "Dodaj synonimy:" add_synonyms: "Dodaj" add_synonyms_explanation: @@ -3804,8 +3805,7 @@ pl_PL: few: "Jego %{count} synonimy również zostaną usunięte." many: "Jego %{count} synonimów również zostanie usuniętych." other: "Jego %{count} synonimów również zostanie usuniętych." - rename_tag: "Zmień nazwę taga" - rename_instructions: "Wybierz nową nazwę dla tego taga:" + description: "Opis" sort_by: "Sortuj po:" sort_by_count: "Liczba" sort_by_name: "nazwa" diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 01e76b7cd5..4cdd6c48b9 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -3282,7 +3282,6 @@ pt: tags: "Etiquetas" choose_for_topic: "tags opcionais" info: "Informações" - default_info: "Esta tag não está restrita a nenhuma categoria e não possui sinônimos. Para adicionar restrições, coloque esta tag em um grupo de tags." category_restricted: "Esta tag é restrita a categorias que você não tem permissão para acessar." synonyms: "Sinônimos" synonyms_description: "Quando as tags seguintes forem usadas, elas serão substituídas por %{base_tag_name}." @@ -3292,7 +3291,6 @@ pt: category_restrictions: one: "Só pode ser usado nesta categoria:" other: "Só pode ser usado nessas categorias:" - edit_synonyms: "Gerenciar Sinônimos" add_synonyms_label: "Adicionar sinônimos:" add_synonyms: "Adicionar" remove_synonym: "Remover Sinônimo" @@ -3304,8 +3302,7 @@ pt: delete_confirm_synonyms: one: "Seu sinônimo também será excluído." other: "Seus %{count} sinônimos também serão excluídos." - rename_tag: "Renomear Etiqueta" - rename_instructions: "Escolha o novo nome para a etiqueta:" + description: "Descrição" sort_by: "Ordenar por:" sort_by_count: "contagem" sort_by_name: "nome" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 65c9a2ff0c..907295f0ba 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -3390,7 +3390,7 @@ pt_BR: tags: "Etiquetas" choose_for_topic: "etiquetas opcionais" info: "Informações" - default_info: "Esta etiqueta não é restrita a nenhuma categoria nem tem sinônimos. Para adicionar restrições, coloque esta etiqueta em um a grupo de etiquetas." + default_info: "Esta etiqueta não está restrita a nenhuma categorias e não possui sinônimos." category_restricted: "Essa tag é restrita a categorias que você não tem permissão para acessar." synonyms: "Sinônimos" synonyms_description: "Quando as seguintes etiquetas forem usadas, serão substituídas por %{base_tag_name}." @@ -3400,7 +3400,6 @@ pt_BR: category_restrictions: one: "Pode ser usada apenas nesta categoria:" other: "Pode ser usada apenas nestas categorias:" - edit_synonyms: "Gerenciar sinônimos" add_synonyms_label: "Adicionar sinônimos:" add_synonyms: "Adicionar" add_synonyms_explanation: @@ -3417,8 +3416,7 @@ pt_BR: delete_confirm_synonyms: one: "Seu sinônimo também será excluído." other: "Seus %{count} sinônimos também serão excluídos." - rename_tag: "Renomear marcador" - rename_instructions: "Escolha um novo nome para a etiqueta:" + description: "Descrição" sort_by: "Ordenar por" sort_by_count: "quantidade" sort_by_name: "nome" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 2a97a99b4b..a9eb63bbaf 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -2709,8 +2709,7 @@ ro: few: "Ești sigur că vrei să ștergi această etichetă și să o scoți din %{count} subiecte care o folosesc?" other: "Ești sigur că vrei să ștergi această etichetă și să o scoți din %{count} subiecte care o folosesc?" delete_confirm_no_topics: "Ești sigur că vrei să ștergi această etichetă?" - rename_tag: "Redenumire etichetă" - rename_instructions: "Alege un nume nou pentru etichetă:" + description: "Descriere" sort_by: "Sortat după:" sort_by_count: "contor" sort_by_name: "nume" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 83e961b90e..d36b50edd3 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -3796,10 +3796,12 @@ ru: many: "Вы должны выбрать как минимум %{count} тегов" other: "Вы должны выбрать как минимум %{count} тегов" info: "Информация" - default_info: "Этот тег не используется ни в одном разделе и не имеет синонимов. Чтобы использовать этот тег, поместите его в группу тегов." + default_info: "Этот тег не ограничен никакими разделами и не имеет синонимов." + staff_info: "Для добавления ограничения поместите этот тег в группу тегов." category_restricted: "Этот тег используется в разделах, к которым у вас нет доступа." synonyms: "Синонимы" synonyms_description: "При использовании следующих тегов они будут заменены на %{base_tag_name}." + save: "Сохранить имя и описание тега" tag_groups_info: one: 'Этот тег принадлежит группе "%{tag_groups}".' few: "Этот тег принадлежит к этим группам: %{tag_groups}." @@ -3810,7 +3812,7 @@ ru: few: "Их можно использовать только в этом разделе:" many: "Их можно использовать только в этом разделе:" other: "Их можно использовать только в этом разделе:" - edit_synonyms: "Управление синонимами" + edit_synonyms: "Редактировать синонимы" add_synonyms_label: "Добавить синонимы:" add_synonyms: "Добавить" add_synonyms_explanation: @@ -3833,8 +3835,8 @@ ru: few: "Его %{count} синонима также будут удалены." many: "Его %{count} синонимов также будут удалены." other: "Его %{count} синонимов также будут удалены." - rename_tag: "Редактировать тег" - rename_instructions: "Выберите новое название тега:" + edit_tag: "Изменить имя и описание тега" + description: "Описание" sort_by: "Сортировка:" sort_by_count: "Количество" sort_by_name: "Название" @@ -4216,6 +4218,14 @@ ru: list: Получение списка пользователей. email: receive_emails: Объединение этой области действия с получателем почты для обработки входящих писем. + badges: + create: Создание новой награды. + show: Получение информации о награде. + update: Обновление награды. + delete: Удаление награды. + list_user_badges: Список наград пользователя. + assign_badge_to_user: Назначение награды пользователю. + revoke_badge_from_user: Отзыв награды у пользователя. web_hooks: title: "Вебхуки" none: "Вебхуки отсутствуют." diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 8c680c1f76..f032f65294 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -2235,8 +2235,7 @@ sk: tags: "Štítky" add_synonyms: "Pridať" delete_tag: "Zmazať štítok" - rename_tag: "Premenovať štítok" - rename_instructions: "Vyberte nové meno pre štítok:" + description: "Popis" sort_by: "Zoradiť podľa:" sort_by_count: "počtu" sort_by_name: "mena" diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index dc82f3d4ec..63b9c877ca 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -3269,6 +3269,7 @@ sl: changed: "spremenjene oznake:" tags: "Oznake" choose_for_topic: "neobvezne oznake" + default_info: "Ta oznaka ni omejena na nobeno kategorijo in nima sopomenk." category_restricted: "Ta oznaka je omejena na kategorije, do katerih nimate dovoljenja za dostop." synonyms: "Sopomenke" synonyms_description: "Ko bodo uporabljene naslednje oznake, bodo nadomeščene z %{base_tag_name}." @@ -3282,7 +3283,6 @@ sl: two: "Uporablja se lahko le v teh dveh kategorijah:" few: "Uporablja se lahko le v teh kategorijah:" other: "Uporablja se lahko le v teh kategorijah:" - edit_synonyms: "Upravljanje sopomenk" add_synonyms_label: "Dodajte sopomenke:" add_synonyms: "Dodaj" add_synonyms_explanation: @@ -3305,8 +3305,7 @@ sl: two: "Izbrisani bosta tudi njeni %{count} sopomenki." few: "Izbrisane bodo tudi njene %{count} sopomenke." other: "Izbrisanih bo tudi njenih %{count} sopomenk." - rename_tag: "Preimenuj oznako" - rename_instructions: "Izberite novo ime za oznako:" + description: "Opis" sort_by: "Uredi po:" sort_by_count: "številu" sort_by_name: "imenu" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 0a7f3ac87a..5ec2d1bb6e 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -1887,8 +1887,7 @@ sq: tags: "Etiketat" add_synonyms: "Shto" delete_tag: "Fshi etiketën" - rename_tag: "Riemëro etiketën" - rename_instructions: "Zgjidhni një emër të ri për këtë etiketë" + description: "Përshkrimi" sort_by: "Rendit sipas:" sort_by_count: "numri" sort_by_name: "emër" diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml index 90d47727d8..37e780ad9a 100644 --- a/config/locales/client.sr.yml +++ b/config/locales/client.sr.yml @@ -1762,6 +1762,7 @@ sr: download: "Preuzmi" tagging: add_synonyms: "Dodaj" + description: "Opis" sort_by_count: "broj" sort_by_name: "ime foruma" cancel_delete_unused: "Odustani" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 80df55cc26..5d4de10e87 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -2178,6 +2178,8 @@ sv: in_posts_by: "i inlägg av %{username}" browser_tip: "%{modifier} + f" browser_tip_description: "igen för att använda inbyggd webbläsarsökning" + recent: "Senaste sökningar" + clear_recent: "Rensa senaste sökningar" type: default: "Ämnen/inlägg" users: "Användare" @@ -3223,8 +3225,8 @@ sv: help: "Detta ämne är ett personligt meddelande" posts: "Inlägg" pending_posts: - label: "Väntar" - label_with_count: "Pågående (%{count})" + label: "Väntande" + label_with_count: "Väntande (%{count})" posts_likes_MF: | Detta ämne har {count, plural, one {# svar} other {# svar}} {ratio, select, low {med ett högt förhållande mellan gillningar/inlägg} @@ -3488,17 +3490,19 @@ sv: one: "välj minst %{count} tagg..." other: "välj minst %{count} taggar..." info: "Info" - default_info: "Denna tagg är inte begränsad till några kategorier, och har inga synonymer. Om du vill lägga till begränsningar lägger du denna tagg i en taggrupp." + default_info: "Denna tagg är inte begränsad till någon kategori, och har inga synonymer." + staff_info: "För att lägga till begränsningar, placera denna tagg i en tagggrupp." category_restricted: "Denna tagg är begränsad till kategorier som du inte har tillträde till." synonyms: "Synonymer" synonyms_description: "När följande taggar används, kommer de att ersättas med %{base_tag_name}." + save: "Spara namn och beskrivning av taggen" tag_groups_info: one: 'Denna tagg tillhör gruppen "%{tag_groups}".' other: "Denna tagg tillhör dessa grupper: %{tag_groups}." category_restrictions: one: "Den kan enbart användas i denna kategori:" other: "Den kan enbart användas i dessa kategorier:" - edit_synonyms: "Hantera synonymer" + edit_synonyms: "Redigera synonymer" add_synonyms_label: "Lägg till synonymer:" add_synonyms: "Lägg till" add_synonyms_explanation: @@ -3515,8 +3519,8 @@ sv: delete_confirm_synonyms: one: "Dess synonym kommer också att raderas." other: "Dess %{count} synonymer kommer också att raderas." - rename_tag: "Döp om taggen" - rename_instructions: "Välj ett nytt namn för taggen:" + edit_tag: "Redigera taggnamn och beskrivning" + description: "Beskrivning" sort_by: "Sortera efter:" sort_by_count: "summa" sort_by_name: "namn" diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml index 322bcc7725..a4f41ea3e4 100644 --- a/config/locales/client.sw.yml +++ b/config/locales/client.sw.yml @@ -2214,8 +2214,7 @@ sw: add_synonyms: "Ongeza" delete_tag: "futa lebo" delete_confirm_no_topics: "Una uhakika unataka kufuta lebo hii?" - rename_tag: "Badili jina la lebo" - rename_instructions: "Chagua jina jipya la lebo" + description: "Maelezo" sort_by: "Pangilia kwa:" sort_by_count: "hesabu" sort_by_name: "jina" diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index f2f0a85f55..434bc1dfce 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -1325,6 +1325,7 @@ te: tagging: tags: "ట్యాగులు" add_synonyms: "కలుపు" + description: "వివరణ" sort_by_name: "పేరు" cancel_delete_unused: "రద్దుచేయి" filters: diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index e86c270e86..93c311ca88 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -2374,8 +2374,7 @@ th: add_synonyms: "เพิ่ม" delete_tag: "ลบแท็ก" delete_confirm_no_topics: "คุณแน่ใจหรือว่าต้องการลบแท็กนี้" - rename_tag: "เปลี่ยนชื่อแท็ก" - rename_instructions: "เลือกชื่อใหม่สำหรับแท็ก:" + description: "รายละเอียด" sort_by: "เรียงโดย:" sort_by_count: "นับ" sort_by_name: "ชื่อ" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index daf981f8df..7e3226fc30 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -3464,7 +3464,7 @@ tr_TR: tags: "Etiketler" choose_for_topic: "opsiyonel etiketler" info: "Bilgi" - default_info: "Bu etiket herhangi bir kategoriyle sınırlı değil ve eş anlamlısı yok. Kısıtlama eklemek için bu etiketi etiket grubuna koyun." + default_info: "Bu etiket hiçbir kategoriyle sınırlandırılmamıştır, ve eş anlamlısı yoktur." category_restricted: "Bu etiket, erişim izniniz olmayan kategorilerle sınırlıdır." synonyms: "Eş Anlamlılar" synonyms_description: "İlgili etiketler kullanılıyorsa, %{base_tag_name} ile değiştirilecektir." @@ -3474,7 +3474,6 @@ tr_TR: category_restrictions: one: "Sadece bu kategoride kullanılabilir:" other: "Sadece bu kategorilerde kullanılabilir:" - edit_synonyms: "Eş anlamlıları Yönet" add_synonyms_label: "Eş anlamlılarını ekle:" add_synonyms: "Ekle" add_synonyms_explanation: @@ -3491,8 +3490,7 @@ tr_TR: delete_confirm_synonyms: one: "Eş anlamlısı da silinecek." other: "%{count} tane eşanlamlısı da silinecek." - rename_tag: "Etiketi Yeniden Adlandır" - rename_instructions: "Bu etiket için yeni bir ad seç:" + description: "Açıklama" sort_by: "Sırala:" sort_by_count: "say" sort_by_name: "ad" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index c04a7289e7..6704225c17 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -3752,7 +3752,7 @@ uk: tags: "Мітки" choose_for_topic: "необов'язкові мітки" info: "Інформація" - default_info: "Цей тег не використовується в жодній категорії та не має синонімів. Щоб використовувати цей тег, розмістіть його у групі ." + default_info: "Цей тег не обмежений розділами та не має синонімів." category_restricted: "Цей тег обмежений категоріями, на які ви не маєте дозволу на доступ." synonyms: "Синоніми" synonyms_description: "Коли будуть використані наступні теги, вони будуть замінені на %{base_tag_name} ." @@ -3766,7 +3766,6 @@ uk: few: "Його можна використовувати лише в таких категоріях:" many: "Його можна використовувати лише в таких категоріях:" other: "Його можна використовувати лише в таких розділах:" - edit_synonyms: "Управління синонімами" add_synonyms_label: "Додайте синоніми:" add_synonyms: "Додати" add_synonyms_explanation: @@ -3789,8 +3788,7 @@ uk: few: "Синоніми %{count} також будуть видалені." many: "Синоніми %{count} також будуть видалені." other: "Синоніми %{count} також будуть видалені." - rename_tag: "Перейменувати мітку" - rename_instructions: "Виберіть нову назву для мітки:" + description: "Опис" sort_by: "Сортувати за:" sort_by_count: "Кількість" sort_by_name: "ім'я" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index a89625b0d0..244641eb15 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -2982,8 +2982,7 @@ ur: one: "کیا آپ واقعی اس ٹیگ کو حذف اور %{count} ٹاپک، جس کو یہ آسائین ہواوا ہے، سے ہٹا دینا چاہتے ہیں؟" other: "کیا آپ واقعی اس ٹیگ کو حذف اور %{count} ٹاپکس، جن کو یہ آسائین ہواوا ہے، سے ہٹا دینا چاہتے ہیں؟" delete_confirm_no_topics: "کیا آپ واقعی اِس ٹَیگ کو حذف کرنا چاہتے ہیں؟" - rename_tag: "ٹیگ کا نام تبدیل کریں" - rename_instructions: "ٹیگ کے لیے نئے نام کا انتخاب کریں:" + description: "تفصیل" sort_by: "ترتیب بہ:" sort_by_count: "شمار" sort_by_name: "نام" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index ed5a823884..57878a2693 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -3103,6 +3103,7 @@ vi: tags: "Thẻ" choose_for_topic: "thẻ không bắt buộc" info: "Thông tin" + default_info: "Thẻ này không bị giới hạn đối với bất kỳ danh mục nào và không có từ đồng nghĩa." category_restricted: "Thẻ này bị hạn chế đối với các danh mục bạn không có quyền truy cập." synonyms: "Từ đồng nghĩa" synonyms_description: "Khi các thẻ sau được sử dụng, chúng sẽ được thay thế bằng %{base_tag_name}." @@ -3110,7 +3111,6 @@ vi: other: "Thẻ này thuộc các nhóm sau: %{tag_groups}." category_restrictions: other: "Nó chỉ có thể được sử dụng trong các danh mục sau:" - edit_synonyms: "Quản lý từ đồng nghĩa" add_synonyms_label: "Thêm từ đồng nghĩa:" add_synonyms: "Thêm" add_synonyms_explanation: @@ -3124,8 +3124,7 @@ vi: delete_confirm_no_topics: "Bạn có chắc chắn muốn xóa thẻ này không?" delete_confirm_synonyms: other: "%{count} từ đồng nghĩa của nó cũng sẽ bị xóa." - rename_tag: "Đổi tên thẻ" - rename_instructions: "Chọn tên mới cho thẻ:" + description: "Mô tả" sort_by: "Xếp theo:" sort_by_count: "đếm" sort_by_name: "tên" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 834012e8c5..948d9a29de 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -3320,7 +3320,7 @@ zh_CN: choose_for_topic_required: other: "您必须至少选择 %{count} 个标签。" info: "信息" - default_info: "此标签不限于任何类别,也没有同义词。要添加限制,请将此标签置于标签组中。" + default_info: "该标签不限于任何类别,并且没有同义词。" category_restricted: "此标签仅限于您无权访问的类别。" synonyms: "同义词" synonyms_description: "使用以下标签时,它们将被替换为 %{base_tag_name}。" @@ -3328,7 +3328,6 @@ zh_CN: other: "此标签属于以下组:%{tag_groups}。" category_restrictions: other: "它只能用于以下类别:" - edit_synonyms: "管理同义词" add_synonyms_label: "添加同义词:" add_synonyms: "添加" add_synonyms_explanation: @@ -3342,8 +3341,7 @@ zh_CN: delete_confirm_no_topics: "确定要删除此标签吗?" delete_confirm_synonyms: other: "其 %{count} 个同义词也将被删除。" - rename_tag: "重命名标签" - rename_instructions: "为标签选择一个新名称:" + description: "描述" sort_by: "排序依据:" sort_by_count: "计数" sort_by_name: "名称" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 07547dc4cc..65cb437cae 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -2677,8 +2677,7 @@ zh_TW: delete_confirm: other: "您確定要刪除此標籤並將它從%{count}個話題中移除嗎?" delete_confirm_no_topics: "您是否確定要刪除這個標籤?" - rename_tag: "重命名標籤" - rename_instructions: "標籤的新名稱:" + description: "描述" sort_by: "排序方式:" sort_by_count: "總數" sort_by_name: "名稱" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 883158fbca..9193a1d7e0 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -1530,6 +1530,18 @@ ar: ignores_count: عدد التجاهلات mutes_count: عدد مرات الكتم description: "المستخدمون الذين تم كتمهم أو تجاهلهم بواسطة العديد من المستخدمين الآخرين" + top_users_by_likes_received: + labels: + user: المستخدم + qtt_like: الإعجابات المتلقاة + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: المستخدم + qtt_like: الإعجابات المتلقاة + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: المستخدم + qtt_like: الإعجابات المتلقاة dashboard: rails_env_warning: "الخادم يعمل في وضع %{env}." host_names_warning: "يستخدم ملف config/database.yml اسم المضيف الافتراضي localhost. حدِّثه لاستخدام اسم مضيف موقعك." diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index 0f197c2d86..1dc35fda76 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -878,6 +878,18 @@ be: ignores_count: ігнаруе падлік mutes_count: колькасць Mutes description: "Карыстальнікі, якія былі прыглушаны і" + top_users_by_likes_received: + labels: + user: Карыстальнік + qtt_like: атрыманыя перавагі + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Карыстальнік + qtt_like: атрыманыя перавагі + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Карыстальнік + qtt_like: атрыманыя перавагі dashboard: rails_env_warning: "Сервер працуе ў рэжыме %{env}." site_settings: diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index 4e35aae1f1..f7e9bedf54 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -638,6 +638,18 @@ bg: top_uploads: labels: filename: Име на файла + top_users_by_likes_received: + labels: + user: Потребител + qtt_like: Получени харесвания + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Потребител + qtt_like: Получени харесвания + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Потребител + qtt_like: Получени харесвания dashboard: rails_env_warning: "Вашият сървър е в % {env} режим." host_names_warning: "Вашият config/database.yml файл използва по подразбиране localhost за hostname. Обновете, за да използва името на вашия сайт за hostname. " diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 397605c6ee..8c815798c9 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -587,6 +587,18 @@ bs_BA: top_uploads: labels: filename: Filename + top_users_by_likes_received: + labels: + user: User + qtt_like: Likes Received + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: User + qtt_like: Likes Received + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: User + qtt_like: Likes Received dashboard: rails_env_warning: "Your server is running in %{env} mode." host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname." diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index c5c57a1bb7..670445668c 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -1151,6 +1151,18 @@ ca: ignores_count: Recompte d'ignorats mutes_count: Recompte de silenciats description: "Usuaris que han estat silenciats i/o ignorats per molts altres usuaris." + top_users_by_likes_received: + labels: + user: Usuari + qtt_like: '''M''agrada'' rebuts' + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Usuari + qtt_like: '''M''agrada'' rebuts' + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Usuari + qtt_like: '''M''agrada'' rebuts' dashboard: rails_env_warning: "El vostre servidor s'executa en mode %{env}." host_names_warning: "El vostre fitxer config/database.yml empra el nom d'amfitrió local per defecte. Actualitzeu-lo per a fer servir el nom del vostre lloc web." diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 983a196dc3..17088b0b62 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -813,6 +813,18 @@ cs: top_uploads: labels: filename: Název souboru + top_users_by_likes_received: + labels: + user: Uživatel + qtt_like: Obdržených 'líbí se' + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Uživatel + qtt_like: Obdržených 'líbí se' + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Uživatel + qtt_like: Obdržených 'líbí se' dashboard: rails_env_warning: "Váš server běží v módu %{env}." host_names_warning: "Vaše konfigurace v souboru config/database.yml používá 'localhost' jako jméno hostitele. Změňte toto nastavení na doménu vašeho webu." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index d0cf46f059..80f2d35ef4 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -1273,6 +1273,18 @@ da: ignored_user: Ignoreret Bruger mutes_count: Tavsheds tæller description: "Brugere, der er gjort tavse og/eller ignoreret af mange andre brugere." + top_users_by_likes_received: + labels: + user: Bruger + qtt_like: Likes modtaget + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Bruger + qtt_like: Likes modtaget + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Bruger + qtt_like: Likes modtaget dashboard: rails_env_warning: "Din server kører i %{env}-tilstand." host_names_warning: "Din config/database.yml fil bruger standardnavnet localhost som værtsnavn. Opdatér den med webstedets værtsnavn." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index df0e1619ac..68c191a1ca 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -206,6 +206,8 @@ de: local_login_cannot_be_disabled_if_second_factor_enforced: "Du kannst die lokale Anmeldung nicht deaktivieren, wenn 2FA erzwungen wird. Deaktiviere die erzwungene 2FA vor dem Deaktivieren lokaler Anmeldungen." cannot_enable_s3_uploads_when_s3_enabled_globally: "Du kannst S3-Uploads nicht aktivieren, weil S3-Uploads bereits global aktiviert sind. Aktivieren auf Website-Ebene kann zu kritischen Problemen mit Uploads führen" cors_origins_should_not_have_trailing_slash: "Du solltest den abschließenden Schrägstrich (/) nicht zu CORS-Ursprüngen hinzufügen." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "Benutzeragenten müssen mindestens 3 Zeichen lang sein, um zu verhindern, dass menschliche Benutzer falsch eingeschätzt werden." + slow_down_crawler_user_agent_cannot_be_popular_browsers: "Du kannst keinen der folgenden Werte zu der Einstellung hinzufügen: %{values}." conflicting_google_user_id: 'Die Google-Konto-ID für dieses Konto hat sich geändert; das Einschreiten des Teams ist aus Sicherheitsgründen erforderlich. Bitte kontaktiere das Team und verweise es auf
    https://meta.discourse.org/t/76575' onebox: invalid_address: "Leider konnten wir keine Vorschau für diese Webseite erstellen, da der Server „%{hostname}“ nicht gefunden werden konnte. Statt einer Vorschau erscheint nur ein Link in deinem Beitrag. :cry:" @@ -896,6 +898,7 @@ de: others: "Keine Lesezeichen." no_drafts: self: "Du hast keine Entwürfe; verfasse eine Antwort in einem beliebigen Thema und sie wird automatisch als neuer Entwurf gespeichert." + no_log_search_queries: "Suchprotokollabfragen sind derzeit deaktiviert (ein Administrator kann sie in den Website-Einstellungen aktivieren)." email_settings: pop3_authentication_error: "Es gab ein Problem mit den bereitgestellten POP3-Anmeldedaten, überprüfe bitte den Benutzernamen und das Passwort und versuche es erneut." imap_authentication_error: "Es gab ein Problem mit den bereitgestellten IMAP-Anmeldedaten, überprüfe bitte den Benutzernamen und das Passwort und versuche es erneut." @@ -1335,6 +1338,25 @@ de: ignores_count: Anzahl ignoriert mutes_count: Anzahl stummgeschaltet description: "Benutzer, die von vielen anderen Benutzern stummgeschaltet und/oder ignoriert wurden." + top_users_by_likes_received: + title: "Top-Benutzer nach erhaltenen Likes" + labels: + user: Benutzer + qtt_like: Erhaltene „Gefällt mir“ + description: "Top 10 Benutzer, die mehr Likes erhalten haben." + top_users_by_likes_received_from_inferior_trust_level: + title: "Top-Benutzer nach Likes, die von einem Benutzer mit einer niedrigeren Vertrauensstufe erhalten wurden" + labels: + user: Benutzer + trust_level: Vertrauensstufe + qtt_like: Erhaltene „Gefällt mir“ + description: "Top 10 Benutzer mit einer höheren Vertrauensstufe, die von Personen mit einer niedrigeren Vertrauensstufe gemocht werden." + top_users_by_likes_received_from_a_variety_of_people: + title: "Top-Benutzer nach Likes, die von einer Vielzahl von Personen erhalten wurden" + labels: + user: Benutzer + qtt_like: Erhaltene „Gefällt mir“ + description: "Top 10 Benutzer, die von einer Vielzahl von Leuten Likes bekommen haben." dashboard: rails_env_warning: "Dein Server läuft im %{env}-Modus." host_names_warning: "Deine config/database.yml-Datei verwendet localhost als Hostnamen. Trage hier den Hostnamen deiner Website ein." @@ -1515,6 +1537,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_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" diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index d5c837eb4e..e88241fc79 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -887,6 +887,18 @@ el: top_uploads: labels: filename: Όνομα αρχείου + top_users_by_likes_received: + labels: + user: Χρήστης + qtt_like: Μου αρέσει που λήφθησαν + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Χρήστης + qtt_like: Μου αρέσει που λήφθησαν + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Χρήστης + qtt_like: Μου αρέσει που λήφθησαν dashboard: rails_env_warning: "Ο διακομιστής σας τρέχει στη %{env} λειτουργία." host_names_warning: "Το αρχείο config/database.yml χρησιμοποιεί ως hostname το localhost . Ενημερώστε το, ώστε να χρησιμοποεί το όνομα του site σας." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index ed45365d58..c38e51f6d9 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -1333,6 +1333,18 @@ es: ignores_count: Número de ignorados mutes_count: Número de silenciados description: "Usuarios que han sido silenciados y/o ignorados por muchos otros usuarios." + top_users_by_likes_received: + labels: + user: Usuario + qtt_like: Me gusta recibidos + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Usuario + qtt_like: Me gusta recibidos + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Usuario + qtt_like: Me gusta recibidos dashboard: 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." diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml index 866fd68aed..5e611c6ff2 100644 --- a/config/locales/server.et.yml +++ b/config/locales/server.et.yml @@ -728,6 +728,18 @@ et: labels: filename: Faili nimi author: Autor + top_users_by_likes_received: + labels: + user: Kasutaja + qtt_like: Meeldimisi saadud + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Kasutaja + qtt_like: Meeldimisi saadud + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Kasutaja + qtt_like: Meeldimisi saadud dashboard: host_names_warning: "Sinu config/database.yml fail kasutab vaikimisi hostinime localhost. Kirjuta sinna oma saidi hostinimi." site_settings: diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index c9210b1285..c146fb71b0 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -847,6 +847,18 @@ fa_IR: top_uploads: labels: filename: نام پرونده + top_users_by_likes_received: + labels: + user: کاربر + qtt_like: پسند‌های دریافتی + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: کاربر + qtt_like: پسند‌های دریافتی + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: کاربر + qtt_like: پسند‌های دریافتی dashboard: rails_env_warning: "سرور شما در حالت %{env} در حال اجراست" host_names_warning: "config/database.yml فایل شما از اسم هاست پیش فرض localhost استفاده کرده. برای استفاده از نام سایتتان این قسمت را به‌روز کنید." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 542c2fb9e7..c5a5d79a8e 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -1325,6 +1325,18 @@ fi: ignores_count: Estojen määrä mutes_count: Vaimennusten määrä description: "Käyttäjät, jotka monet muista on vaimentaneet tai estäneet." + top_users_by_likes_received: + labels: + user: Käyttäjä + qtt_like: Saatuja tykkäyksiä + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Käyttäjä + qtt_like: Saatuja tykkäyksiä + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Käyttäjä + qtt_like: Saatuja tykkäyksiä dashboard: rails_env_warning: "Palvelintasi ajetaan tilassa %{env}." host_names_warning: "Sivuston config/database.yml tiedosto käyttää oletusarvoista localhost-isäntänimeä. Päivitä se käyttämään sivuston isäntänimeä." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index e193ac75d1..4525cf33c9 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -1325,6 +1325,18 @@ fr: ignores_count: Ignorés mutes_count: Mis en sourdine description: "Les utilisateurs qui ont été mis en sourdine ou ignorés par de nombreux autres utilisateurs." + top_users_by_likes_received: + labels: + user: Utilisateur + qtt_like: '« J''aime » reçus' + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Utilisateur + qtt_like: '« J''aime » reçus' + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Utilisateur + qtt_like: '« J''aime » reçus' dashboard: rails_env_warning: "Votre serveur fonctionne dans l'environnement de %{env}." host_names_warning: "Votre fichier config/database.yml utilise le nom d'hôte par défaut. Veuillez renseigner votre nom d'hôte." diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml index fb528e9a16..59fc8d44bd 100644 --- a/config/locales/server.gl.yml +++ b/config/locales/server.gl.yml @@ -1280,6 +1280,18 @@ gl: ignores_count: Número de ignorados mutes_count: Número de silenciados description: "Usuarios que foron silenciados e/ou ignorados por moitos outros usuarios." + top_users_by_likes_received: + labels: + user: Usuario + qtt_like: Gústames recibidos + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Usuario + qtt_like: Gústames recibidos + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Usuario + qtt_like: Gústames recibidos dashboard: rails_env_warning: "O seu servidor está a executarse en modo %{env}." host_names_warning: "O ficheiro config/database.yml está a usar o nome do servidor predeterminado. Actualíceo para utilizar o nome do servidor do seu sitio." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 50e29a5786..8aafe3b38e 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -67,6 +67,7 @@ he: about_json_values: "about.json מכיל ערכים שגויים: %{errors}" modifier_values: "המחליפים ב־about.json מכילים ערכים שגויים: %{errors}" git: "שגיאה בעת שכפול מאגר git, הגישה נדחיתה או שהמאגר לא נמצא" + git_ref_not_found: "לא ניתן למשוך (checkout) הפניית git:‏ %{ref}" unpack_failed: "חילוץ הקובץ נכשל" file_too_big: "הקובץ גדול מדי ללא דחיסה." unknown_file_type: "הקובץ שהעלית הוא כפי הנראה אינה ערכת עיצוב תקנית של Discourse." @@ -219,6 +220,8 @@ he: local_login_cannot_be_disabled_if_second_factor_enforced: "אי אפשר להשבית כניסה מקומית אם נאכף אימות דו־שלבי. יש להשבית את אכיפת האימות הדו־שלבי בטרם השבתת כניסה מקומית." cannot_enable_s3_uploads_when_s3_enabled_globally: "לא ניתן להפעיל העלאות ל־S3 כיוון שהעלאות ל־S3 כבר פעילות באופן גלובלי והפעלת האפשרות הזאת ברמת האתר עשויה להוביל לתקלות חמורות בהעלאה." cors_origins_should_not_have_trailing_slash: "אין להוסיף לוכסן (/) לסוף מקורות ה־CORS." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "סוכני משתמש חייבים להיות באורך של 3 תווים לפחות כדי למנוע הגבלת שגויה של כמות פניות ממשתמשים אנושיים." + slow_down_crawler_user_agent_cannot_be_popular_browsers: "אי אפשר להוסיף אף אחת מהערכים הבאים להגדרה: %{values}." conflicting_google_user_id: 'מזהה חשבון ה־Google לחשבון זה השתנה, התערבות של חבר סגל נדרשת מטעמי אבטחה. נא ליצור קשר עם אחד מחברי הסגל ולהפנות אותו אל
    https://meta.discourse.org/t/76575' onebox: invalid_address: "מחילה אך לא הצלחנו לייצר תצוגה מקדימה לאתר הזה, כיוון שהשרת ‚%{hostname}’ לא נמצא. במקום תצוגה מקדימה יופיע קישור בלבד בפוסט שלך. :cry:" @@ -696,6 +699,9 @@ he: two: "%{count} לייקים" many: "%{count} לייקים" other: "%{count} לייקים" + cannot_permanently_delete: + many_posts: "אין אפשרות למחוק נושא זה לצמיתות כיוון שיש פוסטים אחרים." + wait_or_different_admin: "עליך להמתין %{time_left} בטרם מחיקת הפוסט הזה לצמיתות או שעל מנהל או מנהלת אחרים לעשות זאת." rate_limiter: slow_down: "ביצעת את הפעולה הזאת יותר מדי פעמים, נא לנסות שוב מאוחר יותר." too_many_requests: "ביצעת את הפעולה הזאת יותר מדי פעמים. נא להמתין %{time_left} בטרם ביצוע ניסיון חוזר." @@ -1414,6 +1420,25 @@ he: ignores_count: מניין בהתעלמות mutes_count: מניין השתקות description: "משתמשים שהושתקו או הועברו להתעלמות על ידי מגוון משתמשים אחרים." + top_users_by_likes_received: + title: "משתמשים מובילים לפי לייקים שהתקבלו" + labels: + user: משתמש + qtt_like: לייקים שהתקבלו + description: "10 המשתמשים המובילים שקיבלו יותר לייקים." + top_users_by_likes_received_from_inferior_trust_level: + title: "משתמשים מובילים לפי לייקים שהתקבלו ממשתמש בדרגת אמון נמוכה יותר" + labels: + user: משתמש + trust_level: דרגת אמון + qtt_like: לייקים שהתקבלו + description: "10 המשתמשים המובילים בדרגת אמון גבוהה יותר שקיבלו לייקים מאנשים בדרגת אמון נמוכה יותר." + top_users_by_likes_received_from_a_variety_of_people: + title: "משתמשים מובילים לפי לייקים שהתקבלו ממגוון אנשים" + labels: + user: משתמש + qtt_like: לייקים שהתקבלו + description: "10 המשתמשים המובילים שקיבלו לייקים ממגוון רחב של אנשים." dashboard: rails_env_warning: "השרת שלך רץ במצב %{env}." host_names_warning: "הקובץ config/database.yml אצלכם משתמש בכתובת ברירת המחדל - localhost. עדכנו אותה להשתמש בכתובת של השרת שלכם." @@ -1494,6 +1519,7 @@ he: tl2_post_edit_time_limit: "מחברים בדרגת אמון 2 ומעלה יכולים לערוך את הפוסטים שלהם (n) דקות לאחר פרסומם. 0 ישבית את ההגבלה הזאת." edit_history_visible_to_public: "לאפשר לכולם לראות גרסאות קודמות של פוסט ערוך. כאשר אפשרות זו מושבתת, רק חברי סגל יכולים לצפות בהן." delete_removed_posts_after: "פוסטים שהוסרו על ידי מחבריהם ימחקו באופן אוטומטי לאחר (n) שעות. אם הגדרה זו מכוונת ל-0, פוסטים ימחקו מיידית." + notify_users_after_responses_deleted_on_flagged_post: "כאשר פוסט מסומן ואז מסירים אותו, כל המשתמשים שהגיבו לפוסט והתגובות שלהם הוסרו יקבלו התראה." max_image_width: "הרוחב המקסימלי של תצוגת תמונה מוקטנת בפוסט" max_image_height: "גובה מקסימלי של תצוגת תמונה מוקטנת בפוסט" responsive_post_image_sizes: "שינוי גודל תצוגה מקדימה של תמונות בתיבה קלה כדי לאפשר הצגה במסכים ברמת אבחנה גבוהה עם יחסי הפיקסלים הבאים. להסיר את כל הערכים כדי לבטל תמונות גמישות." @@ -1593,6 +1619,7 @@ he: allowed_iframes: "רשימה של קידומות שמות מתחם בתור מקור של iframe שאפשר לאפשר בבטחה בפוסטים ב־Discourse" allowed_crawler_user_agents: "סוכני משתמשים של סורקי רשת שמורשים לגשת לאתר. אזהרה! שינוי הגדרה זו תחסום את כל הסורקים שאינם מופיעים כאן!" blocked_crawler_user_agents: "מילה ייחודית שאינה תלוית רישיות במחרוזת סוכן המשתמש (user agent) שמגדירה לאילו סורקי רשת אין הרשאת גישה לאתר. לא חל במקרה שהגדרת רשימת היתר." + 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 (מדיניות אבטחת תוכן בדיווח בלבד)" @@ -2458,6 +2485,11 @@ he: email_in_spam_header: "כתובת הדוא״ל הראשונה של המשתמש סומנה כזבל" already_silenced: "המשתמש כבר הושתק על ידי %{staff} %{time_ago}." already_suspended: "המשתמש כבר הושעה על ידי %{staff} %{time_ago}." + cannot_delete_has_posts: + one: "למשתמש %{username} יש פוסט %{count} בנושא ציבורי או הודעה פרטית, לכן לא ניתן למחוק אותו." + two: "למשתמש %{username} יש %{count} פוסטים בנושא ציבורי או הודעה פרטית, לכן לא ניתן למחוק אותו." + many: "למשתמש %{username} יש %{count} פוסטים בנושא ציבורי או הודעה פרטית, לכן לא ניתן למחוק אותו." + other: "למשתמש %{username} יש %{count} פוסטים בנושא ציבורי או הודעה פרטית, לכן לא ניתן למחוק אותו." reviewables_reminder: submitted: one: "הוגשו פריטים לפני למעלה משעה (%{count}). [נא לסקור אותם](%{base_path}/review)." @@ -2578,6 +2610,21 @@ he: test_mailer: title: "שולח-מיילים לבדיקה" subject_template: "[%{email_prefix}] הודעה לבדיקת יכולות שליחה" + text_body_template: | + זאת הודעת בדיקת דוא״ל מאת + + [**%{base_url}**][0] + + אנו מקווים שבדיקת עבירות הדוא״ל הזאת צלחה! + + להלן [רשימת גורמים לאימות הגדרות עבירות דוא״ל][1]. + + בהצלחה, + + חבריך ב־[Discourse](https://www.discourse.org) + + [0]: %{base_url} + [1]: https://meta.discourse.org/t/email-delivery-configuration-checklist/209839 new_version_mailer: title: "שולח-מיילים של גרסה חדשה" subject_template: "[%{email_prefix}] גרסת Discourse חדשה, עדכון זמין." @@ -2615,6 +2662,11 @@ he: inappropriate: "התגובה שלך סומנה כ**לא מתאימה**: הקהילה מרגישה שהיא פוגענית או הפרה של [הנחיות הקהילה](%{base_path}/guidelines)" spam: "הפוסט שלך סומן כ**ספאם**: הקהילה מרגישה שזה פרסומת, דבר שהוא קידום מכירות באופיו במקום להיות שימושי או רלוונטי לנושא. " notify_moderators: "הפוסט שלך סומן בדגל **לתשומת לב מפקח**: הקהילה מרגישה שמשהו בפוסט דורש התערבות ידנית של צוות האתר. " + responder: + off_topic: "הפוסט סומן כ **מחוץ לנושא**: הקהילה מרגישה שהוא לא מתאים לנושא, כפי שמוגדר על ידי הכותרת והפוסט הראשון." + inappropriate: "התגובה סומנה כ**לא מתאימה**: הקהילה מרגישה שהיא פוגענית או מפרה את [הנחיות הקהילה](%{base_path}/guidelines)" + spam: "הפוסט סומן כ**ספאם**: הקהילה מרגישה שזו פרסומת, איזשהו קידום מכירות במקום להיות שימושי או תואם לנושא." + notify_moderators: "הפוסט סומן ב**לתשומת לב מפקח**: הקהילה מרגישה שמשהו בפוסט דורש התערבות ידנית של סגל האתר." flags_dispositions: agreed: "תודה שעדכנת אותנו. אנחנו מסכימים שיש כאן בעיה ואנחנו מנסים לבדוק את העניין." agreed_and_deleted: "תודה שעדכנת אותנו. אנחנו מסכימים שישנה בעיה והסרנו את הפוסט." @@ -2702,6 +2754,23 @@ he: ``` נא לקרוא בעיון את [הנחיות הקהילה](%{base_url}/guidelines) שלנו לפרטים נוספים. + flags_agreed_and_post_deleted_for_responders: + title: "התגובה הוסרה מהפוסט שסומן על ידי הסגל" + subject_template: "התגובה הוסרה מהפוסט שסומן על ידי הסגל" + text_body_template: | + שלום רב, + + זאת הודעה אוטומטית מהאתר %{site_name} כדי ליידע אותך ש[פוסט](%{base_url}%{url}) שהגבת אליו הוסר. + + %{flag_reason} + + הפוסט הזה סומן על ידי הקהילה ואחד או אחת מחברי הסגל בחרו להסיר אותו. + + ``` markdown + %{flagged_post_raw_content} + ``` + + למידע נוסף על סיבת ההסרה, כדאי לעיין ב[הנחיות הקהילה](%{base_url}/guidelines) שלנו. usage_tips: text_body_template: | לעצות זריזות איך כדאי להתחיל להשתמש במערכת, ניתן [לקרוא את הפוסט הזה בבלוג](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml index 84e4ba2506..243bf28689 100644 --- a/config/locales/server.hu.yml +++ b/config/locales/server.hu.yml @@ -150,6 +150,8 @@ hu: site_settings: default_categories_already_selected: "Nem választhat olyan kategóriát, amely már szerepel egy másik listában." s3_upload_bucket_is_required: "Nem lehet engedélyezni a feltöltést az S3-ba, amíg nincs megadva az „s3_upload_bucket”." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "A felhasználói ügynököknek legalább 3 karakteresnek kell lenniük, hogy elkerülje az emberi felhasználók téves korlátozását." + slow_down_crawler_user_agent_cannot_be_popular_browsers: "A következő értékek egyikét sem adhatja hozzá a beállításhoz: %{values}." onebox: invalid_address: "Sajnos nem tudtunk előnézetet generálni ehhez a weboldalhoz, mert a (z) „%{hostname}” szerver nem található. Az előnézet helyett csak egy link jelenik meg a bejegyzésben. :cry:" invite: @@ -860,6 +862,25 @@ hu: ignores_count: Letiltások száma mutes_count: Némítások száma description: "Olyan felhasználók, akiket sok más felhasználó némított el és / vagy tiltott le." + top_users_by_likes_received: + title: "Legnépszerűbb felhasználók a kapott kedvelések alapján" + labels: + user: Felhasználó + qtt_like: Kapott kedvelések + description: "A 10 legnépszerűbb felhasználó, akik a legtöbb kedvelést kapták." + top_users_by_likes_received_from_inferior_trust_level: + title: "Legnépszerűbb felhasználók az alacsonyabb szintű felhasználóktól kapott kedvelések alapján" + labels: + user: Felhasználó + trust_level: Bizalmi szint + qtt_like: Kapott kedvelések + description: "10 legnépszerűbb magasabb szintű felhasználó, akiket az alacsonyabb szintű felhasználók kedveltek." + top_users_by_likes_received_from_a_variety_of_people: + title: "Legnépszerűbb felhasználók a különböző emberektől kapott kedvelések alapján" + labels: + user: Felhasználó + qtt_like: Kapott kedvelések + description: "A 10 legnépszerűbb felhasználó, akiket sokféle ember kedvelt." dashboard: rails_env_warning: "A kiszolgáló jelenleg %{env} módban fut." host_names_warning: "A config/database.yml fájl az alapértelmezett „localhost” domaint használja. Állítsa be az oldala saját domain nevét." @@ -872,6 +893,7 @@ hu: 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." 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." 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ó." min_password_length: "Minimum jelszóhossz." diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml index 1de6ae0b72..1dfb88f9db 100644 --- a/config/locales/server.hy.yml +++ b/config/locales/server.hy.yml @@ -1026,6 +1026,18 @@ hy: ignored_user: Անտեսված Օգտատեր ignores_count: Անտեսումների քանակ mutes_count: Խլացվածների քանակը + top_users_by_likes_received: + labels: + user: Օգտատեր + qtt_like: Ստացած Հավանումները + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Օգտատեր + qtt_like: Ստացած Հավանումները + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Օգտատեր + qtt_like: Ստացած Հավանումները dashboard: rails_env_warning: "Ձեր սերվերը աշխատում է %{env} ռեժիմով:" host_names_warning: "Ձեր config/database.yml ֆայլը օգտագործում է լռելյայն localhost հոսթի անուն: Թարմացրեք այն Ձեր կայքի հոսթի անունով:" diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml index 2ad5654032..fec91c5ad6 100644 --- a/config/locales/server.id.yml +++ b/config/locales/server.id.yml @@ -548,6 +548,18 @@ id: labels: user: Pengguna location: Lokasi + top_users_by_likes_received: + labels: + user: Pengguna + qtt_like: Likes Diterima + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Pengguna + qtt_like: Likes Diterima + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Pengguna + qtt_like: Likes Diterima dashboard: rails_env_warning: "Server Anda berjalan dalam modus %{env}." host_names_warning: "Berkas config/database.yml Anda menggunakan nama host default localhost. Ubahlah untuk menggunakan nama host dari situs Anda." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index f4bee3a007..d9426a335b 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -1334,6 +1334,25 @@ it: ignores_count: Conteggio ignorati mutes_count: Conteggio silenziati description: "Utenti che sono stati silenziati e/o ignorati da molti altri utenti." + top_users_by_likes_received: + title: "Migliori utenti per Mi piace ricevuti" + labels: + user: Utente + qtt_like: Mi piace - Ricevuti + description: "I 10 migliori utenti che hanno ricevuto più Mi piace." + top_users_by_likes_received_from_inferior_trust_level: + title: "Migliori utenti per Mi piace ricevuti da un utente con un basso livello di attendibilità" + labels: + user: Utente + trust_level: Livello di attendibilità + qtt_like: Mi piace - Ricevuti + description: "I migliori 10 utenti con un livello di attendibilità superiore che hanno ricevuto Mi piace da persone con un livello di attendibilità inferiore." + top_users_by_likes_received_from_a_variety_of_people: + title: "I migliori utenti per Mi piace ricevuti da varie persone" + labels: + user: Utente + qtt_like: Mi piace - Ricevuti + description: "I migliori 10 utenti che hanno ricevuto Mi piace da una vasta gamma di persone." dashboard: rails_env_warning: "Il tuo server è in modalità %{env}." host_names_warning: "Il tuo file config/database.yml usa l'hostname di default: localhost. Aggiornalo con l'hostname del tuo sito." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index b154d21805..eef9f9d42d 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -1274,6 +1274,18 @@ ja: ignores_count: 無視数 mutes_count: ミュート数 description: "多数のユーザーによってミュートまたは無視されたユーザー。" + top_users_by_likes_received: + labels: + user: ユーザー + qtt_like: '「いいね!」された数' + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: ユーザー + qtt_like: '「いいね!」された数' + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: ユーザー + qtt_like: '「いいね!」された数' dashboard: rails_env_warning: "サーバーは %{env} モードで実行中です。" host_names_warning: "現在 config/database.yml ファイルは、デフォルトの localhost をホスト名として使用しています。あなたのサイトのホスト名に更新してください。" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 521d6bd4e7..f1f1a560f0 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -1283,6 +1283,18 @@ ko: ignores_count: 카운트 무시 mutes_count: 음소거 수 description: "다른 많은 사용자가 음소거하거나 무시한 사용자" + top_users_by_likes_received: + labels: + user: 사용자 + qtt_like: '''좋아요'' 받은 횟수' + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: 사용자 + qtt_like: '''좋아요'' 받은 횟수' + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: 사용자 + qtt_like: '''좋아요'' 받은 횟수' dashboard: rails_env_warning: "당신의 서버는 %{env} 모드에서 실행되고 있습니다." host_names_warning: "당신의 config/database.yml 파일은 기본 호스트네임을 사용하고 있습니다. 사이트의 호스트네임으로 업데이트 하세요." @@ -2008,6 +2020,7 @@ ko: invalid_css_color: "잘못된 색상입니다. 색상 이름 또는 16진수 값을 입력해야 합니다." invalid_email: "유효하지 않은 이메일 주소입니다." invalid_username: "해당 이름의 사용자가 없습니다." + valid_username: "해당 아이디의 사용자가 없습니다." invalid_group: "그 이름을 가진 그룹이 없습니다." invalid_integer_min_max: "%{min}이상 %{max}이하이어야 합니다." invalid_integer_min: "%{min}이상 이어야 합니다." diff --git a/config/locales/server.lt.yml b/config/locales/server.lt.yml index d46e5dfcd9..f5dd080126 100644 --- a/config/locales/server.lt.yml +++ b/config/locales/server.lt.yml @@ -58,21 +58,29 @@ lt: themes: bad_color_scheme: "Negalima atnaujinti temos, netinkama spalvų paletė" other_error: "Įvyko klaida atnaujinant temą" + ember_selector_error: "Atsiprašome, naudoti #ember arba .ember-view CSS parinkiklius neleidžiama, nes šie pavadinimai dinamiškai generuojami vykdymo metu ir laikui bėgant keisis, o tai galiausiai sugadins CSS. Išbandykite kitą parinkiklį." compile_error: unrecognized_extension: "Neatpažintas failo plėtinys: %{extension}" import_error: generic: Importuojant šią temą įvyko klaida + about_json: "Importo klaida: about.json neegzistuoja arba yra neteisingas. Ar tikrai tai Discourse tema?" about_json_values: "about.json yra neteisingos reikšmės: %{errors}" + modifier_values: "apie.json modifikatoriuose yra negaliojančių reikšmių: %{errors}" git: "Klaida git kapinyno klonavimui, prieiga atmetama arba saugykla nerasta" git_ref_not_found: "Nepavyko patikrinti „git“ nuorodos: %{ref}" + unpack_failed: "Nepavyko atidaryti failo" file_too_big: "Nesuspaustas failas yra per didelis." + unknown_file_type: "Jūsų įkeltas failas neatrodo kaip galiojanti discourse tema." + not_allowed_theme: "„%{repo}“ nėra leidžiamų temų sąraše (patikrinkite „allowed_theme_repos“ visuotinį nustatymą)." errors: component_no_user_selectable: "Tematikos komponentai negali būti pasirinktini vartotojui" component_no_default: "Tema komponentai negali būti numatytoji tema" + component_no_color_scheme: "Temos komponentai negali turėti spalvų palečių" no_multilevels_components: "Temos su vaikų temomis negali būti patys vaikų temos" optimized_link: Optimizuotos vaizdo nuorodos yra trumpalaikės ir neturėtų būti įtrauktos į temos šaltinio kodą. settings_errors: invalid_yaml: "Nurodytas YAML negalimas." + data_type_not_a_number: "“%{name}” tipo nustatymas nepalaikomas. Palaikomi tipai yra `integer`, `bool`, `list`, `enum` ir `upload`" name_too_long: "Yra nustatymas su per ilgu pavadinimu. Maksimalus ilgis yra 255" default_value_missing: "Nustatymas „%{name}“ neturi numatytosios vertės" default_not_match_type: "Nustatymas „%{name}“ numatytosios vertės tipas neatitinka nustatymo tipo." @@ -87,6 +95,7 @@ lt: string_value_not_valid_min: "Turi sudaryti mažiausia %{min} simbolių." string_value_not_valid_max: "Turi sudaryti daugiausia %{max} simbolių." locale_errors: + top_level_locale: "Aukščiausio lygio raktas lokalės faile turi atitikti lokalės pavadinimą" invalid_yaml: "Vertimas YAML negaliojantis" emails: incoming: @@ -99,7 +108,11 @@ lt: errors: empty_email_error: "Taip atsitinka, kai gaunamas neapdorotas paštas yra tuščias." no_message_id_error: "Taip atsitinka, kai laiške nėra antraštės \"Message-Id\"." + no_sender_detected_error: "Taip nutinka, kai antraštėje „Nuo“ nepavyko rasti tinkamo el. pašto adreso." + from_reply_by_address_error: "Taip atsitinka, kai antraštė Nuo atitinka atsakymą el. pašto adresu." + inactive_user_error: "Atsitinka, kai siuntėjas nėra aktyvus." silenced_user_error: "Atsitinka, kai siuntėjas buvo nutildytas." + bad_destination_address: "Taip nutinka, kai nė vienas el. pašto adresas laukuose To/Cc neatitiko sukonfigūruoto gaunamo el. pašto adreso." strangers_not_allowed_error: "Taip atsitinka, kai vartotojas bandė sukurti naują temą kategorijoje, kurioje jis nėra narys." insufficient_trust_level_error: "Taip atsitinka, kai vartotojas bandė sukurti naują temą kategorijoje, kuriai jis neturi reikiamo patikimumo lygio." reply_user_not_matching_error: "Taip atsitinka, kai gaunamas atsakymas iš kito el. pašto adreso, nei buvo išsiųstas pranešimas." @@ -108,11 +121,13 @@ lt: unsubscribe_not_allowed: "Tai atsitinka, kai šiam vartotojui neleidžiama atsisakyti prenumeratos el. paštu." email_not_allowed: "Tai atsitinka, kai el. pašto adreso nėra leidimų sąraše arba jis yra blokuojamame sąraše." unrecognized_error: "Nenustatyta klaida" + view_redacted_media: "Žiūrėti mediją" errors: &errors format: ! "%{attribute} %{message}" format_with_full_message: "%{attribute}:%{message}" messages: too_long_validation: "yra apribotas iki %{max} simbolių; įvedėte %{length}." + invalid_boolean: "Neteisinga loginė vertė." taken: "jau užimtas" accepted: privalo buti patvirtintas blank: negali būti tuščia @@ -142,6 +157,7 @@ lt: emojis_disabled: "negali turėti jaustukų" ip_address_already_screened: "jau įtraukta į esamą taisyklę" other_than: "turi būti kitoks nei %{count}" + auth_overrides_username: "Vartotojo vardas turi būti atnaujintas autentifikavimo teikėjo pusėje, nes “auth_overrides_username` nustatymas yra įjungtas." template: body: ! "Kilo problemų dėl šių laukų:" embed: @@ -150,9 +166,18 @@ lt: invalid_category_id: "Jūs nurodėte kategoriją, kuri neegzistuoja" default_categories_already_selected: "Negalite pasirinkti kitame sąraše naudojamos kategorijos." default_tags_already_selected: "Negalite pasirinkti žymos, naudojamos kitame sąraše." + s3_upload_bucket_is_required: "Negalite įgalinti įkėlimų į S3, jei nepateikėte „s3_upload_bucket“." + enable_s3_uploads_is_required: "Negalite įgalinti S3 inventoriaus naudojimo, nebent įjungėte S3 įkėlimus." + page_publishing_requirements: "Puslapio publikavimo įjungti negalima, jei įjungta saugi laikmena." + s3_backup_requires_s3_settings: "Negalite naudoti S3 kaip atsarginės vietos, nebent nurodėte „%{setting_name}“." secure_media_requirements: "Prieš įjungiant saugią laikmeną, turi būti įjungtas S3 įkėlimas." share_quote_facebook_requirements: "Jei norite įgalinti „Facebook“ citatų bendrinimą, turite nustatyti „Facebook“ programos ID." + second_factor_cannot_be_enforced_with_discourse_connect_enabled: "Negalite vykdyti 2FA, jei DiscoureConnect yra įjungtas." + local_login_cannot_be_disabled_if_second_factor_enforced: "Negalite išjungti vietinio prisijungimo, jei taikomas 2FA. Prieš išjungdami vietinius prisijungimus, išjunkite priverstinį 2FA." + cannot_enable_s3_uploads_when_s3_enabled_globally: "Negalite įgalinti S3 įkėlimų, nes S3 įkėlimai jau įgalinti visame pasaulyje, o įjungus šį svetainės lygį gali kilti kritinių įkėlimų problemų" cors_origins_should_not_have_trailing_slash: "Neturėtumėte pridėti galinio brūkšnio (/) prie CORS šaltinių." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "Vartotojų agentai turi būti ne trumpesni kaip 3 simboliai, kad būtų išvengta klaidingo greičio ribojimo žmonių vartotojams." + slow_down_crawler_user_agent_cannot_be_popular_browsers: "Prie nustatymo negalite pridėti nė vienos iš šių reikšmių: %{values}." onebox: invalid_address: "Deja, nepavyko sukurti šio tinklalapio peržiūros, nes nepavyko rasti serverio %{hostname}Vietoj peržiūros jūsų įraše bus rodoma tik nuoroda. :cry:" error_response: "Deja, nepavyko sukurti šio tinklalapio peržiūros, nes žiniatinklio serveris grąžino %{status_code}klaidos kodą. Vietoj peržiūros jūsų įraše bus rodoma tik nuoroda. :cry:" @@ -196,6 +221,7 @@ lt: provider_not_enabled: "Jums neleidžiama peržiūrėti prašomo šaltinio. Autentifikavimo teikėjas neįgalintas." provider_not_found: "Jums neleidžiama peržiūrėti prašomo šaltinio. Autentifikavimo teikėjas neegzistuoja." read_only_mode_enabled: "Svetainė veikia tik skaitymo režimu. Sąveika išjungta." + invalid_grant_badge_reason_link: "Išorinė arba netinkama discourse nuoroda neleidžiama dėl ženklelio priežasties" email_template_cant_be_modified: "Šis el. pašto šablonas negali būti pakeistas" not_in_group: title_topic: "Norėdami pamatyti šią temą, turite paprašyti narystės “%{group}” grupėje." @@ -251,6 +277,7 @@ lt: pm_reached_recipients_limit: "Deja, pranešime negali būti daugiau nei %{recipients_limit} gavėjų." removed_direct_reply_full_quotes: "Automatiškai pašalinta viso ankstesnio įrašo citata." watched_words_auto_tag: "Automatiškai pažymėta tema" + secure_upload_not_allowed_in_public_topic: "Atsiprašome, šis saugus įkėlimas (-ai) negali būti naudojamas viešoje temoje: %{upload_filenames}." slow_mode_enabled: "Ši tema veikia lėtu režimu." just_posted_that: "labai panašu į tai, ką parašėte prieš tai" invalid_characters: "turi neleistinų simbolių" @@ -293,6 +320,8 @@ lt: errors: already_bookmarked_post: "Negalite du kartus pažymėti to paties įrašo." cannot_set_past_reminder: "Negalite nustatyti žymių priminimo praeityje." + cannot_set_reminder_in_distant_future: "Negalite nustatyti žymės priminimo daugiau nei 10 metų ateityje." + time_must_be_provided: "turi būti numatytas laikas visiems priminimams" for_topic_must_use_first_post: "Norėdami pažymėti temą, galite naudoti tik pirmąjį įrašą." reminders: later_today: "Šiandien vėliau" @@ -461,6 +490,7 @@ lt: admin_quick_start_title: "PERSKAITYKITE PIRMIAUSIA: administratoriaus greitas pradžios vadovas" category: topic_prefix: "Viskas apie %{category} kategoriją" + post_template: "%{replace_paragraph}\n\nNaudokite šias pastraipas ilgesniam aprašymui arba kategorijų gairėms ar taisyklėms nustatyti:\n\n– Kodėl žmonės turėtų naudoti šią kategoriją? Kam tai?\n\n– Kuo tai skiriasi nuo kitų jau turimų kategorijų?\n\n– Kas apskritai turėtų būti šios kategorijos temose?\n\n– Ar mums reikia šios kategorijos? Ar galime sujungti su kita kategorija ar subkategorija?\n" errors: not_found: "Kategorijos nėra!" uncategorized_parent: "(Be kategorijos) kategorija negali turėti subkategorijų" @@ -469,6 +499,7 @@ lt: email_already_used_in_group: "„%{email}“ jau naudoja grupė „%{group_name}“." email_already_used_in_category: "„%{email}“ jau naudojamas kategorijoje „%{category_name}“." description_incomplete: "Kategorijos aprašymo įraše turi būti bent viena pastraipa." + permission_conflict: "Bet kuriai grupei, kuriai leidžiama patekti į subkategoriją, taip pat turi būti leidžiama patekti į pagrindinę kategoriją. Šios grupės turi prieigą prie vienos iš subkategorijų, tačiau neturi prieigos prie pagrindinės kategorijos: %{group_names}." disallowed_topic_tags: "Šioje temoje yra žymų, kurių neleidžia ši kategorija: „%{tags}“" disallowed_tags_generic: "Šioje temoje yra neleidžiamų žymų." cannot_delete: @@ -677,6 +708,7 @@ lt: please_continue: "Tęsti į %{site_name}" error: "Keičiant el. pašto adresą įvyko klaida. Galbūt adresas jau naudojamas?" doesnt_exist: "Šis el. pašto adresas nėra susietas su jūsų paskyra." + error_staged: "Keičiant el. pašto adresą įvyko klaida. Adresą jau naudoja suaktyvintas naudotojas." already_done: "Deja, ši patvirtinimo nuoroda nebegalioja. Galbūt jūsų el. pašto adresas jau buvo pakeistas?" confirm: "Patvirtinti" max_secondary_emails_error: "Pasiekėte didžiausią leistiną antrinių el. laiškų limitą." @@ -727,6 +759,7 @@ lt: title: "Nederamas" notify_user: title: "Siųsti @%{username} žinutę" + description: "Noriu tiesiogiai ir asmeniškai pasikalbėti su šiuo asmeniu apie jo įrašą." short_description: "Noriu tiesiogiai ir asmeniškai pasikalbėti su šiuo asmeniu apie jo įrašą." email_title: 'Jūsų įrašas temote "%{title}"' email_body: "%{link}\n\n%{message}" @@ -734,6 +767,7 @@ lt: title: "Kažkas kito" description: "Šis pranešimas reikalauja darbuotojų dėmesio dėl kitos priežasties, kuri nėra išvardyta aukščiau." short_description: "Reikia darbuotojų dėmesio dėl kitos priežasties" + email_title: 'Įrašas „%{title}“ reikalauja darbuotojų dėmesio' email_body: "%{link}\n\n%{message}" bookmark: title: "Žymės" @@ -748,9 +782,11 @@ lt: title: "juodraščio klaida" description: "Juodraštis redaguojamas kitame lange. Įkelkite šį puslapį iš naujo." draft_backup: + pm_title: "Atsarginės temų juodraščių kopijos" pm_body: "Tema, kurioje yra atsarginių juodraščių" user_activity: no_bookmarks: + self: "Jūs neturite pažymėtų įrašų; žymos leidžia greitai nukreipti į konkrečius įrašus." search: "Nerasta jokių žymių pagal pateiktą paieškos užklausą." others: "Nieko neišsaugota" no_drafts: @@ -768,15 +804,22 @@ lt: malformed_attestation_error: "Dekoduojant atestacijos duomenis įvyko klaida." user_verification_error: "Būtinas vartotojo patvirtinimas." unsupported_public_key_algorithm_error: "Serveris nepalaiko pateikto viešojo rakto algoritmo." + public_key_error: "Viešojo rakto patvirtinimas nepavyko." + ownership_error: "Saugos raktas nepriklauso vartotojui." + unknown_cose_algorithm_error: "Saugos raktui naudojamas algoritmas neatpažįstamas." topic_flag_types: spam: title: "Šlamštas" + description: "Ši tema yra reklama. Tai nėra naudinga ar aktuali šiai svetainei, bet yra reklaminio pobūdžio." long_form: "pažymėtas kaip šlamštas" short_description: "Tai reklama" inappropriate: title: "Nederamas" notify_moderators: title: "Kažkas kito" + long_form: "pažymėjo tai moderatoriaus dėmesiui" + short_description: "Reikia darbuotojų dėmesio dėl kitos priežasties" + email_title: 'Tema "%{title}" reikalauja moderatoriaus dėmesio' email_body: "%{link}\n\n%{message}" ignored: hidden_content: "

    Nepaisomas turinys

    " @@ -797,9 +840,11 @@ lt: unwatch_category: "Nustokite žiūrėti visas temas %{category}" mailing_list_mode: "Išjunkite adresatų sąrašo režimą" all: "Nesiųskite man laiškų iš %{sitename}" + not_found_description: "Atsiprašome, nepavyko rasti šio prenumeratos atsisakymo. Ar gali būti, kad el. laiške esanti nuoroda yra per sena ir pasibaigė jos galiojimo laikas?" log_out: "Atsijungti" submit: "Išsaugoti nuostatas" digest_frequency: + title: "Jūs gaunate suvestinius el. laiškus %{frequency}" never_title: "Jūs negaunate suvestinių el. laiškų" select_title: "Nustatykite el. laiškų suvestinių dažnumą:" never: "niekada" @@ -861,9 +906,11 @@ lt: title: "Moderatoriaus veikla" labels: moderator: Moderatorius + time_read: Skaitymo laikas topic_count: Sukurtos temos post_count: Įrašai sukurti revision_count: Pataisymai + description: Moderatoriaus veiklos sąrašas, įskaitant peržiūrėtas vėliavėles, skaitymo laiką, sukurtas temas, sukurtus įrašus, sukurtus asmeninius pranešimus ir pataisymus. flags_status: values: agreed: Sutiko @@ -883,6 +930,8 @@ lt: signups: title: "Registracijos" xaxis: "Diena" + yaxis: "Registracijų skaičius" + description: "Naujų paskyrų registracija šiam laikotarpiui." new_contributors: title: "Nauji bendraautoriai" xaxis: "Diena" @@ -913,14 +962,17 @@ lt: title: "DAU/MAU" xaxis: "Diena" yaxis: "DAU/MAU" + description: "Paskutinę dieną prisijungusių narių skaičius, padalytas iš narių, prisijungusių per pastarąjį mėnesį, skaičiaus – pateikiamas %, kuris rodo bendruomenės „lipnumą“. Siekite >30 proc." daily_engaged_users: title: "Kasdien dalyvaujantys vartotojai" xaxis: "Diena" yaxis: "Įsitraukę vartotojai" + description: "Vartotojų, kurie per paskutinę dieną pažymėjo „Patinka“ arba paskelbė įrašus, skaičius." profile_views: title: "Nario profilio peržiūros" xaxis: "Diena" yaxis: "Peržiūrėtų vartotojų profilių skaičius" + description: "Iš viso naujų naudotojų profilių peržiūrų." topics: title: "Temos" xaxis: "Diena" @@ -950,6 +1002,7 @@ lt: labels: level: Lygis description: "Vartotojų skaičius sugrupuotas pagal patikimumo lygį." + description_link: "https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/" users_by_type: title: "Vartotojai pagal tipą" xaxis: "Tipas" @@ -998,10 +1051,12 @@ lt: title: "Pranešti moderatoriams" xaxis: "Diena" yaxis: "Pranešimų skaičius" + description: "Kiek kartų moderatoriams buvo privačiai pranešta vėliavėle." notify_user_private_messages: title: "Pranešti vartotojui" xaxis: "Diena" yaxis: "Pranešimų skaičius" + description: "Kiek kartų naudotojams buvo privačiai pranešta vėliavėle." top_referrers: xaxis: "Narys" num_clicks: "Spustelėjimai" @@ -1026,9 +1081,11 @@ lt: labels: num_clicks: "Spustelėjimai" topic: "Tema" + description: "Temos, kurios sulaukė daugiausiai paspaudimų iš išorinių šaltinių." page_view_anon_reqs: title: "Anonimas" xaxis: "Diena" + yaxis: "Anoniminės puslapių peržiūros" description: "Naujų puslapių peržiūrų skaičius, kurį sudaro lankytojai, neprisijungę prie paskyros." page_view_logged_in_reqs: title: "Prisijungęs" @@ -1044,8 +1101,10 @@ lt: page_view_logged_in_mobile_reqs: title: "Prisijungę prie puslapių peržiūros" xaxis: "Diena" + yaxis: "Puslapių peržiūros prisijungus mobiliuoju telefonu" page_view_anon_mobile_reqs: xaxis: "Diena" + description: "Naujų puslapių skaičius iš lankytojų mobiliajame įrenginyje, kurie nėra prisijungę." http_background_reqs: title: "Fonas" xaxis: "Diena" @@ -1081,8 +1140,10 @@ lt: yaxis: "Viso" description: "Sukurtų naujų temų, kurios nesulaukė atsakymo, skaičius." mobile_visits: + title: "Vartotojo apsilankymai (mobili)" xaxis: "Diena" yaxis: "Apsilankymai" + description: "Unikalių naudotojų, kurie apsilankė naudodami mobilųjį įrenginį, skaičius." web_crawlers: labels: page_views: "Puslapio peržiūros" @@ -1116,6 +1177,25 @@ lt: labels: ignored_user: Ignoruoti vartotoją description: "Vartotojai, kuriuos nutildė ir (arba) ignoravo daugelis kitų vartotojų." + top_users_by_likes_received: + title: "Geriausi naudotojai pagal paspaudimus „Patinka“." + labels: + user: Narys + qtt_like: Gauta patikimų + description: "10 geriausių vartotojų, kurie sulaukė daugiau „patinka“ paspaudimų." + top_users_by_likes_received_from_inferior_trust_level: + title: "Geriausi vartotojai pagal mygtukus „Patinka“, gauti iš naudotojo, kurio patikimumo lygis žemesnis" + labels: + user: Narys + trust_level: Patikimumo lygis + qtt_like: Gauta patikimų + description: "10 geriausių vartotojų, turinčių aukštesnį patikimumo lygį, kuriems patinka žmonės, turintys mažesnį patikimumo lygį." + top_users_by_likes_received_from_a_variety_of_people: + title: "Geriausi naudotojai pagal paspaudimus „Patinka“, gautus iš įvairių žmonių" + labels: + user: Narys + qtt_like: Gauta patikimų + description: "Top 10 vartotojų, kurie gavo \"Patinka\" iš įvairių žmonių." dashboard: rails_env_warning: "Jūsų serveris veikia %{env} režimu." out_of_date_themes: "Galimi naujiniai šioms temoms:" @@ -1128,7 +1208,10 @@ lt: support_mixed_text_direction: "Palaikykite įvairias teksto kryptis iš kairės į dešinę ir iš dešinės į kairę." min_post_length: "Mažiausias leistinas įrašo ilgis simboliais" min_first_post_length: "Mažiausias leistinas pirmojo įrašo (temos turinio) ilgis simboliais" + min_personal_message_post_length: "Mažiausias leistinas pranešimų ilgis simboliais" + max_post_length: "Didžiausias leistinas įrašo ilgis simboliais" topic_featured_link_enabled: "Įgalinti nuorodos su temomis paskelbimą." + show_topic_featured_link_in_digest: "Rodyti temos nuorodą santraukos el. laiške." min_topic_views_for_delete_confirm: "Minimalus peržiūrų skaičius, kurį turi turėti tema, kad ištrynus pasirodytų patvirtinimo iššokantis langas" min_topic_title_length: "Minimalus leistinas temos pavadinimo ilgis simboliais" max_topic_title_length: "Maksimalus leistinas temos pavadinimo ilgis simboliais" @@ -1136,6 +1219,7 @@ lt: max_emojis_in_title: "Maksimalūs leidžiami jaustukai temos pavadinime" min_search_term_length: "Minimalus galiojantis paieškos termino ilgis simboliais" search_prefer_recent_posts: "Jei paieška jūsų dideliame forume yra lėta, ši parinktis pirmiausia išbando naujesnių įrašų indeksą" + log_search_queries: "Registruoti vartotojų atliekamas paieškos užklausas" search_ignore_accents: "Ignoruoti akcentus ieškant teksto." allow_uncategorized_topics: "Leisti kurti temas be kategorijos. ĮSPĖJIMAS: Jei yra temų, nepriskirtų kategorijoms, prieš išjungdami jas turite suskirstyti jas į kategorijas." allow_duplicate_topic_titles: "Leisti temas su identiškais, pasikartojančiais pavadinimais." @@ -1143,7 +1227,9 @@ lt: title: "Šios svetainės pavadinimas, naudojamas pavadinimo žymoje." short_site_description: "Trumpas aprašymas, naudojamas pagrindinio puslapio žymoje." crawl_images: "Gaukite vaizdus iš nuotolinių URL, kad įterptumėte teisingus pločio ir aukščio matmenis." + download_remote_images_threshold: "Minimali vieta diske, reikalinga nuotoliniams vaizdams atsisiųsti vietoje (procentais)" staff_edit_locks_post: "Pranešimai bus užrakinti nuo redagavimo, jei juos redaguos darbuotojai" + edit_history_visible_to_public: "Leisti visiems matyti ankstesnes redaguoto įrašo versijas. Kai išjungta, peržiūrėti gali tik darbuotojai." delete_removed_posts_after: "Autoriaus pašalintos žinutės bus automatiškai ištrintos po (n) valandų. Jei nustatytas 0, įrašai bus nedelsiant ištrinti." fixed_category_positions: "Jei pažymėsite, galėsite suskirstyti kategorijas į nustatytą tvarką. Jei nepažymėta, kategorijos pateikiamos pagal veiklos eilę." show_pinned_excerpt_mobile: "Rodyti ištrauką apie prisegtas temas mobiliojo įrenginio rodinyje." @@ -1151,13 +1237,23 @@ lt: logo: "Logotipo vaizdas viršutiniame kairiajame svetainės kampe. Naudokite platų stačiakampį vaizdą, kurio aukštis yra 120, o kraštinių santykis didesnis nei 3: 1. Jei jis paliekamas tuščias, bus rodomas svetainės pavadinimo tekstas." logo_dark: "Tamsios schemos alternatyva “logotipo” svetainės nustatymui." logo_small_dark: "Tamsios schemos alternatyva „mažo logotipo“ svetainės nustatymui." + large_icon: "Vaizdas naudojamas kaip kitų metaduomenų piktogramų pagrindas. Idealiu atveju turėtų būti didesnis nei 512 x 512. Jei paliksite tuščią, bus naudojamas logotipas_mažas." + email_custom_headers: "Brūkšniais atskirtas pasirinktinių el. laiškų antraščių sąrašas" enforce_second_factor: "Priverčia vartotojus įgalinti dviejų veiksnių autentifikavimą. Pasirinkite „viskas“, kad jis būtų taikomas visiems vartotojams. Pasirinkite „personalas“, kad jis būtų taikomas tik darbuotojams." force_https: "Priverskite savo svetainę naudoti tik HTTPS. ĮSPĖJIMAS: NEGALIMA to įjungti, kol nepatikrinate, kad HTTPS yra visiškai nustatytas ir veikia visiškai visur! Ar patikrinote savo CDN, visus socialinius prisijungimus ir visus išorinius logotipus / priklausomybes, kad įsitikintumėte, jog jie taip pat suderinami su HTTPS?" + summary_score_threshold: "Minimalus balas, kurio reikia, kad įrašas būtų įtrauktas į „Apibendrinti šią temą“" summary_timeline_button: "Rodyti mygtuką “Apibendrinti” laiko juostoje" enable_system_message_replies: "Leidžia vartotojams atsakyti į sistemos pranešimus, net jei asmeniniai pranešimai yra išjungti" + long_polling_interval: "Kiek laiko serveris turi palaukti prieš atsakydamas klientams, kai nėra duomenų, kuriuos reikia siųsti (tik prisijungę vartotojai)" polling_interval: "Kai apklausa trunka neilgai, kaip dažnai reikia prisijungti prie klientų apklausos milisekundėmis" hide_post_sensitivity: "Tikimybė, kad pažymėtas įrašas bus paslėptas" silence_new_user_sensitivity: "Tikimybė, kad naujas vartotojas bus nutildytas remiantis šlamšto vėliavomis" + tl3_additional_likes_per_day_multiplier: "Padidinkite patinkančių paspaudimų limitą per dieną tl3 (įprastai) padaugindami iš šio skaičiaus" + tl4_additional_likes_per_day_multiplier: "Padidinkite tl4 (lyderio) paspaudimų „patinka“ limitą per dieną, padaugindami iš šio skaičiaus" + tl2_additional_edits_per_day_multiplier: "Padidinkite tl2 (nario) pakeitimų limitą per dieną, padaugindami iš šio skaičiaus" + tl3_additional_edits_per_day_multiplier: "Padidinkite tl3 (įprasto) pakeitimų limitą per dieną, padaugindami iš šio skaičiaus" + tl4_additional_edits_per_day_multiplier: "Padidinkite tl4 (lyderio) pakeitimų limitą per dieną, padaugindami iš šio skaičiaus" + num_users_to_silence_new_user: "Jei naujo vartotojo įrašai gauna num_spam_flags_to_silence_new_user šlamšto žymas nuo daugelio skirtingų vartotojų, paslėpkite visus jų įrašus ir neleiskite skelbti ateityje. 0, kad išjungtumėte." must_approve_users: "Darbuotojai turi patvirtinti visas naujas vartotojų paskyras, prieš jiems leidžiant pasiekti svetainę." persistent_sessions: "Vartotojai liks prisijungę, kai interneto naršyklė bus uždaryta" maximum_session_age: "Vartotojas liks prisijungęs n valandų nuo paskutinio apsilankymo" @@ -1175,6 +1271,7 @@ lt: send_tl1_welcome_message: "Siųskite naujiems 1 patikimumo lygio vartotojams sveikinimo pranešimą." send_tl2_promotion_message: "Nusiųskite naujiems 2 patikimumo lygio vartotojams pranešimą apie reklamą." suppress_reply_directly_below: "Nerodyti įrašo išplėstinių atsakymų skaičiaus, kai tiesiai po šiuo įrašu yra tik vienas atsakymas." + topics_per_period_in_top_summary: "Populiariausių temų skaičius, rodomas numatytojoje populiariausių temų suvestinėje." topics_per_period_in_top_page: "Populiariausių temų, rodomų išplėstinėse „Rodyti daugiau“ populiariausiose temose, skaičius." redirect_users_to_top_page: "Automatiškai nukreipti naujus ir ilgai nedalyvaujančius vartotojus į viršutinį puslapį." moderators_view_emails: "Leisti moderatoriams peržiūrėti vartotojo el. laiškus" @@ -1190,13 +1287,21 @@ lt: password_unique_characters: "Mažiausias unikalių simbolių skaičius, kurį turi turėti slaptažodis." block_common_passwords: "Neleiskite slaptažodžių, kurie yra 10 000 dažniausiai naudojamų slaptažodžių." discourse_connect_secret: "Slapta eilutė, naudojama „DiscourseConnect“ informacijos kriptografiniam autentifikavimui, įsitikinkite, kad ji yra 10 simbolių ar ilgesnė" + discourse_connect_not_approved_url: "Peradresuoti nepatvirtintas DiscourseConnect paskyras į šį URL" + allow_new_registrations: "Leisti naujų vartotojų registracijas. Atžymėkite šį žymėjimą, kad niekas negalėtų sukurti naujos paskyros." enable_signup_cta: "Parodykite pranešimą grįžtantiems anoniminiams vartotojams, raginančius juos prisiregistruoti." google_oauth2_client_id: "„Google“ programos kliento ID." enable_discord_logins: "Leisti vartotojams autentifikuoti naudojant „Discord“?" discord_client_id: '„Discord“ kliento ID (reikia vieno? Apsilankykite „Discord“ kūrėjų portale)' + discord_secret: "Discord slaptas raktas" enable_backups: "Leiskite administratoriams kurti forumo atsargines kopijas" + backup_frequency: "Dienų skaičius tarp atsarginių kopijų kūrimo." + enable_safe_mode: "Leisti vartotojams įjungti saugųjį režimą, kad būtų derinami papildiniai." + rate_limit_create_topic: "Sukūrę temą, vartotojai turi palaukti (n) sekundžių prieš kurdami kitą temą." rate_limit_create_post: "Po paskelbimo, vartotojai turi palaukti (n) sekundžių prieš kurdami kitą įrašą." rate_limit_new_user_create_topic: "Sukūrę temą, nauji vartotojai, prieš sukurdami kitą temą, turi palaukti (n) sekundžių." + rate_limit_new_user_create_post: "Po paskelbimo nauji vartotojai turi palaukti (n) sekundžių prieš kurdami kitą įrašą." + max_likes_per_day: "Didžiausias paspaudimų „Patinka“ skaičius vienam vartotojui per dieną." max_flags_per_day: "Maksimalus vėliavų skaičius vienam vartotojui per dieną." max_bookmarks_per_day: "Maksimalus žymių skaičius vienam vartotojui per dieną." max_edits_per_day: "Maksimalus redagavimo skaičius vienam vartotojui per dieną." @@ -1210,14 +1315,34 @@ lt: max_post_deletions_per_day: "Maksimalus įrašų skaičius, kurį vartotojas gali ištrinti per dieną. Nustatykite 0, kad išjungtumėte įrašų trynimą." limit_suggested_to_category: "Siūlomose temose rodyti tik dabartinės kategorijos temas." suggested_topics_max_days_old: "Siūlomos temos neturėtų būti senesnės nei n dienų." + purge_deleted_uploads_grace_period_days: "Atidėjimo laikotarpis (dienomis), kol ištrintas įkėlimas ištrinamas." + s3_access_key_id: "„Amazon S3“ prieigos rakto ID, kuris bus naudojamas vaizdams, priedams ir atsarginėms kopijoms įkelti." avatar_sizes: "Automatiškai sugeneruotų avatarų dydžių sąrašas." external_system_avatars_enabled: "Naudokite išorinės sistemos avatarų paslaugą." external_system_avatars_url: "Išorinės sistemos avatarų paslaugos URL. Leidžiami pakeitimai yra {username} {first_letter} {color} {size}" external_emoji_url: "Jaustukų vaizdų išorinės paslaugos URL. Palikite tuščią, kad išjungtumėte." use_site_small_logo_as_system_avatar: "Naudokite mažą svetainės logotipą, o ne sistemos vartotojo avatarą. Reikalingas logotipas." enable_listing_suspended_users_on_search: "Įgalinkite nuolatinius vartotojus rasti sustabdytus vartotoj" + selectable_avatars_enabled: "Priversti vartotojus pasirinkti avatarą iš sąrašo." + selectable_avatars: "Sąrašas avatarų, kuriuos vartotojai gali pasirinkti." + allow_all_attachments_for_group_messages: "Leisti visus el. pašto priedus grupiniams pranešimams." + png_to_jpg_quality: "Konvertuoto JPG failo kokybė (1 – žemiausia kokybė, 99 – geriausia kokybė, 100 – išjungti)." + recompress_original_jpg_quality: "Įkeltų vaizdo failų kokybė (1 – žemiausia kokybė, 99 – geriausia kokybė, 100 – išjungti)." + image_preview_jpg_quality: "Pakeisto dydžio vaizdo failų kokybė (1 – žemiausia kokybė, 99 – geriausia kokybė, 100 – išjungti)." strip_image_metadata: "Juostelės vaizdo metaduomenys." + composer_media_optimization_image_bytes_optimization_threshold: "Minimalus vaizdo failo dydis, norint suaktyvinti kliento pusės optimizavimą" + composer_media_optimization_image_resize_dimensions_threshold: "Minimalus vaizdo plotis, kad būtų galima suaktyvinti kliento pusės dydį" + min_ratio_to_crop: "Santykis naudojamas apkarpyti aukščio vaizdus. Įveskite pločio/aukščio rezultatą." default_invitee_trust_level: "Numatytasis pakviestų vartotojų patikimumo lygis (0-4)." + tl1_requires_topics_entered: "Kiek temų turi įvesti naujas vartotojas, kad galėtų pasiekti 1 pasitikėjimo lygį." + tl1_requires_read_posts: "Kiek įrašų turi perskaityti naujas vartotojas, kad galėtų pasiekti 1 pasitikėjimo lygį." + tl2_requires_read_posts: "Kiek įrašų turi perskaityti vartotojas, kad pasiektų 2 pasitikėjimo lygį." + tl2_requires_days_visited: "Kiek dienų vartotojas turi apsilankyti svetainėje, kad galėtų pasiekti 2 pasitikėjimo lygį." + tl2_requires_likes_received: "Kiek teigiamų įvertinimų turi gauti vartotojas, kad pasiektų 2 pasitikėjimo lygį." + tl3_requires_topics_replied_to: "Minimalus temų, į kurias vartotojas turi atsakyti per paskutines (tl3 laiko tarpas) dienas, skaičius, kad galėtų būti paaukštintas iki 3 patikimumo lygio. (0 arba aukštesnis)" + tl3_requires_topics_viewed: "Temų, sukurtų per paskutines (tl3 laiko tarpsnio) dienas, procentas, kurį naudotojas turi peržiūrėti, kad galėtų būti paaukštintas iki 3 patikimumo lygio. (0–100)" + tl3_requires_topics_viewed_cap: "Didžiausias reikalingas temų skaičius, peržiūrėtas per paskutines (tl3 laiko tarpas) dienas." + tl3_requires_posts_read_all_time: "Minimalus bendras įrašų skaičius, kurį vartotojas turi perskaityti, kad atitiktų 3 patikimumo lygį." min_trust_to_edit_wiki_post: "Minimalus patikimumo lygis, reikalingas norint redaguoti įrašą, pažymėtą kaip wiki." min_trust_to_edit_post: "Minimalus patikimumo lygis, reikalingas norint redaguoti įrašus." min_trust_to_allow_self_wiki: "Minimalus patikimumo lygis, kurio reikia norint sukurti vartotojo įrašą „wiki“." @@ -1233,20 +1358,85 @@ lt: newuser_max_links: "Kiek nuorodų naujas vartotojas gali pridėti prie įrašo." newuser_max_embedded_media: "Kiek įterptųjų medijos elementų naujas vartotojas gali pridėti prie įrašo." newuser_max_attachments: "Kiek priedų naujas vartotojas gali pridėti prie įrašo." + newuser_max_mentions_per_post: "Didžiausias @name pranešimų skaičius, kurį naujas vartotojas gali naudoti įraše." newuser_max_replies_per_topic: "Maksimalus atsakymų skaičius, kurį naujas vartotojas gali pateikti vienoje temoje, kol kas nors į juos atsakys." + enable_mentions: "Leiskite vartotojams paminėti kitus vartotojus." + category_style: "Vizualus stilius kategorijos ženklelių." dark_mode_none: "Nieko" + max_image_megapixels: "Didžiausias leistinas vaizdo megapikselių skaičius. Vaizdai su didesniu megapikselių skaičiumi bus atmesti." + topic_views_heat_medium: "Po tiek peržiūrų rodinių laukas vidutiniškai paryškinamas." + topic_views_heat_high: "Po šio daugybės peržiūrų, peržiūros laukas yra stipriai paryškintas." + privacy_policy_url: "Jei turite Privatumo politikos dokumentą, talpinamą kitur, kurį norite naudoti, pateikite visą URL čia." + incoming_email_prefer_html: "Įeinantiems el. laiškams naudokite HTML, o ne tekstą." + display_name_on_email_from: "Rodyti visus vardus el. pašte iš laukų" + unsubscribe_via_email: "Leiskite naudotojams atšaukti el. laiškų prenumeratą išsiųsdami el. laišką, kurio temoje arba tekste yra „atsisakyti prenumeratos“." + unsubscribe_via_email_footer: "Pridėkite prenumeratos atsisakymą el. paštu mailto: nuoroda į išsiųstų el. laiškų poraštę" + delete_email_logs_after_days: "Ištrinkite el. pašto žurnalus po (N) dienų. 0 laikyti neribotą laiką" + disallow_reply_by_email_after_days: "Neleisti atsakyti el. paštu po (N) dienų. 0 laikyti neribotą laiką" + max_emails_per_day_per_user: "Maksimalus el. laiškų, kuriuos reikia išsiųsti vartotojams per dieną, skaičius. 0, kad išjungtumėte ribą" + bounce_score_threshold: "Maksimalus atmetimo balas, kol nustosime siųsti el. laiškus vartotojui." + reset_bounce_score_after_days: "Automatiškai iš naujo nustatyti atmetimo balą po X dienų." + blocked_attachment_content_types: "Raktinių žodžių, naudojamų įtraukiant priedus pagal turinio tipą, sąrašas." + blocked_attachment_filenames: "Raktinių žodžių, naudojamų blokuoti priedus pagal failo pavadinimą, sąrašas." + forwarded_emails_behaviour: "Kaip apdoroti persiųstą el. laišką Discourse" + always_show_trimmed_content: "Visada rodyti apkarpytas gaunamų el. laiškų dalis. ĮSPĖJIMAS: gali atskleisti el. pašto adresus." + trim_incoming_emails: "Apkarpykite dalį gaunamų el. laiškų, kurie nėra svarbūs." + pop3_polling_enabled: "El. pašto atsakymų apklausa per POP3." + relative_date_duration: "Dienų skaičius po paskelbimo, kai paskelbimo datos bus rodomos kaip santykinės (7 d.), o ne absoliučios (vasario 20 d.)." + logout_redirect: "Vieta, į kurią reikia nukreipti naršyklę atsijungus (pvz., https://example.com/logout)" + allow_uploaded_avatars: "Leisti vartotojams įkelti pasirinktines profilio nuotraukas." + default_avatars: "URL į avatarus, kurie bus naudojami pagal numatytuosius nustatymus naujiems vartotojams, kol jie juos pakeis." + automatically_download_gravatars: "Atsisiųskite “Gravatars” vartotojams, sukūrus paskyrą arba pakeitus el. paštą." + digest_topics: "Didžiausias populiarių temų skaičius, rodomas el. pašto suvestinėje." + digest_posts: "Didžiausias populiarių įrašų, rodomų el. pašto suvestinėje, skaičius." + digest_other_topics: "Didžiausias temų skaičius, rodomas el. laiškų santraukos skiltyje „Naujienos jūsų stebimose temose ir kategorijose“." + show_inactive_accounts: "Leisti prisijungusiems vartotojams naršyti neaktyvių paskyrų profilius." + hide_suspension_reasons: "Nerodyti sustabdymo priežasčių viešai naudotojų profiliuose." user_selected_primary_groups: "Leiskite vartotojams nustatyti savo pagrindinę grupę" max_notifications_per_user: "Maksimalus vieno vartotojo pranešimų skaičius, jei šis skaičius viršijamas, senieji pranešimai bus ištrinti. Taikoma kas savaitę. Norėdami išjungti, nustatykite 0" disable_avatar_education_message: "Išjungti švietimo pranešimą keičiant avataras." + header_dropdown_category_count: "Kiek kategorijų gali būti rodoma antraštės išskleidžiamajame meniu." disable_category_edit_notifications: "Išjungti pranešimus apie temų kategorijų redagavimą." automatically_unpin_topics: "Automatiškai atsegti temas, kai vartotojas pasiekia apačią." read_time_word_count: "Žodžių skaičius per minutę apskaičiuojant numatomą skaitymo laiką." + full_name_required: "Pilnas vardas yra būtinas vartotojo profilio laukas." + enable_names: "Rodyti visą vartotojo vardą jo profilyje, vartotojo kortelėje ir el. laiškuose. Išjungti, kad visur paslėptumėte visą vardą." + display_name_on_posts: "Rodyti visą vartotojo vardą jo įrašuose, be @naudotojo vardo." + short_progress_text_threshold: "Kai pranešimų skaičius temoje viršys šį skaičių, eigos juostoje bus rodomas tik dabartinis įrašo numeris. Jei pakeisite eigos juostos plotį, gali tekti pakeisti šią reikšmę." show_copy_button_on_codeblocks: "Pridėkite mygtuką prie kodų blokų, kad nukopijuotumėte bloko turinį į vartotojo iškarpinę." embed_truncate: "Truncate the embedded posts." embed_post_limit: "Maskimalus įterpiamų įrašų skaičius." embed_username_required: "Kuriant temą reikalingas vartotojo vardas." + delete_drafts_older_than_n_days: "Ištrinkite senesnius nei (n) dienų juodraščius." + prevent_anons_from_downloading_files: "Neleiskite anoniminiams vartotojams atsisiųsti priedų." enable_emoji: "Įgalinti Emoji" + emoji_autocomplete_min_chars: "Minimalus simbolių skaičius, reikalingas automatinio užbaigimo jaustukų iššokančiam langui suaktyvinti" + enable_inline_emoji_translation: "Įgalinamas įterptųjų jaustukų vertimas (be tarpų ar skyrybos ženklų prieš tai)" + approve_post_count: "Naujo arba pagrindinio naudotojo įrašų, kuriuos reikia patvirtinti, skaičius" + approve_unless_trust_level: "Naudotojų įrašai žemiau šio pasitikėjimo lygio turi būti patvirtinti" + approve_new_topics_unless_trust_level: "Naujos temos vartotojams žemiau šio patikimumo lygio turi būti patvirtintos" + auto_close_messages_post_count: "Maksimalus pranešimų skaičius, leidžiamas pranešime prieš jį automatiškai uždarant (0 išjungti)" + auto_close_topics_create_linked_topic: "Sukurkite naują susietą temą, kai tema automatiškai uždaroma pagal nustatymą „Automatiškai uždarytų temų įrašų skaičių“" + returning_user_notice_tl: "Minimalus patikimumo lygis, reikalingas norint pamatyti grįžtančius vartotojo pašto pranešimus." + returning_users_days: "Kiek dienų turėtų praeiti, kol vartotojas laikomas grįžtančiu." + blur_tl0_flagged_posts_media: "Sulieti pažymėtų įrašų vaizdus, kad paslėptumėte potencialiai NSFW turinį." + enable_page_publishing: "Leiskite darbuotojams skelbti temas naujuose URL adresuose su savo stiliumi." + show_published_pages_login_required: "Anoniminiai vartotojai gali matyti paskelbtus puslapius, net jei reikia prisijungti." + skip_auto_delete_reply_likes: "Kai automatiškai ištrinate senus atsakymus, praleiskite įrašų, turinčių tokį ar daugiau teigiamų įvertinimų skaičių, trynimą." + default_email_digest_frequency: "Kaip dažnai vartotojai pagal numatytuosius nustatymus gauna suvestinius el. laiškus." + default_include_tl0_in_digests: "Pagal numatytuosius nustatymus įtraukite naujų vartotojų įrašus į suvestinius el. laiškus. Vartotojai gali tai pakeisti savo nustatymuose." + default_email_level: "Nustatykite numatytąjį pranešimų el. paštu lygį įprastoms temoms." + default_topics_automatic_unpin: "Automatiškai atsegti temas, kai naudotojas pasiekia apačią pagal numatytuosius nustatymus." + default_categories_watching: "Kategorijų, kurios žiūrimos pagal numatytuosius nustatymus, sąrašas." default_categories_tracking: "Kategorijų, kurios stebimos pagal numatytuosius nustatymus, sąrašas." + default_categories_muted: "Kategorijų, kurios yra nutildytos pagal numatytuosius nustatymus, sąrašas." + default_text_size: "Teksto dydis, kuris pasirenkamas pagal numatytuosius nustatymus" + default_title_count_mode: "Numatytasis puslapio pavadinimo skaitiklio režimas" + allow_user_api_keys: "Leisti generuoti vartotojo API raktus" + allow_user_api_key_scopes: "Leidžiamų naudotojo API raktų apimčių sąrašas" + min_trust_level_for_user_api_key: "Patikimumo lygis, reikalingas generuoti vartotojo API raktus" + allowed_user_api_auth_redirects: "Leidžiamas vartotojo API raktų autentifikavimo peradresavimo URL. Pakaitos simbolis * gali būti naudojamas norint atitikti bet kurią jo dalį (pvz., www.example.com/*)." + allowed_user_api_push_urls: "Leidžiami serverio siuntimo naudotojui API URL adresai" tagging_enabled: "Įgalinti žymas temose?" min_trust_to_create_tag: "Minimalus patikimumo lygis, reikalingas žymai sukurti." max_tag_length: "Maksimalus simbolių skaičius, kurį galima naudoti žymoje." @@ -1259,8 +1449,12 @@ lt: force_lowercase_tags: "Priversti visas naujas žymas rašyti mažosiomis raidėmis." company_name: "Įmonės pavadinimas" governing_law: "Reglamentuojantys teisės aktai" + city_for_disputes: "Ginčų miestas" push_notifications_icon: "Ženklelio piktograma, rodoma pranešimų kampe. Rekomenduojamas 96 × 96 vienspalvis PNG su skaidrumu." short_title: "Trumpas pavadinimas bus naudojamas vartotojo pagrindiniame ekrane, paleidimo priemonėje ar kitose vietose, kur erdvė gali būti ribota. Jis turėtų būti apribotas iki 12 simbolių." + gravatar_name: "Gravatar teikėjo pavadinimas" + gravatar_base_url: "Gravatar teikėjo API bazės URL" + gravatar_login_url: "URL, susijęs su gravatar_base_url, kuris suteikia vartotojui prisijungimą prie Gravatar paslaugos" errors: invalid_email: "Neteisingas el. pašto adresas." invalid_username: "Nėra vartotojo su tokiu vartotojo vardu." @@ -1269,12 +1463,23 @@ lt: invalid_integer_min_max: "Vertė turi būti nuo %{min} iki %{max}." invalid_integer_min: "Vertė turi būti %{min} arba didesnė." invalid_integer_max: "Vertė negali būti didesnė nei %{max}." + invalid_integer: "Reikšmė turi būti sveikas skaičius." invalid_string: "Netinkama vertė." invalid_string_min_max: "Turi būti tarp %{min} ir %{max} simbolių." invalid_string_min: "Turi būti ne mažiau kaip %{min} simbolių." invalid_string_max: "Turi būti ne daugiau kaip %{max} simbolių." invalid_json: "Netinkamas JSON." invalid_reply_by_email_address: "Reikšmę turi sudaryti „%{reply_key}“ ir ji turi skirtis nuo pranešimo el. pašto." + max_username_length_exists: "Negalite nustatyti maksimalaus vartotojo vardo ilgio žemiau ilgiausio vartotojo vardo (%{username})." + max_username_length_range: "Negalite nustatyti didžiausios vertės žemiau minimumo." + invalid_hex_value: "Spalvų reikšmės turi būti 6 skaitmenų šešioliktainiai kodai." + empty_selectable_avatars: "Prieš įgalindami šį nustatymą, pirmiausia turite įkelti bent du pasirinktus avatarus." + allowed_unicode_usernames: + leading_trailing_slash: "Reguliarioji išraiška neturi prasidėti ir baigtis pasviruoju brūkšniu." + placeholder: + discourse_connect_provider_secrets: + key: "www.example.com" + value: "DiscourseConnect paslaptis" search: extreme_load_error: "Svetainė yra ekstremalios apkrovos, paieška yra išjungta, bandykite dar kartą vėliau" within_post: "#%{post_number} nuo %{username}" @@ -1308,15 +1513,30 @@ lt: blank: "negali būti tuščia" unavailable: "yra nepasiekiamas" invalid: "turi neleistinų simbolių" + topic_statuses: + autoclosed_disabled: "Ši tema dabar atidaryta. Leidžiami nauji atsakymai." + autoclosed_disabled_lastpost: "Ši tema dabar atidaryta. Leidžiami nauji atsakymai." + auto_deleted_by_timer: "Automatiškai ištrinamas laikmačiu." login: invalid_second_factor_method: "Pasirinktas dviejų veiksnių metodas yra neteisingas." not_enabled_second_factor_method: "Pasirinktas dviejų veiksnių metodas jūsų paskyroje neįjungtas." + security_key_alternative: "Išbandykite kitą būdą" + security_key_authenticate: "Autentifikuokite naudodami saugos raktą" + security_key_not_allowed_error: "Saugos rakto autentifikavimo procesas buvo nutrauktas arba buvo atšauktas." + security_key_invalid: "Įvyko klaida, patvirtinanti saugos raktą." admin_not_allowed_from_ip_address: "Tu negali prisijungti kaip administratorius iš šio IP adreso." errors: "%{errors}" not_available: "Negalimas. Gal pabandykite %{suggestion}?" + omniauth_error: + invalid_iat: "Nepavyko patvirtinti prieigos rakto dėl serverio laikrodžio skirtumų. Prašau, pabandykite dar kartą." omniauth_error_unknown: "Apdorojant jūsų prisijungimo duomenis įvyko klaida. Bandykite dar kartą." omniauth_confirm_title: "Prisijunkite naudodami %{provider}" omniauth_confirm_button: "Tęsti" + authenticator_error_no_valid_email: "El. pašto adresai, susieti su %{account} , neleidžiami. Gali reikėti sukonfigūruoti paskyrą naudojant kitą el. pašto adresą." + new_registrations_disabled: "Šiuo metu naujų paskyrų registracija neleidžiama." + password_too_long: "Slaptažodžiai ribojami iki 200 simbolių." + email_too_long: "Jūsų pateiktas el. pašto adresas per ilgas. Pašto dėžutės pavadinimai turi būti ne ilgesni kaip 254 simboliai, o domenų vardai – ne ilgesni kaip 253 simboliai." + wrong_invite_code: "Įvestas pakvieto kodas buvo neteisingas." reserved_username: "Šis vartotojo vardas neleidžiamas." missing_user_field: "Neužpildėte visų vartotojo laukų" auth_complete: "Autentifikavimas baigtas." @@ -1330,6 +1550,9 @@ lt: invalid_security_key: "Netinkamas saugos raktas." missing_second_factor_name: "Pateikite vardą." missing_second_factor_code: "Pateikite kodą." + second_factor_toggle: + totp: "Vietoj to naudokite autentifikavimo programėlę arba saugos raktą" + backup_code: "Vietoj to naudokite atsarginį kodą" admin: email: sent_test: "išsiųsta!" @@ -1338,11 +1561,14 @@ lt: updating_username: "Atnaujinamas vartotojo vardas ..." changing_post_ownership: "Keičiama įrašo nuosavybė ..." merging_given_daily_likes: "Sujungiama atsižvelgiant į kasdienius mėgstamus dalykus ..." + merging_user_visits: "Vartotojų apsilankymų sujungimas..." updating_site_settings: "Atnaujinami svetainės nustatymai ..." + updating_user_stats: "Vartotojo statistikos atnaujinimas..." merging_user_attributes: "Sujungiami naudotojo atributai ..." deleting_source_user: "Ištrinamas šaltinio vartotojas ..." user: deactivated_by_staff: "Išjungė darbuotojai" + activated_by_staff: "Aktyvuoja darbuotojai" username: too_long: "yra per ilgas" characters: "turi būti tik skaičiai, raidės, brūkšneliai, taškai ir pabraukimai" @@ -1360,6 +1586,24 @@ lt: domain_not_allowed: "Svetainė netinkama. Leidžiami domenai yra: %{domains}" destroy_reasons: inactive_user: "Neaktyvus vartotojas" + invite_mailer: + subject_template: "%{inviter_name} pakvietė jus į „%{topic_title}“ %{site_domain_name}" + text_body_template: | + %{inviter_name} pakvietė jus į diskusiją + + > **%{topic_title}** + > + > %{topic_excerpt} + + at + + > %{site_title} -- %{site_description} + + Jei jus domina, spustelėkite toliau pateiktą nuorodą: + + %{invite_link} + custom_invite_mailer: + subject_template: "%{inviter_name} pakvietė jus į „%{topic_title}“ %{site_domain_name}" invite_forum_mailer: text_body_template: | %{inviter_name} pakvietė jus prisijungti @@ -1400,8 +1644,28 @@ lt: new_version_mailer: title: "Nauja versija Mailer" subject_template: "[%{email_prefix}] Išleista Nauja Discourse versija" + text_body_template: | + Oho, yra nauja [Discourse]versija (https://www.discourse.org)! + + Jūsų versija: %{installed_version} + Nauja versija: **%{new_version}** + + – Atnaujinkite naudodami mūsų paprastą **[naršyklės naujinimas vienu spustelėjimu](%{base_url}/admin/upgrade)** + + – Sužinokite, kas naujo [leidimo pastabose]( https://meta.discourse.org/tag/release-notes) arba peržiūrėkite [neapdorotą „GitHub“ pakeitimų žurnalą](https://github.com/discourse/discourse/commits/main) + + – Apsilankykite [meta.discourse.org](https:// meta.discourse.org) naujienoms, diskusijoms ir Discourse palaikymui new_version_mailer_with_notes: subject_template: "[%{email_prefix}] Galimas atnaujinimas" + flag_reasons: + inappropriate: "Jūsų įrašas buvo pažymėtas kaip **netinkamas**: bendruomenė mano, kad jis yra piktnaudžiaujantis, įžeidžiantis arba pažeidžia [mūsų bendruomenės gaires] (%{base_path}/gairės)." + spam: "Jūsų įrašas buvo pažymėtas kaip **šlamštas**: bendruomenė mano, kad tai reklama, kažkas, kas yra pernelyg reklaminio pobūdžio, o ne naudinga ar aktualu temai, kaip tikėtasi." + notify_moderators: "Jūsų įrašas buvo pažymėtas **moderatoriaus dėmesiui**: bendruomenė mano, kad dėl įrašo reikia rankinio darbuotojo įsikišimo." + responder: + off_topic: "Įrašas buvo pažymėtas kaip **ne į temą**: bendruomenė mano, kad jis nėra tinkamas temai, kaip šiuo metu apibrėžia pavadinimas ir pirmasis įrašas." + inappropriate: "Jūsų įrašas buvo pažymėtas kaip **netinkamas**: bendruomenė mano, kad jis yra piktnaudžiaujantis, įžeidžiantis arba pažeidžia [mūsų bendruomenės gaires] (%{base_path}/gairės)." + spam: "Jūsų įrašas buvo pažymėtas kaip **šlamštas**: bendruomenė mano, kad tai reklama, kažkas, kas yra pernelyg reklaminio pobūdžio, o ne naudinga ar aktualu temai, kaip tikėtasi." + notify_moderators: "Jūsų įrašas buvo pažymėtas **moderatoriaus dėmesiui**: bendruomenė mano, kad dėl įrašo reikia rankinio darbuotojo įsikišimo." flags_dispositions: agreed: "Dėkojame, kad pranešėte mums. Mes sutinkame, kad yra problema, ir mes ją nagrinėjame." agreed_and_deleted: "Dėkojame, kad pranešėte mums. Sutinkame, kad iškilo problema, ir pašalinome įrašą." @@ -1440,6 +1704,22 @@ lt: Jūsų įrašas liks paslėptas, kol darbuotojas jį peržiūrės. Jei reikia papildomų nurodymų, skaitykite mūsų [bendruomenės gaires] (%{base_url}/gairės). + flags_agreed_and_post_deleted_for_responders: + subject_template: "Darbuotojai pašalino atsakymą iš pažymėto įrašo" + text_body_template: | + Sveiki, + + Tai yra automatizuota %{site_name} žinutė, leidžianti jums žinoti, kad [post](%{base_url}%{url}), į kurį atsakėte, buvo pašalintas. + + %{flag_reason} + + Šis įrašas buvo pažymėtas bendruomenės ir darbuotojas pasirinko jį pašalinti. + + “``markdown + %{flagged_post_raw_content} + ``` + + Norėdami gauti daugiau informacijos apie pašalinimo priežastis, peržiūrėkite mūsų [bendruomenės gaires] (%{base_url}/gairės). welcome_user: title: "Sveikas narį" subject_template: "Sveiki atvykę į %{site_name}!" @@ -1448,6 +1728,22 @@ lt: subject_template: "Dėkojeme jog praleidžiate laika su mumis" welcome_invite: subject_template: "Sveiki atvykę į %{site_name}!" + text_body_template: | + Dėkojame, kad priėmėte kvietimą į %{site_name} – sveiki! + + – Mes sukūrėme šią naują paskyrą **%{username}** jums. Pakeiskite savo vardą arba slaptažodį apsilankę [jūsų vartotojo profilis][prefs]. + + – Kai prisijungiate, **naudokite tą patį el. pašto adresą iš pradinio kvietimo** – kitaip negalėsime pasakyti, kad tai jūs! + + %{new_user_tips} + + Mes visada tikime [civilizuotu bendruomenės elgesiu](%{base_url}/gairės). + + Mėgaukitės viešnage! + + [prefs]: %{user_preferences_url} + tl2_promotion_message: + subject_template: "Sveikiname padidinus pasitikėjimo lygį!" backup_succeeded: title: "Atsarginė kopija sukurta" subject_template: "Atsarginė kopija sėkmingai užbaigta." @@ -1490,22 +1786,61 @@ lt: text_body_template: "Atsiprašome, bet nepavyko eksportuoti duomenų. Patikrinkite žurnalus arba [susisiekite su darbuotoju] (%{base_url}/apie)." email_reject_insufficient_trust_level: subject_template: "[%{email_prefix}] El. Pašto problema - nepakankamas pasitikimumo lygis" + text_body_template: | + Atsiprašome, bet jūsų el. Laiškas %{destination} (pavadinimu %{former_title}) neveikė. + + Jūsų paskyra neturi reikiamo patikimumo lygio, kad galėtumėte paskelbti naujas temas šiuo el. pašto adresu. Jei manote, kad tai klaida, [susisiekite su darbuotoju] (%{base_url}/apie). email_reject_screened_email: subject_template: "[%{email_prefix}] El. pašto problema - užblokuotas el. paštas" email_reject_not_allowed_email: subject_template: "[%{email_prefix}] El. pašto problema - užblokuotas el. paštas" email_reject_inactive_user: + title: "El. pašto atmetimas Neaktyvus vartotojas" subject_template: "[%{email_prefix}] El. Pašto problema - neaktyvus vartotojas" + text_body_template: | + Atsiprašome, bet jūsų el. Laiškas %{destination} (pavadinimu %{former_title}) neveikė. + + Jūsų paskyra, susieta su šiuo el. pašto adresu, nėra aktyvuota. Prieš siųsdami el. laiškus, suaktyvinkite savo paskyrą. + email_reject_reply_user_not_matching: + text_body_template: | + Atsiprašome, bet jūsų el. pašto žinutė adresu %{destination} (pavadinimas %{former_title}) neveikė. + + Jūsų atsakymas išsiųstas iš kito el. pašto adreso nei tas, kurio tikėjomės, todėl nesame tikri, ar tai tas pats asmuo. Pabandykite siųsti iš kito el. pašto adreso arba [susisiekite su darbuotoju](%{base_url}/apie). + email_reject_parsing: + title: "El. pašto atmetimo analizė" + subject_template: "[%{email_prefix}] El. pašto problema – turinys neatpažintas" + email_reject_invalid_post_specified: + title: "El. pašto atmetimas Nurodytas neteisingas pranešimas" + text_body_template: | + Atsiprašome, bet jūsų el. pašto žinutė adresu %{destination} (pavadinimas %{former_title}) neveikė. + + Priežastis: + + %{post_error} + + Jei galite išspręsti problemą, bandykite dar kartą. + date_invalid: "Pranešimo sukūrimo data nerasta. Ar el. laiške trūksta antraštės: Data?" + email_reject_post_too_short: + text_body_template: | + Atsiprašome, bet jūsų el. pašto žinutė adresu %{destination} (pavadinimas %{former_title}) neveikė. + + Siekiant paskatinti gilesnius pokalbius, labai trumpi atsakymai neleidžiami. Ar galėtumėte atsakyti bent %{count} simboliais? Arba galite pažymėti, kad įrašas patinka el. paštu, atsakydamas „+1“. + email_reject_reply_key: + title: "El. pašto atmetimo atsakymo raktas" email_reject_auto_generated: title: "Automatiškai sugeneruotas el. pašto atmetimas" subject_template: "[%{email_prefix}] El. pašto problema - automatiškai sugeneruotas atsakymas" email_reject_reply_not_allowed: subject_template: "[%{email_prefix}] El. Pašto problema - atsakymas neleidžiamas" + email_error_notification: + title: "El. pašto klaidos pranešimas" + subject_template: "[%{email_prefix}] El. pašto problema – POP autentifikavimo klaida" email_revoked: subject_template: "Ar jūsų el. pašto adresas teisingas?" ignored_users_summary: subject_template: "Daugelis kitų naudotojų ignoruoja vartotoją" too_many_spam_flags: + title: "Per daug šlamšto vėliavėlių" subject_template: "Laukianti Nauja paskyra" text_body_template: | Sveiki, @@ -1528,12 +1863,15 @@ lt: subject_template: "Naujas naudotojo %{username} įrašas užblokuotas dėl pakartotinių nuorodų" unsilenced: title: "Nebenutildytas" + subject_template: "Paskyra nebesulaikyta" text_body_template: | Sveiki, Tai automatinis pranešimas nuo %{site_name} , informuojantis, kad jūsų paskyra nebėra sulaikyta po darbuotojų peržiūros. Dabar galite vėl kurti naujus atsakymus ir temas. Ačiū už kantrybę. + dashboard_problems: + subject_template: "Nauji patarimai jūsų svetainės informacijos suvestinėje" new_user_of_the_month: title: "Jūs esate naujas mėnesio vartotojas!" subject_template: "Jūs esate naujas mėnesio vartotojas!" @@ -1718,6 +2056,8 @@ lt: text_body_template: |2 %{message} + account_suspended: + title: "Paskyra sustabdyta" account_suspended_forever: subject_template: "[%{email_prefix}] Jūsų paskyra buvo sustabdyta" account_silenced: @@ -2150,9 +2490,13 @@ lt: name: Vertinama respected: name: Gerbiamas + long_description: | + Šis ženklelis suteikiamas, kai 100 skirtingų įrašų sulaukia bent 2 paspaudimus „Patinka“. Bendruomenė auga ir gerbia jūsų indėlį į pokalbius čia. admired: name: Žavisi description: 300 įrašų sulaukė 5 „patinka“ + long_description: | + Šis ženklelis suteikiamas, kai sulaukiate bent 5 „patinka“ paspaudimų ant 300 skirtingų įrašų. Oho! Bendruomenė žavisi jūsų dažnu, kokybišku indėliu į čia vykstančius pokalbius. out_of_love: name: Nebeliko meilės higher_love: @@ -2161,6 +2505,8 @@ lt: name: Įsimylėjas thank_you: name: Ačiū + gives_back: + description: Turi 100 teigiamų įvertinimų ir davė 100 teigiamų įvertinimų empathetic: name: Empatiškas description: Turi 500 teigiamų įvertinimų ir davė 1000 teigiamų įvertinimų @@ -2172,6 +2518,8 @@ lt: first_mention: name: Pirmasis paminėjimas description: Įraše paminėjo vartotoją + long_description: | + Šis ženklelis suteikiamas pirmą kartą paminėjus kažkieno @naudotojo vardą savo įraše. Kiekvienas paminėjimas generuoja pranešimą tam asmeniui, kad jis žinotų apie jūsų įrašą. Tiesiog pradėkite rašyti @ (prie simbolio), norėdami paminėti bet kurį vartotoją arba, jei leidžiama, grupę – tai patogus būdas atkreipti jų dėmesį. first_onebox: name: Pirmasis įterpimas first_reply_by_email: diff --git a/config/locales/server.lv.yml b/config/locales/server.lv.yml index 6a456a777d..d03a208262 100644 --- a/config/locales/server.lv.yml +++ b/config/locales/server.lv.yml @@ -359,6 +359,18 @@ lv: top_uploads: labels: filename: Faila nosaukums + top_users_by_likes_received: + labels: + user: Lietotājs + qtt_like: Saņemtās atzinības + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Lietotājs + qtt_like: Saņemtās atzinības + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Lietotājs + qtt_like: Saņemtās atzinības site_settings: disabled: "atslēgt" dark_mode_none: "Nav" diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 0d17138cd3..00326dba4b 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -909,6 +909,18 @@ nb_NO: labels: filename: Filnavn author: Forfatter + top_users_by_likes_received: + labels: + user: Bruker + qtt_like: Likes mottatt + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Bruker + qtt_like: Likes mottatt + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Bruker + qtt_like: Likes mottatt dashboard: rails_env_warning: "Serveren din kjører i %{env] modus." host_names_warning: "Din config/database.yml-fil bruker forvalgt lokalvert-vertsnavn. Oppdater det til å bruke din sides vertsnavn." diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index cd4401f382..4bcf7bad2d 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -1236,6 +1236,18 @@ nl: ignores_count: Aantal genegeerd mutes_count: Aantal gedempt description: "Gebruikers die door veel andere gebruikers zijn gedempt en/of genegeerd." + top_users_by_likes_received: + labels: + user: Gebruiker + qtt_like: Ontvangen likes + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Gebruiker + qtt_like: Ontvangen likes + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Gebruiker + qtt_like: Ontvangen likes dashboard: rails_env_warning: "Uw server werkt in de modus voor %{env}." host_names_warning: "Uw bestand config/database.yml gebruikt de standaardhostnaam localhost. Werk deze bij naar de hostnaam van uw website." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index a1fd2c245b..f079111ac5 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_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: invalid_address: "Przepraszamy, nie mogliśmy wygenerować podglądu tej strony internetowej, ponieważ nie można znaleźć serwera „%{hostname}”. Zamiast podglądu w poście pojawi się tylko link. :cry:" @@ -1438,6 +1439,18 @@ pl_PL: ignores_count: Licznik ignorowanych 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: + labels: + user: Użytkownik + qtt_like: Polubienia otrzymane + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Użytkownik + qtt_like: Polubienia otrzymane + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Użytkownik + qtt_like: Polubienia otrzymane dashboard: 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." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 40b422278b..1cb575718a 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -946,6 +946,18 @@ pt: top_uploads: labels: filename: Nome do ficheiro + top_users_by_likes_received: + labels: + user: Utilizador + qtt_like: Gostos Recebidos + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Utilizador + qtt_like: Gostos Recebidos + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Utilizador + qtt_like: Gostos Recebidos dashboard: rails_env_warning: "O seu servidor está a executar em modo %{env}." host_names_warning: "O ficheiro config/database.yml está a utilizar o nome do servidor local por defeito. Modifique para usar o nome do servidor do seu sítio." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 4c4e1e7ca7..003f9d2199 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -1326,6 +1326,18 @@ pt_BR: ignores_count: Contagem de ações de ignorar mutes_count: Contagem de ações de silenciar description: "Usuários(as) que foram silenciados(as) e/ou ignorados(as) por muitos outros(as) usuários(as)." + top_users_by_likes_received: + labels: + user: Usuário(a) + qtt_like: Curtidas recebidas + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Usuário(a) + qtt_like: Curtidas recebidas + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Usuário(a) + qtt_like: Curtidas recebidas dashboard: rails_env_warning: "Seu servidor está rodando no modo %{env}." host_names_warning: "O arquivo config/database.yml está usando hostname do localhost padrão. Modifique para usar o hostname do seu site." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 00f8522651..8c9920ff7f 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -774,6 +774,18 @@ ro: top_uploads: labels: filename: Numele fișierului + top_users_by_likes_received: + labels: + user: Utilizatori + qtt_like: Aprecieri primite + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Utilizatori + qtt_like: Aprecieri primite + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Utilizatori + qtt_like: Aprecieri primite dashboard: rails_env_warning: "Serverul funcționează în modul %{env}." host_names_warning: "Fișierul config/database.yml folosește numele implicit al localhost. Actualizează pentru a folosi numele de gazdă al site-ului." diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 3ffae43c8e..53405b3818 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -1441,6 +1441,25 @@ ru: ignores_count: Количество игнорируемых mutes_count: Количество пользователей с отключёнными уведмлениями description: "Пользователи с максимальным количеством отключённых уведомлений, скрытых тем и сообщений." + top_users_by_likes_received: + title: "Топ пользователей по полученным симпатиям" + labels: + user: Пользователь + qtt_like: Получено симпатий + description: "Топ 10 пользователей, получившие больше всего симпатий" + top_users_by_likes_received_from_inferior_trust_level: + title: "Топ пользователей по количеству симпатий, полученных от пользователей с более низким уровнем доверия" + labels: + user: Пользователь + trust_level: Уровень доверия + qtt_like: Получено симпатий + description: "Топ-10 пользователей с более высоким уровнем доверия, получившие симпатии от пользователей с более низким уровнем доверия." + top_users_by_likes_received_from_a_variety_of_people: + title: "Топ пользователей по количеству симпатий, полученных от самых разных пользователей." + labels: + user: Пользователь + qtt_like: Получено симпатий + description: "Топ пользователей по количеству симпатий, полученных от самых разных пользователей." dashboard: rails_env_warning: "Ваш сервер работает в режиме %{env}." host_names_warning: "Ваш файл config/database.yml использует локальное имя хоста по умолчанию. Поменяйте его на имя хоста вашего сайта." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index c22b8537fc..4b7e63dc47 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -756,6 +756,18 @@ sk: top_uploads: labels: filename: Názov súboru + top_users_by_likes_received: + labels: + user: Používateľ + qtt_like: Obdržaných 'páči sa mi' + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Používateľ + qtt_like: Obdržaných 'páči sa mi' + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Používateľ + qtt_like: Obdržaných 'páči sa mi' dashboard: rails_env_warning: "Váš server beží v %{env} móde" host_names_warning: "Váš súbor config/database.yml používa lokálne meno hostiteľa. Aktualizujte ho menom Vašej stránky." diff --git a/config/locales/server.sl.yml b/config/locales/server.sl.yml index dfa3528ae9..da67dd4cf4 100644 --- a/config/locales/server.sl.yml +++ b/config/locales/server.sl.yml @@ -713,6 +713,18 @@ sl: top_uploads: labels: filename: Ime datoteke + top_users_by_likes_received: + labels: + user: Uporabnik + qtt_like: Všečkov prejetih + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Uporabnik + qtt_like: Všečkov prejetih + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Uporabnik + qtt_like: Všečkov prejetih dashboard: s3_cdn_warning: 'Strežnik je konfiguriran za nalaganje datotek v S3, vendar hkrati ni konfiguriran tudi S3 CDN. To lahko privede do visokih stroškov uporabe S3 in počasnejšega delovanja strani. Če želite izvedeti več, glejte "Using Object Storage for Uploads".' out_of_date_themes: "Posodobitve so na voljo za naslednje prilagoditve:" diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 527a127e2b..e5543c1d8a 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -584,6 +584,18 @@ sq: top_uploads: labels: filename: Filename + top_users_by_likes_received: + labels: + user: User + qtt_like: Pëlqime të marra + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: User + qtt_like: Pëlqime të marra + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: User + qtt_like: Pëlqime të marra dashboard: rails_env_warning: "Your server is running in %{env} mode." host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname." diff --git a/config/locales/server.sr.yml b/config/locales/server.sr.yml index 6ac81e9905..8a93d9e0ca 100644 --- a/config/locales/server.sr.yml +++ b/config/locales/server.sr.yml @@ -327,6 +327,18 @@ sr: top_uploads: labels: filename: Ime datoteke + top_users_by_likes_received: + labels: + user: Korisnik + qtt_like: Primljeno Lajkova + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Korisnik + qtt_like: Primljeno Lajkova + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Korisnik + qtt_like: Primljeno Lajkova site_settings: min_topic_title_length: "Minimalna dozvoljena dužina naslova teme u znakovima" max_topic_title_length: "Maksimalna dozvoljena dužina naslova teme u znakovima" diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 4ecbf3bd6a..06797d9e3a 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -206,6 +206,8 @@ sv: local_login_cannot_be_disabled_if_second_factor_enforced: "Du kan inte inaktivera lokal inloggning om 2FA har genomdrivits. Inaktivera genomdriven 2FA innan du inaktiverar lokala inloggningar." cannot_enable_s3_uploads_when_s3_enabled_globally: "Du kan inte aktivera S3-uppladdningar eftersom S3-uppladdningar redan är aktiverade globalt, och aktivering av denna webbplatsnivå kan orsaka kritiska problem med uppladdningar" cors_origins_should_not_have_trailing_slash: "Du bör inte lägga till det avslutande snedstrecket (/) till CORS-ursprung." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "Användaragenter måste vara minst 3 tecken långt för att undvika felaktiga hastighetsbegränsning för mänskliga användare." + slow_down_crawler_user_agent_cannot_be_popular_browsers: "Du kan inte lägga till något av följande värden till inställningen: %{values}." conflicting_google_user_id: 'Google-konto-ID:et för det här kontot har ändrats och av säkerhetsskäl krävs personalens ingripande. Kontakta personalen och hänvisa dem till
    https://meta.discourse.org/t/76575' onebox: invalid_address: "Tyvärr kunde vi inte generera en förhandsvisning av den här webbsidan eftersom servern '%{hostname}' inte kunde hittas. I stället för en förhandsvisning visas enbart en länk i ditt inlägg. :cry:" @@ -876,6 +878,7 @@ sv: others: "Inga bokmärken." no_drafts: self: "Du har inga utkast; börja att skriva ett svar i ett ämne så sparas det automatiskt som ett nytt utkast." + no_log_search_queries: "Sökloggsfrågor är för närvarande inaktiverade (en administratör kan aktivera dem i webbplatsinställningarna)." email_settings: pop3_authentication_error: "Det uppstod ett problem med de angivna POP3-autentiseringsuppgifterna. Kontrollera användarnamn och lösenord och försök igen." imap_authentication_error: "Det uppstod ett problem med de angivna IMAP-autentiseringsuppgifterna. Kontrollera användarnamn och lösenord och försök igen." @@ -1315,6 +1318,25 @@ sv: ignores_count: Räknade ignoreringar mutes_count: Räknade tystningar description: "Användare som har tystats och/eller ignorerats av många andra användare." + top_users_by_likes_received: + title: "Toppanvändare efter mottagna gillningar" + labels: + user: Användare + qtt_like: Mottagna gillningar + description: "Topp 10 användare som har fått fler gillningar." + top_users_by_likes_received_from_inferior_trust_level: + title: "Toppanvändare efter gillningar givna av en användare med lägre förtroendenivå" + labels: + user: Användare + trust_level: Förtroendenivå + qtt_like: Mottagna gillningar + description: "Topp 10 användare i en högre förtroendenivå som gillas av personer med en lägre förtroendenivå." + top_users_by_likes_received_from_a_variety_of_people: + title: "Toppanvändare efter gillningar utifrån en mängd olika personer" + labels: + user: Användare + qtt_like: Mottagna gillningar + description: "Topp 10 användare som har haft gillningar från ett brett spektrum av personer." dashboard: rails_env_warning: "Din server kör i %{env}-läge." host_names_warning: "Din config/database.yml-fil använder lokalvärdens standard-värdnamn. Uppdatera den till att använda din webbplats värdnamn." @@ -1394,6 +1416,7 @@ sv: tl2_post_edit_time_limit: "En författare på fn2+ kan redigera eller radera sitt inlägg i (n) minuter efter att ha lagt upp ett inlägg. Ange 0 för att alltid tillåta." edit_history_visible_to_public: "Tillåt att alla ser tidigare versioner av ett redigerat inlägg. Om detta inaktiveras kan endast personal se detta." delete_removed_posts_after: "Inlägg som tagits bort av författaren kommer automatiskt att raderas efter (n) timmar. Ange 0 för att omedelbart ta bort inlägg." + notify_users_after_responses_deleted_on_flagged_post: "När ett inlägg flaggas och sedan tas bort kommer alla användare som har svarat på inlägget och fått sina svar borttagna att meddelas." max_image_width: "Maxbredd för bildikoner i ett inlägg" max_image_height: "Maxhöjd för bildikoner i ett inlägg" responsive_post_image_sizes: "Ändra storlek på förhandsgranskningsbilder i Lightbox för att möjliggöra höga DPI-skärmar med följande pixelkvot. Ta bort alla värden för att inaktivera responsiva bilder." @@ -1494,6 +1517,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_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" @@ -2470,6 +2494,11 @@ sv: inappropriate: "Ditt inlägg blev flaggat som **olämpligt**. Medlemmarna i forumet kände att det är störande, kränkande eller att det strider mot [våra riktlinjer](%{base_path}/guidelines)." spam: "Ditt inlägg blev flaggat som **skräppost**. Medlemmarna i forumet kände att det är reklam, något som är överdrivet säljande istället för att vara nyttigt eller relevant för ämnet." notify_moderators: "Ditt inlägg blev flaggat **för moderators uppmärksamhet**. Medlemmarna i forumet kände att något i inlägget kräver manuellt ingripande av personal." + responder: + off_topic: "Inlägget flaggades som **off-topic**: gemenskapen tycker inte att det passar in i ämnet, vilket för närvarande definieras av titeln samt det första inlägget." + inappropriate: "Inlägget flaggades som **olämpligt**: gemenskapen tycker att det är stötande, kränkande eller ett brott mot [våra riktlinjer för gemenskapen](%{base_path}/guidelines)." + spam: "Inlägget flaggades som **skräppost**: gemenskapen tycker att det är en reklam, något som är överdrivet marknadsföringsmässigt i stället för att vara användbart eller relevant för ämnet vilket förväntas." + notify_moderators: "Inlägget flaggades **för moderators uppmärksamhet**: gemenskapen anser att något med inlägget kräver manuellt ingripande av en medarbetare." flags_dispositions: agreed: "Tack för att du meddelade oss. Vi håller med om att det finns ett problem och vi undersöker det." agreed_and_deleted: "Tack för att du meddelade oss. Vi håller med om att det finns ett problem och vi har raderat inlägget." @@ -2560,6 +2589,23 @@ sv: ``` Läs mer ingående information i våra [forumriktlinjer](%{base_url}/guidelines). + flags_agreed_and_post_deleted_for_responders: + title: "Svar borttaget från flaggat inlägg av personal" + subject_template: "Svar borttaget från flaggat inlägg av personal" + text_body_template: | + Hejsan, + + Detta är ett automatiskt meddelande från %{site_name} för att informera dig om att ett [inlägg](%{base_url}%{url}) som du svarade på togs bort. + + %{flag_reason} + + Detta inlägg flaggades av forumet och personalen valde att radera det. + + ``` markdown + %{flagged_post_raw_content} + ``` + + För mer detaljerad förklaring av orsaken vänligen studera våra [forumriktlinjer](%{base_url}/guidelines). usage_tips: text_body_template: | För att få ett par tips och förslag hur du kommer igång som ny användare, [gå till detta blogginlägg](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). diff --git a/config/locales/server.sw.yml b/config/locales/server.sw.yml index bd24784c51..68b0017193 100644 --- a/config/locales/server.sw.yml +++ b/config/locales/server.sw.yml @@ -745,6 +745,18 @@ sw: top_uploads: labels: filename: Jina la faili + top_users_by_likes_received: + labels: + user: Mtumiaji + qtt_like: Umepokea Likes + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Mtumiaji + qtt_like: Umepokea Likes + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Mtumiaji + qtt_like: Umepokea Likes dashboard: rails_env_warning: "Seva inafanyakazi ndani ya %{env} halitumizi." host_names_warning: "Fili lako la config/database.yml linatumia localhost hostname. Sasisha ili itumie hostname ya tovuti yako." diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index 195842014c..41a4ca8094 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -523,6 +523,18 @@ te: top_uploads: labels: filename: దస్త్రం పేరు + top_users_by_likes_received: + labels: + user: సభ్యుడు + qtt_like: అందుకున్న ఇష్టాలు + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: సభ్యుడు + qtt_like: అందుకున్న ఇష్టాలు + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: సభ్యుడు + qtt_like: అందుకున్న ఇష్టాలు dashboard: rails_env_warning: "మీ సర్వరు %(env) రీతిలో నడుస్తోంది" host_names_warning: "మీ config/database.yml దస్త్రం అప్రమేయ లోకల్ హోస్ట్ వాడుతున్నది. దాన్ని మీ సైటు పేరుకు మార్చగలరు. " diff --git a/config/locales/server.th.yml b/config/locales/server.th.yml index 4405684511..869b857573 100644 --- a/config/locales/server.th.yml +++ b/config/locales/server.th.yml @@ -278,6 +278,18 @@ th: top_uploads: labels: filename: ชื่อไฟล์ + top_users_by_likes_received: + labels: + user: ผู้ใช้ + qtt_like: ได้รับการชอบ + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: ผู้ใช้ + qtt_like: ได้รับการชอบ + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: ผู้ใช้ + qtt_like: ได้รับการชอบ site_settings: disabled: "ปิดใช้งานแล้ว" dark_mode_none: "ไม่มี" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index e90bd66b32..7b614c5ff3 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -1329,6 +1329,18 @@ tr_TR: ignores_count: Yoksayma toplamı mutes_count: Susturma toplamı description: "Diğer birçok kullanıcı tarafından susturulmuş veya yok sayılmış kullanıcılar." + top_users_by_likes_received: + labels: + user: Kullanıcı + qtt_like: Alınan Beğeniler + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Kullanıcı + qtt_like: Alınan Beğeniler + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Kullanıcı + qtt_like: Alınan Beğeniler dashboard: rails_env_warning: "Sunucunuz %{env} modunda çalışıyor." host_names_warning: "config/database.yml dosyasınızda, bilgisayar adı olarak öntanımlı değer olan \"localhost\" ayarlı. Değeri, sitenizin bilgisayar adını kullanacak biçimde güncelleyiniz." diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 86f68601e6..a7e2e0bfa7 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -1432,6 +1432,18 @@ uk: ignores_count: Проігноровано mutes_count: Приглушено description: "Користувачі, які були відключені та/або проігноровані іншими користувачами." + top_users_by_likes_received: + labels: + user: Користувач + qtt_like: Отримані вподобання + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Користувач + qtt_like: Отримані вподобання + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Користувач + qtt_like: Отримані вподобання dashboard: rails_env_warning: "Ваш сервер працює в режимі %{env}." host_names_warning: "Ваш файл config/database.yml використовує локальне ім’я хоста за замовчуванням. Поміняйте його на ім’я хоста вашого веб-сайту." diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index e81f5abb10..6f530cd7a7 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -1111,6 +1111,18 @@ ur: ignores_count: شمار کو نظر انداز mutes_count: شمار کو خاموش description: "جو صارفین بہت سے دوسرے صارفین کی طرف سے خاموش اور/یا نظر انداز کیے گئے ہیں۔" + top_users_by_likes_received: + labels: + user: صارف + qtt_like: لائیکس موصول ہوے + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: صارف + qtt_like: لائیکس موصول ہوے + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: صارف + qtt_like: لائیکس موصول ہوے dashboard: rails_env_warning: "آپ کا سرور %{env} مَوڈ میں چل رہا ہے۔" host_names_warning: "آپ کی config/database.yml فائل ڈیفالٹ localhost نام استعمال کر رہا ہے۔ اِسے اپنے سائیٹ ہوسٹ کے نام پر اپ ڈیٹ کریں۔" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index 6aa84b65ef..616e90052a 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -850,6 +850,18 @@ vi: labels: filename: Tên tập tin author: Tác giả + top_users_by_likes_received: + labels: + user: Người dùng + qtt_like: Likes Đã Nhận + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: Người dùng + qtt_like: Likes Đã Nhận + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: Người dùng + qtt_like: Likes Đã Nhận dashboard: rails_env_warning: "Máy chủ của bạn đang chạy trong chế độ %{env}." host_names_warning: "Cài đặt của bạn config/database.yml đang sử dụng hostname mặc định. Cập nhật lại để sử dụng hostname của bạn" diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 0dd8d4a112..f78807be8d 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -1283,6 +1283,18 @@ zh_CN: ignores_count: 被忽略次数 mutes_count: 被设为免打扰的次数 description: "被许多其他用户设为免打扰和/或忽略的用户。" + top_users_by_likes_received: + labels: + user: 用户 + qtt_like: 获赞 + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: 用户 + qtt_like: 获赞 + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: 用户 + qtt_like: 获赞 dashboard: rails_env_warning: "您的服务器正在以%{env}模式运行。" host_names_warning: "您的 config/database.yml 文件使用的是默认的 localhost 主机名。请更新为您的站点主机名。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 7dc96149f1..38bd36787f 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -1048,6 +1048,18 @@ zh_TW: ignores_count: 忽略數 mutes_count: 禁言數 description: "使用者被許多使用者靜音或忽略。" + top_users_by_likes_received: + labels: + user: 使用者 + qtt_like: 收到的讚 + top_users_by_likes_received_from_inferior_trust_level: + labels: + user: 使用者 + qtt_like: 收到的讚 + top_users_by_likes_received_from_a_variety_of_people: + labels: + user: 使用者 + qtt_like: 收到的讚 dashboard: rails_env_warning: "伺服器現在運行 %{env} 模式。" host_names_warning: "伺服器上 config/database.yml 的主機名稱為 localhost 。請更改成你的網站主機名稱。" diff --git a/plugins/discourse-local-dates/config/locales/client.da.yml b/plugins/discourse-local-dates/config/locales/client.da.yml index 126c6f28f2..0ecdd38016 100644 --- a/plugins/discourse-local-dates/config/locales/client.da.yml +++ b/plugins/discourse-local-dates/config/locales/client.da.yml @@ -40,3 +40,4 @@ da: every_three_months: "Hver tredje måned" every_six_months: "Hvert halvår" every_year: "Hvert år" + default_title: "%{site_name} Begivenhed" diff --git a/plugins/discourse-narrative-bot/config/locales/server.lt.yml b/plugins/discourse-narrative-bot/config/locales/server.lt.yml index 0544b5d0b4..2a1b046ea0 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.lt.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.lt.yml @@ -12,6 +12,7 @@ lt: discourse_narrative_bot_disable_public_replies: "Išjunkite viešus atsakymus naudodami „Discourse Narrative Bot“" discourse_narrative_bot_welcome_post_type: "Sveikinimo įrašo tipas, kurį turėtų išsiųsti Discourse pasakojimo robotas" discourse_narrative_bot_welcome_post_delay: "Palaukite (n) sekundžių prieš siųsdami „Discourse Narrative Bot“ pasveikinimo įrašą." + discourse_narrative_bot_skip_tutorials: "Discourse Narrative Bot konsultacijos, kuriuos reikia praleisti" badges: certified: name: Sertifikuota @@ -19,6 +20,7 @@ lt: long_description: | Šis ženklelis suteikiamas sėkmingai užbaigus interaktyvią naują vartotojo pamoką. Jūs ėmėtės iniciatyvos išmokti pagrindines diskusijų priemones, o dabar esate sertifikuotas! licensed: + name: Licencijuota description: "Baigėme pažengusio vartotojo vadovą" long_description: | Šis ženklelis suteikiamas sėkmingai baigus interaktyvią pažengusio vartotojo mokymo programą. Įvaldėte pažangias diskusijų priemones - dabar esate visiškai licencijuotas! @@ -28,28 +30,59 @@ lt: subject_template: "Dabar, kai esate paaukštintas, atėjo laikas sužinoti apie kai kurias išplėstines funkcijas!" text_body_template: | Atsakykite į šį pranešimą naudodami „@%{discobot_username} %{reset_trigger}“, kad sužinotumėte daugiau apie tai, ką galite padaryti. + timeout: + message: |- + Ei,%{username}, tik tikrinu, nes kurį laiką apie tave negirdėjau. + + - Jei norite tęsti, atsakykite man bet kuriuo metu. + + - Jei norite praleisti šį veiksmą, pasakykite „%{skip_trigger}“. + + - Norėdami pradėti iš naujo, pasakykite „%{reset_trigger}“. + + Jei nenorite, tai taip pat gerai. Aš robotas. Tu nepakenksi mano jausmams. :sob: dice: + trigger: "ridenimas" invalid: |- Atsiprašau, matematiškai neįmanoma mesti to kauliukų derinio. :confounded: + out_of_range: |- + Ar žinojote, kad [didžiausias pusių skaičius](https://www.wired.com/2016/05/mathematical-challenge-of-designing-the-worlds-most-complex-120-sided-dice) matematiškai teisingas kauliukas yra 120? + results: |- + > :game_die: %{results} quote: trigger: "citata" "1": + quote: "Kiekvieno sudėtingumo viduryje slypi galimybė" author: "Albertas Einšteinas" + "2": + quote: "Laisvės neverta turėti, jei ji nereiškia laisvės klysti." + author: "Mahatma Gandhi" + "3": + quote: "Neverk, nes viskas baigėsi, šypsokis, nes taip atsitiko." + author: "Dr Seuss" "4": + quote: "Jei norite, kad kažkas būtų padaryta teisingai, padarykite tai patys." author: "Charles-Guillaume Étienne" "5": + quote: "Tikėk, kad gali, ir tu jau pusiaukelėje." author: "Teodoras Ruzveltas" "6": quote: "Gyvenimas yra kaip šokolado dėžutė. Niekada nežinai, ką gausi." + author: "Forresto Gumpo mama" "7": + quote: "Tai vienas mažas žingsnelis žmogui, milžiniškas šuolis žmonijai." author: "Neilas Armstrongas" "8": quote: "Kasdien darykite vieną dalyką, kuris jus gąsdina." + author: "Eleanor Roosevelt" "9": quote: "Klaidos visada atleidžiamos, jei žmogus turi drąsos jas pripažinti." author: "Bruce Lee" "10": + quote: "Ką žmogaus protas gali įsivaizduoti ir kuo tikėti, tą jis gali ir pasiekti." author: "Napoleono kalnas" + results: |- + > :left_speech_bubble: _%{quote}_ — %{author} magic_8_ball: trigger: "likimas" answers: @@ -82,37 +115,118 @@ lt: random_mention: reply: |- Sveiki! Norėdami sužinoti, ką galiu padaryti, pasakykite „@%{discobot_username} %{help_trigger}“. + tracks: |- + Šiuo metu žinau, kaip atlikti šiuos dalykus: + + `@%{discobot_username} %{reset_trigger} {name-of-tutorial}` + > Pradeda interaktyvią mokymo programą, skirtą tik jums, asmenine žinute. „{name-of-tutorial}“ gali būti vienas iš: „%{tracks}“. + bot_actions: |- + `@%{discobot_username} %{dice_trigger} 2d6` + > :game_die: 3, 6 + + ` @%{discobot_username} %{quote_trigger}` + %{quote_sample} + + ` @%{discobot_username} %{magic_8_ball_trigger}` + > :crystal_ball: Taip gali remtis + do_not_understand: + first_response: |- + Ei, ačiū už atsakymą! + + Deja, kaip blogai užprogramuotas robotas, aš negaliu to suprasti. :frowning: + track_response: Galite bandyti dar kartą arba, jei norite praleisti šį veiksmą, pasakykite „%{skip_trigger}“. Priešingu atveju, norėdami pradėti iš naujo, pasakykite „%{reset_trigger}“. + second_response: |- + O, atsiprašau. Vis dar nesuprantu. :anguished: + + Esu tik robotas, bet jei norite susisiekti su tikru asmeniu, žr. [mūsų kontaktų puslapį](%{base_path}/apie). + + Tuo tarpu aš nepasirodysiu tau kelyje. new_user_narrative: reset_trigger: "pamoka" + title: "Naujo vartotojo mokymo programos užbaigimo sertifikatas" hello: title: "Sveikinimai!" likes: + instructions: |- + Štai vienaragio nuotrauka: + + + + Jei jums tai patinka (o kam nepatiktų!), eikite į priekį ir paspauskite :heart: mygtuką, esantį po šiuo įrašu, kad praneštumėte man. reply: |- Ačiū, kad patinka mano įrašas! not_found: |- Ar pamiršote pamėgti :heart: mano [įrašą?] (%{url}) :crying_cat_face: + formatting: + reply: |- + Puikus darbas! HTML ir BBCode taip pat tinka formatavimui – norėdami sužinoti daugiau, [išbandykite šią mokymo programą](http://commonmark.org/help) :nerd: quoting: reply: |- Puikus darbas, pasirinkote mano mėgstamiausią citatą! :left_speech_bubble: mention: + instructions: |- + Kartais galbūt norėsite atkreipti asmens dėmesį, net jei neatsakote į juos tiesiogiai. Įveskite `@`, tada užpildykite savo vartotojo vardą, kad juos paminėtumėte. + + Ar galite paminėti **`@%{discobot_username}`** savo atsakyme? reply: |- _Ar kas nors pasakė mano vardą!? _ :raised_hand: Tikiu, kad pasakėte! :wave: Na, aš čia! Ačiū, kad paminėjote mane. :ok_hand: + not_found: |- + Niekur nematau savo vardo :frowning: Ar galite pabandyti dar kartą paminėti mane kaip “@%{discobot_username}”? + + (Ir taip, mano vartotojo vardas parašytas _disco_, kaip 1970-ųjų šokių traku. Aš [myliu naktinį gyvenimą!] (https://www.youtube.com/watch?v=B_wGI3_sGf8) :dancer:) + end: + message: |- + Ačiū, kad palaikote mane @%{username}! Sukūriau tai jums, manau, kad jūs tai uždirbote: + + %{certificate} + + Kol kas tai viskas! Peržiūrėkite [**naujausias mūsų diskusijų temas**](%{base_uri}/naujausias) arba [**diskusijų kategorijas**](%{base_uri}/kategorijos). :sunglasses: + + (Jei norite dar kartą su manimi pasikalbėti ir sužinoti daugiau, bet kuriuo metu%{discobot_username} certificate: alt: "Pasiekimo pažymėjimas" advanced_user_narrative: reset_trigger: "išplėstinė pamoka" + cert_title: "Sėkmingai baigus pažangaus vartotojo mokymo programą" title: ":arrow_up: Išplėstinės vartotojo funkcijos" + start_message: |- + Kaip _pažengęs_ vartotojas, ar jau lankėtės [jūsų nuostatų puslapyje](%{base_uri}/my/preferences) @%{username}? Yra daug būdų pritaikyti savo patirtį, pavyzdžiui, pasirinkti tamsią arba šviesią temą. + + Bet aš nukrypstu, pradėkime! edit: + bot_created_post_raw: "@%{discobot_username} yra pats šauniausias mano žinomas robotas :wink:" + instructions: |- + Visi daro klaidų. Bet nesijaudinkite, visada galite redaguoti savo įrašus, kad juos pataisytumėte! + + Ar galite pradėti nuo **redagavimo** įrašo, kurį ką tik sukūriau jūsų vardu? reply: |- Puikus darbas! Atminkite, kad po 5 minučių atlikti pakeitimai bus rodomi kaip viešos redagavimo peržiūros, o viršutiniame dešiniajame kampe bus rodoma pieštuko piktograma su peržiūrų skaičiumi. category_hashtag: + instructions: |- + Ar žinojote, kad savo įraše galite nurodyti kategorijas ir žymas? Pavyzdžiui, ar matėte %{category} kategoriją? + + Įveskite „#“ sakinio viduryje ir pasirinkite bet kurią kategoriją arba žymą. + not_found: |- + Hmm, niekur nematau kategorijos. Atminkite, kad „#“ negali būti pirmasis simbolis. Ar galite tai nukopijuoti kitame atsakyme? + + ```tekstas + Galiu sukurti kategorijos nuorodą per # + ``` reply: |- Puikiai! Atminkite, kad tai veikia abiejų kategorijų _ir_ žymas, jei žymės įjungtos. change_topic_notification_level: not_found: |- Panašu, kad vis dar žiūrite :eyes: šią temą! Jei jums sunku rasti, pranešimo lygio mygtukas yra temos apačioje. + poll: + reply: |- + Sveiki, gera apklausa! Kaip man sekėsi tave mokyti? + + [poll] + * :+1: + * :-1: + [/poll] details: not_found: |- Kyla sunkumų kuriant išsamios informacijos valdiklį? Į kitą atsakymą pabandykite įtraukti: diff --git a/plugins/poll/config/locales/client.lt.yml b/plugins/poll/config/locales/client.lt.yml index c3d1673439..7b27f328e4 100644 --- a/plugins/poll/config/locales/client.lt.yml +++ b/plugins/poll/config/locales/client.lt.yml @@ -53,6 +53,7 @@ lt: percentage: "Procentas" count: "Skaičiuoti" error_while_toggling_status: "Atsiprašome, perjungiant šios apklausos būseną įvyko klaida." + error_while_casting_votes: "Atsiprašome, balsuojant įvyko klaida." error_while_fetching_voters: "Atsiprašome, rodant rinkėjus įvyko klaida." error_while_exporting_results: "Apgailestaujame, eksportuojant apklausos rezultatus įvyko klaida." ui_builder: @@ -78,6 +79,7 @@ lt: label: Balsavimo apribojimas šioms grupėms poll_chart_type: label: Rezultatų diagrama + bar: Juosta poll_config: max: Maksimalūs pasirinkimai min: Minimalūs pasirinkimai diff --git a/plugins/poll/config/locales/server.lt.yml b/plugins/poll/config/locales/server.lt.yml index 455c8d1bbc..a9a0e63603 100644 --- a/plugins/poll/config/locales/server.lt.yml +++ b/plugins/poll/config/locales/server.lt.yml @@ -10,12 +10,14 @@ lt: poll_maximum_options: "Maskimalus įterpiamų įrašų skaičius" poll_edit_window_mins: "Minutės po įrašo sukūrimo, per kurias galima redaguoti apklausas." poll_minimum_trust_level_to_create: "Nustatykite minimalų patikimumo lygį, reikalingą apklausoms kurti." + poll_export_data_explorer_query_id: "„Data Explorer“ užklausos ID, naudojamas eksportuoti apklausos rezultatus (0, kad išjungtumėte)." poll: poll: "apklausa" invalid_argument: "Netinkama argumento „%{argument}“ reikšmė „%{value}“." multiple_polls_without_name: "Yra keli baseinai be pavadinimo. Naudokite ' vardą ' atributas vienareikšmiškai identifikuoti savo apklausas." multiple_polls_with_same_name: "There are multiple polls with the same name: %{name}. Use the 'name' attribute to uniquely identify your polls." default_poll_must_have_at_least_1_option: "Apklausoje turi būti bent 1 pasirinkimas." + named_poll_must_have_at_least_1_option: "Apklausa, pavadinta %{name} , turi turėti bent 1 variantą." default_poll_must_have_less_options: one: "Apklausa turi turėti mažiau nei %{count} variantą." few: "Apklausa turi turėti mažiau nei %{count} variantų." @@ -28,9 +30,13 @@ lt: other: "Poll named %{name} must have less than %{count} options." default_poll_must_have_different_options: "Apklausa turi turėti skirtingus atsakymus." named_poll_must_have_different_options: "Apklausa pavadinimu %{name} turi turėti bent jau 2 variantus." + default_poll_must_not_have_any_empty_options: "Apklausa neturi turėti jokių tuščių variantų." + named_poll_must_not_have_any_empty_options: "Apklausa, pavadinta %{name} , neturi turėti jokių tuščių variantų." default_poll_with_multiple_choices_has_invalid_parameters: "Apklausa su keliais pasirinkimų yra neleistinų parametrus." named_poll_with_multiple_choices_has_invalid_parameters: "Apklausa pavadintas % {name} , su daug pasirinkimo yra neleistinų parametrus." requires_at_least_1_valid_option: "Jūs privalote pasirinkti bent 1 galiojantį variantą." + edit_window_expired: + cannot_edit_default_poll_with_votes: "Negalite pakeisti apklausos po pirmųjų %{minutes} minučių." no_poll_with_this_name: "Nėra baseinas pavadintas % {name} , susijęs su šio pranešimo." post_is_deleted: "Negali veikti ištrintų paštu." user_cant_post_in_topic: "Negalite balsuoti, nes negalite rašyti šioje temoje." diff --git a/plugins/styleguide/config/locales/client.lt.yml b/plugins/styleguide/config/locales/client.lt.yml index 671ee2b596..c0a7baa924 100644 --- a/plugins/styleguide/config/locales/client.lt.yml +++ b/plugins/styleguide/config/locales/client.lt.yml @@ -17,6 +17,7 @@ lt: typography: title: "Tipografija" example: "Sveiki atvykę į Discourse" + paragraph: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tai yra minim man, bet tai, kad pratimas gali būti ne taip, kaip jis gali būti susijęs su pasekmėmis. Duis aute irure dolor in reprehenderit in voluptate velit esse lum dolore eu fugiat nulla pariatur. Išskirtinis yra ne proident occaecat cupidatat, kuri yra oficiali, bet kurioje vietoje yra darbo vietoje." date_time_inputs: title: "Įvesties data/laikas" font_scale: From 03b0c9f267571af7359de040f55db7b7358f3723 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Tue, 7 Dec 2021 11:40:00 -0500 Subject: [PATCH 058/119] A11Y: Remove dupe label on signup confirm field (#15212) --- .../discourse/app/templates/components/user-fields/confirm.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/templates/components/user-fields/confirm.hbs b/app/assets/javascripts/discourse/app/templates/components/user-fields/confirm.hbs index a60986f98b..d0e67af3ca 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-fields/confirm.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-fields/confirm.hbs @@ -1,5 +1,5 @@ {{#if this.field.name}} -
    {{plugin-outlet name="groups-form-membership-below-automatic" diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js index 15c5c0d56f..ffe5b0bf25 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-membership-test.js @@ -7,9 +7,24 @@ import { import { click, visit } from "@ember/test-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import { test } from "qunit"; +import Site from "discourse/models/site"; acceptance("Managing Group Membership", function (needs) { needs.user(); + needs.pretender((server, helper) => { + server.get("/associated_groups", () => + helper.response({ + associated_groups: [ + { + id: 123, + name: "test-group", + provider_name: "google_oauth2", + label: "google_oauth2:test-group", + }, + ], + }) + ); + }); test("As an admin", async function (assert) { updateCurrentUser({ can_create_group: true }); @@ -94,6 +109,36 @@ acceptance("Managing Group Membership", function (needs) { assert.strictEqual(emailDomains.header().value(), "foo.com"); }); + test("As an admin on a site that can associate groups", async function (assert) { + let site = Site.current(); + site.set("can_associate_groups", true); + updateCurrentUser({ can_create_group: true }); + + await visit("/g/alternative-group/manage/membership"); + + const associatedGroups = selectKit( + ".group-form-automatic-membership-associated-groups" + ); + await associatedGroups.expand(); + await associatedGroups.selectRowByName("google_oauth2:test-group"); + await associatedGroups.keyboard("enter"); + + assert.equal(associatedGroups.header().name(), "google_oauth2:test-group"); + }); + + test("As an admin on a site that can't associate groups", async function (assert) { + let site = Site.current(); + site.set("can_associate_groups", false); + updateCurrentUser({ can_create_group: true }); + + await visit("/g/alternative-group/manage/membership"); + + assert.ok( + !exists('label[for="automatic_membership_associated_groups"]'), + "it should not display associated groups automatic membership label" + ); + }); + test("As a group owner", async function (assert) { updateCurrentUser({ moderator: false, admin: false }); @@ -104,6 +149,11 @@ acceptance("Managing Group Membership", function (needs) { "it should not display automatic membership label" ); + assert.ok( + !exists('label[for="automatic_membership_associated_groups"]'), + "it should not display associated groups automatic membership label" + ); + assert.ok( !exists(".groups-form-automatic-membership-retroactive"), "it should not display automatic membership retroactive checkbox" diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index f80dfe27c5..33944246e8 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -180,6 +180,10 @@ class Admin::GroupsController < Admin::AdminController custom_fields = DiscoursePluginRegistry.editable_group_custom_fields permitted << { custom_fields: custom_fields } unless custom_fields.blank? + if guardian.can_associate_groups? + permitted << { associated_group_ids: [] } + end + permitted = permitted | DiscoursePluginRegistry.group_params params.require(:group).permit(permitted) diff --git a/app/controllers/associated_groups_controller.rb b/app/controllers/associated_groups_controller.rb new file mode 100644 index 0000000000..cf82ae20c7 --- /dev/null +++ b/app/controllers/associated_groups_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AssociatedGroupsController < ApplicationController + requires_login + + def index + guardian.ensure_can_associate_groups! + render_serialized(AssociatedGroup.all, AssociatedGroupSerializer, root: 'associated_groups') + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 6fbb21044f..6a2d6d37d1 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -736,6 +736,10 @@ class GroupsController < ApplicationController end end + if guardian.can_associate_groups? + permitted_params << { associated_group_ids: [] } + end + permitted_params = permitted_params | DiscoursePluginRegistry.group_params params.require(:group).permit(*permitted_params) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 57c794f9cc..1047d6f9ff 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -171,6 +171,7 @@ class Users::OmniauthCallbacksController < ApplicationController elsif Guardian.new(user).can_access_forum? && user.active # log on any account that is active with forum access begin user.save! if @auth_result.apply_user_attributes! + @auth_result.apply_associated_attributes! rescue ActiveRecord::RecordInvalid => e @auth_result.failed = true @auth_result.failed_reason = e.record.errors.full_messages.join(", ") diff --git a/app/jobs/scheduled/clean_up_associated_groups.rb b/app/jobs/scheduled/clean_up_associated_groups.rb new file mode 100644 index 0000000000..9b3514798b --- /dev/null +++ b/app/jobs/scheduled/clean_up_associated_groups.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Jobs + class CleanUpAssociatedGroups < ::Jobs::Scheduled + every 1.day + + def execute(args) + AssociatedGroup.cleanup! + end + end +end diff --git a/app/models/associated_group.rb b/app/models/associated_group.rb new file mode 100644 index 0000000000..9d6e9365dc --- /dev/null +++ b/app/models/associated_group.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +class AssociatedGroup < ActiveRecord::Base + has_many :user_associated_groups, dependent: :destroy + has_many :users, through: :user_associated_groups + has_many :group_associated_groups, dependent: :destroy + has_many :groups, through: :group_associated_groups + + def label + "#{provider_name}:#{name}" + end + + def self.has_provider? + Discourse.enabled_authenticators.any? { |a| a.provides_groups? } + end + + def self.cleanup! + AssociatedGroup.left_joins(:group_associated_groups, :user_associated_groups) + .where("group_associated_groups.id IS NULL AND user_associated_groups.id IS NULL") + .where("last_used < ?", 1.week.ago).delete_all + end +end + +# == Schema Information +# +# Table name: associated_groups +# +# id :bigint not null, primary key +# name :string not null +# provider_name :string not null +# provider_id :string not null +# last_used :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# associated_groups_provider_id (provider_name,provider_id) UNIQUE +# diff --git a/app/models/group.rb b/app/models/group.rb index 37e08a23db..ebb703e88c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -21,6 +21,7 @@ class Group < ActiveRecord::Base has_many :group_users, dependent: :destroy has_many :group_requests, dependent: :destroy has_many :group_mentions, dependent: :destroy + has_many :group_associated_groups, dependent: :destroy has_many :group_archived_messages, dependent: :destroy @@ -32,6 +33,7 @@ class Group < ActiveRecord::Base has_many :reviewables, foreign_key: :reviewable_by_group_id, dependent: :nullify has_many :group_category_notification_defaults, dependent: :destroy has_many :group_tag_notification_defaults, dependent: :destroy + has_many :associated_groups, through: :group_associated_groups, dependent: :destroy belongs_to :flair_upload, class_name: 'Upload' belongs_to :smtp_updated_by, class_name: 'User' @@ -768,6 +770,20 @@ class Group < ActiveRecord::Base self end + def add_automatically(user, subject: nil) + if users.exclude?(user) && add(user) + logger = GroupActionLogger.new(Discourse.system_user, self) + logger.log_add_user_to_group(user, subject) + end + end + + def remove_automatically(user, subject: nil) + if users.include?(user) && remove(user) + logger = GroupActionLogger.new(Discourse.system_user, self) + logger.log_remove_user_from_group(user, subject) + end + end + def staff? STAFF_GROUPS.include?(self.name.to_sym) end diff --git a/app/models/group_associated_group.rb b/app/models/group_associated_group.rb new file mode 100644 index 0000000000..be71f09eb0 --- /dev/null +++ b/app/models/group_associated_group.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +class GroupAssociatedGroup < ActiveRecord::Base + belongs_to :group + belongs_to :associated_group + + after_commit :add_associated_users, on: [:create, :update] + before_destroy :remove_associated_users + + def add_associated_users + with_mutex do + associated_group.users.in_batches do |users| + users.each do |user| + group.add_automatically(user, subject: associated_group.label) + end + end + end + end + + def remove_associated_users + with_mutex do + User.where("NOT EXISTS( + SELECT 1 + FROM user_associated_groups uag + JOIN group_associated_groups gag + ON gag.associated_group_id = uag.associated_group_id + WHERE uag.user_id = users.id + AND gag.id != :gag_id + AND gag.group_id = :group_id + )", gag_id: id, group_id: group_id).in_batches do |users| + users.each do |user| + group.remove_automatically(user, subject: associated_group.label) + end + end + end + end + + private + + def with_mutex + DistributedMutex.synchronize("group_associated_group_#{group_id}_#{associated_group_id}") do + yield + end + end +end + +# == Schema Information +# +# Table name: group_associated_groups +# +# id :bigint not null, primary key +# group_id :bigint not null +# associated_group_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_group_associated_groups (group_id,associated_group_id) UNIQUE +# index_group_associated_groups_on_associated_group_id (associated_group_id) +# index_group_associated_groups_on_group_id (group_id) +# diff --git a/app/models/user.rb b/app/models/user.rb index fc62d99047..d610cc524f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,6 +38,7 @@ class User < ActiveRecord::Base has_many :reviewable_scores, dependent: :destroy has_many :invites, foreign_key: :invited_by_id, dependent: :destroy has_many :user_custom_fields, dependent: :destroy + has_many :user_associated_groups, dependent: :destroy has_many :pending_posts, -> { merge(Reviewable.pending) }, class_name: 'ReviewableQueuedPost', foreign_key: :created_by_id has_one :user_option, dependent: :destroy @@ -83,6 +84,7 @@ class User < ActiveRecord::Base has_many :topics_allowed, through: :topic_allowed_users, source: :topic has_many :groups, through: :group_users has_many :secure_categories, through: :groups, source: :categories + has_many :associated_groups, through: :user_associated_groups, dependent: :destroy # deleted in user_second_factors relationship has_many :totps, -> { diff --git a/app/models/user_associated_group.rb b/app/models/user_associated_group.rb new file mode 100644 index 0000000000..1413efb7c6 --- /dev/null +++ b/app/models/user_associated_group.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class UserAssociatedGroup < ActiveRecord::Base + belongs_to :user + belongs_to :associated_group + + after_commit :add_to_associated_groups, on: [:create, :update] + before_destroy :remove_from_associated_groups + + def add_to_associated_groups + associated_group.groups.each do |group| + group.add_automatically(user, subject: associated_group.label) + end + end + + def remove_from_associated_groups + Group.where("NOT EXISTS( + SELECT 1 + FROM user_associated_groups uag + JOIN group_associated_groups gag + ON gag.associated_group_id = uag.associated_group_id + WHERE uag.user_id = :user_id + AND uag.id != :uag_id + AND gag.group_id = groups.id + )", uag_id: id, user_id: user_id).each do |group| + group.remove_automatically(user, subject: associated_group.label) + end + end +end + +# == Schema Information +# +# Table name: user_associated_groups +# +# id :bigint not null, primary key +# user_id :bigint not null +# associated_group_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_user_associated_groups (user_id,associated_group_id) UNIQUE +# index_user_associated_groups_on_associated_group_id (associated_group_id) +# index_user_associated_groups_on_user_id (user_id) +# diff --git a/app/serializers/associated_group_serializer.rb b/app/serializers/associated_group_serializer.rb new file mode 100644 index 0000000000..db7dec8723 --- /dev/null +++ b/app/serializers/associated_group_serializer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AssociatedGroupSerializer < ApplicationSerializer + attributes :id, + :name, + :provider_name, + :label +end diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb index 826b2c6533..0b51aed01b 100644 --- a/app/serializers/group_show_serializer.rb +++ b/app/serializers/group_show_serializer.rb @@ -36,7 +36,8 @@ class GroupShowSerializer < BasicGroupSerializer :imap_old_emails, :imap_new_emails, :message_count, - :allow_unknown_sender_topic_replies + :allow_unknown_sender_topic_replies, + :associated_group_ids def self.admin_or_owner_attributes(*attrs) attributes(*attrs) @@ -121,6 +122,14 @@ class GroupShowSerializer < BasicGroupSerializer end end + def associated_group_ids + object.associated_groups.map(&:id) + end + + def include_associated_group_ids? + scope.can_associate_groups? + end + private def authenticated? diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 29972ae8f5..c65628748f 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -21,6 +21,7 @@ class SiteSerializer < ApplicationSerializer :can_tag_pms, :tags_filter_regexp, :top_tags, + :can_associate_groups, :wizard_required, :topic_featured_link_allowed_category_ids, :user_themes, @@ -134,6 +135,14 @@ class SiteSerializer < ApplicationSerializer scope.can_tag_pms? end + def can_associate_groups + scope.can_associate_groups? + end + + def include_can_associate_groups? + scope.is_admin? + end + def include_tags_filter_regexp? SiteSetting.tagging_enabled end diff --git a/app/services/group_action_logger.rb b/app/services/group_action_logger.rb index 9fb65afbde..d38d0e7c5a 100644 --- a/app/services/group_action_logger.rb +++ b/app/services/group_action_logger.rb @@ -21,17 +21,19 @@ class GroupActionLogger )) end - def log_add_user_to_group(target_user) + def log_add_user_to_group(target_user, subject = nil) GroupHistory.create!(default_params.merge( action: GroupHistory.actions[:add_user_to_group], - target_user: target_user + target_user: target_user, + subject: subject )) end - def log_remove_user_from_group(target_user) + def log_remove_user_from_group(target_user, subject = nil) GroupHistory.create!(default_params.merge( action: GroupHistory.actions[:remove_user_from_group], - target_user: target_user + target_user: target_user, + subject: subject )) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4c5c10671e..05a88bd799 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4124,6 +4124,7 @@ en: automatic_membership_user_count: one: "%{count} user has the new email domains and will be added to the group." other: "%{count} users have the new email domains and will be added to the group." + automatic_membership_associated_groups: "Users who are members of a group on a service listed here will be automatically added to this group when they log in with the service." primary_group: "Automatically set as primary group" name_placeholder: "Group name, no spaces, same as username rule" primary: "Primary Group" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b46fb39dac..a85fe23ffb 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1724,6 +1724,7 @@ en: google_oauth2_client_secret: "Client secret of your Google application." google_oauth2_prompt: "An optional space-delimited list of string values that specifies whether the authorization server prompts the user for reauthentication and consent. See https://developers.google.com/identity/protocols/OpenIDConnect#prompt for the possible values." google_oauth2_hd: "An optional Google Apps Hosted domain that the sign-in will be limited to. See https://developers.google.com/identity/protocols/OpenIDConnect#hd-param for more details." + google_oauth2_hd_groups: "(experimental) Retrieve users' Google groups on the hosted domain on authentication. Retrieved Google groups can be used to grant automatic Discourse group membership (see group settings)." enable_twitter_logins: "Enable Twitter authentication, requires twitter_consumer_key and twitter_consumer_secret. See Configuring Twitter login (and rich embeds) for Discourse." twitter_consumer_key: "Consumer key for Twitter authentication, registered at https://developer.twitter.com/apps" @@ -2390,6 +2391,7 @@ en: leading_trailing_slash: "The regular expression must not start and end with a slash." 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." placeholder: discourse_connect_provider_secrets: diff --git a/config/routes.rb b/config/routes.rb index 041b713b7d..1f75aaa30c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -634,6 +634,8 @@ Discourse::Application.routes.draw do end end + resources :associated_groups, only: %i[index], constraints: AdminConstraint.new + # aliases so old API code works delete "admin/groups/:id/members" => "groups#remove_member", constraints: AdminConstraint.new put "admin/groups/:id/members" => "groups#add_members", constraints: AdminConstraint.new diff --git a/config/site_settings.yml b/config/site_settings.yml index f2019ac435..ff5e68b359 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -411,6 +411,9 @@ login: - "select_account" google_oauth2_hd: default: "" + google_oauth2_hd_groups: + default: false + validator: GoogleOauth2HdGroupsValidator enable_twitter_logins: default: false twitter_consumer_key: diff --git a/db/migrate/20211106085344_create_associated_groups.rb b/db/migrate/20211106085344_create_associated_groups.rb new file mode 100644 index 0000000000..72c441fe5f --- /dev/null +++ b/db/migrate/20211106085344_create_associated_groups.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +class CreateAssociatedGroups < ActiveRecord::Migration[6.1] + def change + create_table :associated_groups do |t| + t.string :name, null: false + t.string :provider_name, null: false + t.string :provider_id, null: false + t.datetime :last_used, null: false, default: -> { "CURRENT_TIMESTAMP" } + + t.timestamps + end + + add_index :associated_groups, %i[provider_name provider_id], unique: true, name: 'associated_groups_provider_id' + end +end diff --git a/db/migrate/20211106085527_create_user_associated_groups.rb b/db/migrate/20211106085527_create_user_associated_groups.rb new file mode 100644 index 0000000000..73464b66f4 --- /dev/null +++ b/db/migrate/20211106085527_create_user_associated_groups.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +class CreateUserAssociatedGroups < ActiveRecord::Migration[6.1] + def change + create_table :user_associated_groups do |t| + t.references :user, null: false + t.references :associated_group, null: false + + t.timestamps + end + + add_index :user_associated_groups, %i[user_id associated_group_id], unique: true, name: 'index_user_associated_groups' + end +end diff --git a/db/migrate/20211106085605_create_group_associated_groups.rb b/db/migrate/20211106085605_create_group_associated_groups.rb new file mode 100644 index 0000000000..281adb11b1 --- /dev/null +++ b/db/migrate/20211106085605_create_group_associated_groups.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +class CreateGroupAssociatedGroups < ActiveRecord::Migration[6.1] + def change + create_table :group_associated_groups do |t| + t.references :group, null: false + t.references :associated_group, null: false + + t.timestamps + end + + add_index :group_associated_groups, %i[group_id associated_group_id], unique: true, name: 'index_group_associated_groups' + end +end diff --git a/lib/auth.rb b/lib/auth.rb index f501d90157..21e2716250 100644 --- a/lib/auth.rb +++ b/lib/auth.rb @@ -6,6 +6,7 @@ require 'auth/auth_provider' require 'auth/result' require 'auth/authenticator' require 'auth/managed_authenticator' +require 'auth/omniauth_strategies/discourse_google_oauth2' require 'auth/facebook_authenticator' require 'auth/github_authenticator' require 'auth/twitter_authenticator' diff --git a/lib/auth/authenticator.rb b/lib/auth/authenticator.rb index 2302d3bce0..4333e87d63 100644 --- a/lib/auth/authenticator.rb +++ b/lib/auth/authenticator.rb @@ -65,4 +65,9 @@ class Auth::Authenticator def revoke(user, skip_remote: false) raise NotImplementedError end + + # provider has implemented user group membership (or equivalent) request + def provides_groups? + false + end end diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 59b0caa2c7..02e015cd10 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -16,6 +16,7 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator end def register_middleware(omniauth) + strategy_class = Auth::OmniAuthStrategies::DiscourseGoogleOauth2 options = { setup: lambda { |env| strategy = env["omniauth.strategy"] @@ -35,8 +36,25 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator # the JWT can fail due to clock skew, so let's skip it completely. # https://github.com/zquestz/omniauth-google-oauth2/pull/392 strategy.options[:skip_jwt] = true + strategy.options[:request_groups] = provides_groups? + + if provides_groups? + strategy.options[:scope] = "#{strategy_class::DEFAULT_SCOPE},#{strategy_class::GROUPS_SCOPE}" + end } } - omniauth.provider :google_oauth2, options + omniauth.provider strategy_class, options + end + + def after_authenticate(auth_token, existing_account: nil) + result = super + if provides_groups? && (groups = auth_token[:extra][:raw_groups]) + result.associated_groups = groups.map { |group| group.slice(:id, :name) } + end + result + end + + def provides_groups? + SiteSetting.google_oauth2_hd.present? && SiteSetting.google_oauth2_hd_groups end end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index e82f1de96e..a7753050b8 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -113,14 +113,16 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result end - def after_create_account(user, auth) - auth_token = auth[:extra_data] + def after_create_account(user, auth_result) + auth_token = auth_result[:extra_data] association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) association.user = user association.save! retrieve_avatar(user, association.info["image"]) retrieve_profile(user, association.info) + + auth_result.apply_associated_attributes! end def find_user_by_email(auth_token) diff --git a/lib/auth/omniauth_strategies/discourse_google_oauth2.rb b/lib/auth/omniauth_strategies/discourse_google_oauth2.rb new file mode 100644 index 0000000000..326e26d81b --- /dev/null +++ b/lib/auth/omniauth_strategies/discourse_google_oauth2.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Auth::OmniAuthStrategies + class DiscourseGoogleOauth2 < OmniAuth::Strategies::GoogleOauth2 + GROUPS_SCOPE ||= "admin.directory.group.readonly" + GROUPS_DOMAIN ||= "admin.googleapis.com" + GROUPS_PATH ||= "/admin/directory/v1/groups" + + def extra + hash = {} + hash[:raw_info] = raw_info + hash[:raw_groups] = raw_groups if options[:request_groups] + hash + end + + def raw_groups + @raw_groups ||= begin + groups = [] + page_token = nil + groups_url = "https://#{GROUPS_DOMAIN}#{GROUPS_PATH}" + + loop do + params = { + userKey: uid + } + params[:pageToken] = page_token if page_token + + response = access_token.get(groups_url, params: params, raise_errors: false) + + if response.status == 200 + response = response.parsed + groups.push(*response['groups']) + page_token = response['nextPageToken'] + break if page_token.nil? + else + Rails.logger.error("[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}") + break + end + end + + groups + end + end + end +end diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 2b0ba30c2f..c63fb92869 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -21,7 +21,8 @@ class Auth::Result :omniauth_disallow_totp, :failed, :failed_reason, - :failed_code + :failed_code, + :associated_groups ] attr_accessor *ATTRIBUTES @@ -36,7 +37,8 @@ class Auth::Result :name, :authenticator_name, :extra_data, - :skip_email_validation + :skip_email_validation, + :associated_groups ] def [](key) @@ -94,6 +96,29 @@ class Auth::Result change_made end + def apply_associated_attributes! + if authenticator&.provides_groups? && !associated_groups.nil? + associated_group_ids = [] + + associated_groups.uniq.each do |associated_group| + begin + associated_group = AssociatedGroup.find_or_create_by( + name: associated_group[:name], + provider_id: associated_group[:id], + provider_name: extra_data[:provider] + ) + rescue ActiveRecord::RecordNotUnique + retry + end + + associated_group_ids.push(associated_group.id) + end + + user.update(associated_group_ids: associated_group_ids) + AssociatedGroup.where(id: associated_group_ids).update_all("last_used = CURRENT_TIMESTAMP") + end + end + def can_edit_name !SiteSetting.auth_overrides_name end @@ -167,6 +192,10 @@ class Auth::Result username || name || email end + def authenticator + @authenticator ||= Discourse.enabled_authenticators.find { |a| a.name == authenticator_name } + end + def resolve_username if staged_user if !username.present? || UserNameSuggester.fix_username(username) == staged_user.username diff --git a/lib/guardian/group_guardian.rb b/lib/guardian/group_guardian.rb index 36d4efc4cc..1a869e4e7f 100644 --- a/lib/guardian/group_guardian.rb +++ b/lib/guardian/group_guardian.rb @@ -36,4 +36,8 @@ module GroupGuardian SiteSetting.enable_personal_messages? && group.users.include?(user) end + + def can_associate_groups? + is_admin? && AssociatedGroup.has_provider? + end end diff --git a/lib/validators/google_oauth2_hd_groups_validator.rb b/lib/validators/google_oauth2_hd_groups_validator.rb new file mode 100644 index 0000000000..b4c3f91431 --- /dev/null +++ b/lib/validators/google_oauth2_hd_groups_validator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class GoogleOauth2HdGroupsValidator + def initialize(opts = {}) + @opts = opts + end + + def valid_value?(value) + @valid = value == "f" || SiteSetting.google_oauth2_hd.present? + end + + def error_message + I18n.t("site_settings.errors.google_oauth2_hd_groups") if !@valid + end +end diff --git a/spec/components/auth/google_oauth2_authenticator_spec.rb b/spec/components/auth/google_oauth2_authenticator_spec.rb index 71bd5ccbc0..b82649070d 100644 --- a/spec/components/auth/google_oauth2_authenticator_spec.rb +++ b/spec/components/auth/google_oauth2_authenticator_spec.rb @@ -3,7 +3,6 @@ require 'rails_helper' describe Auth::GoogleOAuth2Authenticator do - it 'does not look up user unless email is verified' do # note, emails that come back from google via omniauth are always valid # this protects against future regressions @@ -113,6 +112,61 @@ describe Auth::GoogleOAuth2Authenticator do expect(result.user).to eq(nil) expect(result.name).to eq("Jane Doe") end + + context "provides groups" do + before do + SiteSetting.google_oauth2_hd = "domain.com" + group1 = OmniAuth::AuthHash.new(id: "12345", name: "group1") + group2 = OmniAuth::AuthHash.new(id: "67890", name: "group2") + @groups = [group1, group2] + @groups_hash = OmniAuth::AuthHash.new( + provider: "google_oauth2", + uid: "123456789", + info: { + first_name: "Jane", + last_name: "Doe", + name: "Jane Doe", + email: "jane.doe@the.google.com" + }, + extra: { + raw_info: { + email: "jane.doe@the.google.com", + email_verified: true, + name: "Jane Doe" + }, + raw_groups: @groups + } + ) + end + + context "enabled" do + before do + SiteSetting.google_oauth2_hd_groups = true + end + + it "adds associated groups" do + result = described_class.new.after_authenticate(@groups_hash) + expect(result.associated_groups).to eq(@groups) + end + + it "handles a blank groups array" do + @groups_hash[:extra][:raw_groups] = [] + result = described_class.new.after_authenticate(@groups_hash) + expect(result.associated_groups).to eq([]) + end + end + + context "disabled" do + before do + SiteSetting.google_oauth2_hd_groups = false + end + + it "doesnt add associated groups" do + result = described_class.new.after_authenticate(@groups_hash) + expect(result.associated_groups).to eq(nil) + end + end + end end context 'revoke' do diff --git a/spec/components/auth/managed_authenticator_spec.rb b/spec/components/auth/managed_authenticator_spec.rb index f98419ac67..2f6e371a0e 100644 --- a/spec/components/auth/managed_authenticator_spec.rb +++ b/spec/components/auth/managed_authenticator_spec.rb @@ -38,6 +38,12 @@ describe Auth::ManagedAuthenticator do ) } + def create_auth_result(attrs) + auth_result = Auth::Result.new + attrs.each { |k, v| auth_result.send("#{k}=", v) } + auth_result + end + describe 'after_authenticate' do it 'can match account from an existing association' do user = Fabricate(:user) @@ -250,14 +256,14 @@ describe Auth::ManagedAuthenticator do let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") } it "doesn't schedule with no image" do - expect { result = authenticator.after_create_account(user, extra_data: create_hash) } + expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) } .to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0) end it "schedules with image" do association.info["image"] = "https://some.domain/image.jpg" association.save! - expect { result = authenticator.after_create_account(user, extra_data: create_hash) } + expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) } .to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1) end end @@ -267,14 +273,14 @@ describe Auth::ManagedAuthenticator do let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") } it "doesn't explode without profile" do - authenticator.after_create_account(user, extra_data: create_hash) + authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) end it "works with profile" do association.info["location"] = "DiscourseVille" association.info["description"] = "Online forum expert" association.save! - authenticator.after_create_account(user, extra_data: create_hash) + authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) expect(user.user_profile.bio_raw).to eq("Online forum expert") expect(user.user_profile.location).to eq("DiscourseVille") end diff --git a/spec/components/auth/omniauth_strategies/discourse_google_oauth2_spec.rb b/spec/components/auth/omniauth_strategies/discourse_google_oauth2_spec.rb new file mode 100644 index 0000000000..e5eda8ff6b --- /dev/null +++ b/spec/components/auth/omniauth_strategies/discourse_google_oauth2_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Auth::OmniAuthStrategies::DiscourseGoogleOauth2 do + let(:response_hash) do + { + email: 'user@domain.com', + email_verified: true + } + end + let(:groups) do + [ + { + id: "12345", + name: "group1" + }, + { + id: "67890", + name: "group2" + } + ] + end + let(:uid) { "12345" } + let(:domain) { "domain.com" } + + def build_response(body, code = 200) + [code, { 'Content-Type' => 'application/json' }, body.to_json] + end + + def build_client(groups_response) + OAuth2::Client.new('abc', 'def') do |builder| + builder.request :url_encoded + builder.adapter :test do |stub| + stub.get('/oauth2/v3/userinfo') { build_response(response_hash) } + stub.get(described_class::GROUPS_PATH) { groups_response } + end + end + end + + let(:successful_groups_client) do + build_client( + build_response( + groups: groups + ) + ) + end + + let(:unsuccessful_groups_client) do + build_client( + build_response( + error: { + code: 403, + message: "Not Authorized to access this resource/api" + } + ) + ) + end + + let(:successful_groups_token) do + OAuth2::AccessToken.from_hash(successful_groups_client, {}) + end + + let(:unsuccessful_groups_token) do + OAuth2::AccessToken.from_hash(unsuccessful_groups_client, {}) + end + + def app + lambda do |_env| + [200, {}, ["Hello."]] + end + end + + def build_strategy(access_token) + strategy = described_class.new(app, 'appid', 'secret', @options) + strategy.stubs(:uid).returns(uid) + strategy.stubs(:access_token).returns(access_token) + strategy + end + + before do + @options = {} + OmniAuth.config.test_mode = true + end + + after do + OmniAuth.config.test_mode = false + end + + context 'request_groups is true' do + before do + @options[:request_groups] = true + end + + context 'groups request successful' do + before do + @strategy = build_strategy(successful_groups_token) + end + + it 'should include users groups' do + expect(@strategy.extra[:raw_groups].map(&:symbolize_keys)).to eq(groups) + end + end + + context 'groups request unsuccessful' do + before do + @strategy = build_strategy(unsuccessful_groups_token) + end + + it 'users groups should be empty' do + expect(@strategy.extra[:raw_groups].empty?).to eq(true) + end + end + end + + context 'request_groups is not true' do + before do + @options[:request_groups] = false + @strategy = build_strategy(successful_groups_token) + end + + it 'should not include users groups' do + expect(@strategy.extra).not_to have_key(:raw_groups) + end + end +end diff --git a/spec/fabricators/associated_group_fabricator.rb b/spec/fabricators/associated_group_fabricator.rb new file mode 100644 index 0000000000..f7aff76893 --- /dev/null +++ b/spec/fabricators/associated_group_fabricator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Fabricator(:associated_group) do + name { sequence(:name) { |n| "group_#{n}" } } + provider_name 'google' + provider_id { SecureRandom.hex(20) } +end diff --git a/spec/models/associated_group_spec.rb b/spec/models/associated_group_spec.rb new file mode 100644 index 0000000000..3127b35e29 --- /dev/null +++ b/spec/models/associated_group_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe AssociatedGroup do + let(:user) { Fabricate(:user) } + let(:associated_group) { Fabricate(:associated_group) } + let(:group) { Fabricate(:group) } + + it "generates a label" do + ag = described_class.new(name: "group1", provider_name: "google") + expect(ag.label).to eq("google:group1") + end + + it "detects whether any auth providers provide associated groups" do + SiteSetting.enable_google_oauth2_logins = true + SiteSetting.google_oauth2_hd = 'domain.com' + SiteSetting.google_oauth2_hd_groups = false + expect(described_class.has_provider?).to eq(false) + + SiteSetting.google_oauth2_hd_groups = true + expect(described_class.has_provider?).to eq(true) + end + + context "cleanup!" do + before do + associated_group.last_used = 8.days.ago + associated_group.save + end + + it "deletes associated groups not used in over a week" do + described_class.cleanup! + expect(described_class.exists?(associated_group.id)).to eq(false) + end + + it "doesnt delete associated groups associated with groups" do + GroupAssociatedGroup.create(group_id: group.id, associated_group_id: associated_group.id) + described_class.cleanup! + expect(described_class.exists?(associated_group.id)).to eq(true) + end + + it "doesnt delete associated groups associated with users" do + UserAssociatedGroup.create(user_id: user.id, associated_group_id: associated_group.id) + described_class.cleanup! + expect(described_class.exists?(associated_group.id)).to eq(true) + end + end +end diff --git a/spec/models/group_associated_group_spec.rb b/spec/models/group_associated_group_spec.rb new file mode 100644 index 0000000000..213a7afecf --- /dev/null +++ b/spec/models/group_associated_group_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe GroupAssociatedGroup do + let(:user) { Fabricate(:user) } + let(:group) { Fabricate(:group) } + let(:group2) { Fabricate(:group) } + let(:associated_group) { Fabricate(:associated_group) } + let(:associated_group2) { Fabricate(:associated_group) } + + before do + UserAssociatedGroup.create(user_id: user.id, associated_group_id: associated_group.id) + @gag = described_class.create(group_id: group.id, associated_group_id: associated_group.id) + end + + it "adds users to group when created" do + expect(group.users.include?(user)).to eq(true) + end + + it "removes users from group when destroyed" do + @gag.destroy! + expect(group.users.include?(user)).to eq(false) + end + + it "does not remove users with multiple associations to group when destroyed" do + UserAssociatedGroup.create(user_id: user.id, associated_group_id: associated_group2.id) + described_class.create(group_id: group.id, associated_group_id: associated_group2.id) + + @gag.destroy! + expect(group.users.include?(user)).to eq(true) + end + + it "removes users with multiple associations to other groups when destroyed" do + UserAssociatedGroup.create(user_id: user.id, associated_group_id: associated_group2.id) + described_class.create(group_id: group2.id, associated_group_id: associated_group2.id) + + @gag.destroy! + expect(group.users.include?(user)).to eq(false) + end +end diff --git a/spec/models/user_associated_group_spec.rb b/spec/models/user_associated_group_spec.rb new file mode 100644 index 0000000000..c686cea938 --- /dev/null +++ b/spec/models/user_associated_group_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe UserAssociatedGroup do + let(:user) { Fabricate(:user) } + let(:group) { Fabricate(:group) } + let(:group2) { Fabricate(:group) } + let(:associated_group) { Fabricate(:associated_group) } + let(:associated_group2) { Fabricate(:associated_group) } + + before do + GroupAssociatedGroup.create(group_id: group.id, associated_group_id: associated_group.id) + @uag = described_class.create(user_id: user.id, associated_group_id: associated_group.id) + end + + it "adds user to group when created" do + expect(group.users.include?(user)).to eq(true) + end + + it "removes user from group when destroyed" do + @uag.destroy! + expect(group.users.include?(user)).to eq(false) + end + + it "does not remove user with multiple associations from group when destroyed" do + GroupAssociatedGroup.create(group_id: group.id, associated_group_id: associated_group2.id) + described_class.create(user_id: user.id, associated_group_id: associated_group2.id) + + @uag.destroy! + expect(group.users.include?(user)).to eq(true) + end + + it "removes users with multiple associations to other groups when destroyed" do + GroupAssociatedGroup.create(group_id: group2.id, associated_group_id: associated_group2.id) + described_class.create(user_id: user.id, associated_group_id: associated_group2.id) + + @uag.destroy! + expect(group.users.include?(user)).to eq(false) + end +end diff --git a/spec/requests/api/schemas/json/group_response.json b/spec/requests/api/schemas/json/group_response.json index c4b31112f6..a3bf6d151c 100644 --- a/spec/requests/api/schemas/json/group_response.json +++ b/spec/requests/api/schemas/json/group_response.json @@ -251,6 +251,12 @@ "allow_unknown_sender_topic_replies": { "type": "boolean" }, + "associated_group_ids": { + "type": "array", + "items": [ + + ] + }, "watching_category_ids": { "type": "array", "items": [ diff --git a/spec/requests/api/schemas/json/site_response.json b/spec/requests/api/schemas/json/site_response.json index 82ae9485d0..ce4bb9c6f1 100644 --- a/spec/requests/api/schemas/json/site_response.json +++ b/spec/requests/api/schemas/json/site_response.json @@ -358,6 +358,9 @@ "wizard_required": { "type": "boolean" }, + "can_associate_groups": { + "type": "boolean" + }, "topic_featured_link_allowed_category_ids": { "type": "array", "items": [ diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index 8c594d555b..8d2a0c2a5a 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -694,6 +694,91 @@ RSpec.describe Users::OmniauthCallbacksController do expect(data["username"]).to eq(fixed_username) end + + context "groups are enabled" do + let(:strategy_class) { Auth::OmniAuthStrategies::DiscourseGoogleOauth2 } + let(:groups_url) { "#{strategy_class::GROUPS_DOMAIN}#{strategy_class::GROUPS_PATH}" } + let(:groups_scope) { strategy_class::DEFAULT_SCOPE + strategy_class::GROUPS_SCOPE } + let(:group1) { { id: "12345", name: "group1" } } + let(:group2) { { id: "67890", name: "group2" } } + let(:uid) { "12345" } + let(:token) { "1245678" } + let(:domain) { "mydomain.com" } + + def mock_omniauth_for_groups(groups) + raw_groups = groups.map { |group| OmniAuth::AuthHash.new(group) } + mock_auth = OmniAuth.config.mock_auth[:google_oauth2] + mock_auth[:extra][:raw_groups] = raw_groups + OmniAuth.config.mock_auth[:google_oauth2] = mock_auth + Rails.application.env_config["omniauth.auth"] = mock_auth + end + + before do + SiteSetting.google_oauth2_hd = domain + SiteSetting.google_oauth2_hd_groups = true + end + + it "updates associated groups" do + mock_omniauth_for_groups([group1, group2]) + get "/auth/google_oauth2/callback.json", params: { + scope: groups_scope.split(' '), + code: 'abcde', + hd: domain + } + expect(response.status).to eq(302) + + associated_groups = AssociatedGroup.where(provider_name: 'google_oauth2') + expect(associated_groups.length).to eq(2) + expect(associated_groups.exists?(name: group1[:name])).to eq(true) + expect(associated_groups.exists?(name: group2[:name])).to eq(true) + + user_associated_groups = UserAssociatedGroup.where(user_id: user.id) + expect(user_associated_groups.length).to eq(2) + expect(user_associated_groups.exists?(associated_group_id: associated_groups.first.id)).to eq(true) + expect(user_associated_groups.exists?(associated_group_id: associated_groups.second.id)).to eq(true) + + mock_omniauth_for_groups([group1]) + get "/auth/google_oauth2/callback.json", params: { + scope: groups_scope.split(' '), + code: 'abcde', + hd: domain + } + expect(response.status).to eq(302) + + user_associated_groups = UserAssociatedGroup.where(user_id: user.id) + expect(user_associated_groups.length).to eq(1) + expect(user_associated_groups.exists?(associated_group_id: associated_groups.first.id)).to eq(true) + expect(user_associated_groups.exists?(associated_group_id: associated_groups.second.id)).to eq(false) + + mock_omniauth_for_groups([]) + get "/auth/google_oauth2/callback.json", params: { + scope: groups_scope.split(' '), + code: 'abcde', + hd: domain + } + expect(response.status).to eq(302) + + user_associated_groups = UserAssociatedGroup.where(user_id: user.id) + expect(user_associated_groups.length).to eq(0) + expect(user_associated_groups.exists?(associated_group_id: associated_groups.first.id)).to eq(false) + expect(user_associated_groups.exists?(associated_group_id: associated_groups.second.id)).to eq(false) + end + + it "handles failure to retrieve groups" do + mock_omniauth_for_groups([]) + + get "/auth/google_oauth2/callback.json", params: { + scope: groups_scope.split(' '), + code: 'abcde', + hd: domain + } + + expect(response.status).to eq(302) + + associated_groups = AssociatedGroup.where(provider_name: 'google_oauth2') + expect(associated_groups.exists?).to eq(false) + end + end end context 'when attempting reconnect' do From 76dff7fd9e2aceeed1c01c7e807f458ac5e1ed80 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 9 Dec 2021 13:47:13 +0100 Subject: [PATCH 090/119] DEV: drops jquery usage from discovery-categories (#15243) --- .../app/components/discovery-categories.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/discovery-categories.js b/app/assets/javascripts/discourse/app/components/discovery-categories.js index 4be34e7e4f..3843a4366e 100644 --- a/app/assets/javascripts/discourse/app/components/discovery-categories.js +++ b/app/assets/javascripts/discourse/app/components/discovery-categories.js @@ -1,19 +1,20 @@ import Component from "@ember/component"; import UrlRefresh from "discourse/mixins/url-refresh"; -import { on } from "discourse-common/utils/decorators"; const CATEGORIES_LIST_BODY_CLASS = "categories-list"; export default Component.extend(UrlRefresh, { classNames: ["contents"], - @on("didInsertElement") - addBodyClass() { - $("body").addClass(CATEGORIES_LIST_BODY_CLASS); + didInsertElement() { + this._super(...arguments); + + document.body.classList.add(CATEGORIES_LIST_BODY_CLASS); }, - @on("willDestroyElement") - removeBodyClass() { - $("body").removeClass(CATEGORIES_LIST_BODY_CLASS); + willDestroyElement() { + this._super(...arguments); + + document.body.classList.remove(CATEGORIES_LIST_BODY_CLASS); }, }); From 5d44adb9b978d296f99bd00adf75765ee06f76e5 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 9 Dec 2021 13:47:47 +0100 Subject: [PATCH 091/119] DEV: refactors d-section (#15245) - go tagless - properly declares properties - deprecates "false" in favour of false - drops jquery --- .../discourse/app/components/d-section.js | 36 ++++++++++++------- .../discourse/app/templates/d-section.hbs | 1 + .../app/templates/navigation/default.hbs | 2 +- .../discourse/app/templates/user/activity.hbs | 2 +- 4 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/templates/d-section.hbs diff --git a/app/assets/javascripts/discourse/app/components/d-section.js b/app/assets/javascripts/discourse/app/components/d-section.js index 5197680242..0ca4b4f6e4 100644 --- a/app/assets/javascripts/discourse/app/components/d-section.js +++ b/app/assets/javascripts/discourse/app/components/d-section.js @@ -1,24 +1,35 @@ +import deprecated from "discourse-common/lib/deprecated"; import Component from "@ember/component"; import { scrollTop } from "discourse/mixins/scroll-top"; // Can add a body class from within a component, also will scroll to the top automatically. export default Component.extend({ - tagName: "section", + tagName: null, + pageClass: null, + bodyClass: null, + scrollTop: true, didInsertElement() { this._super(...arguments); - const pageClass = this.pageClass; - if (pageClass) { - $("body").addClass(`${pageClass}-page`); + if (this.pageClass) { + document.body.classList.add(`${this.pageClass}-page`); } - const bodyClass = this.bodyClass; - if (bodyClass) { - $("body").addClass(bodyClass); + if (this.bodyClass) { + document.body.classList.add(this.bodyClass); } if (this.scrollTop === "false") { + deprecated("Uses boolean instead of string for scrollTop.", { + since: "2.8.0.beta9", + dropFrom: "2.9.0.beta1", + }); + + return; + } + + if (!this.scrollTop) { return; } @@ -27,14 +38,13 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - const pageClass = this.pageClass; - if (pageClass) { - $("body").removeClass(`${pageClass}-page`); + + if (this.pageClass) { + document.body.classList.remove(`${this.pageClass}-page`); } - const bodyClass = this.bodyClass; - if (bodyClass) { - $("body").removeClass(bodyClass); + if (this.bodyClass) { + document.body.classList.remove(this.bodyClass); } }, }); diff --git a/app/assets/javascripts/discourse/app/templates/d-section.hbs b/app/assets/javascripts/discourse/app/templates/d-section.hbs new file mode 100644 index 0000000000..200a9f4462 --- /dev/null +++ b/app/assets/javascripts/discourse/app/templates/d-section.hbs @@ -0,0 +1 @@ +
    {{yield}}
    diff --git a/app/assets/javascripts/discourse/app/templates/navigation/default.hbs b/app/assets/javascripts/discourse/app/templates/navigation/default.hbs index 8ce72b8ebe..df1f1fa5e7 100644 --- a/app/assets/javascripts/discourse/app/templates/navigation/default.hbs +++ b/app/assets/javascripts/discourse/app/templates/navigation/default.hbs @@ -1,4 +1,4 @@ -{{#d-section bodyClass="navigation-topics" class="navigation-container" scrollTop="false"}} +{{#d-section bodyClass="navigation-topics" class="navigation-container" scrollTop=false}} {{d-navigation filterMode=filterMode canCreateTopic=canCreateTopic diff --git a/app/assets/javascripts/discourse/app/templates/user/activity.hbs b/app/assets/javascripts/discourse/app/templates/user/activity.hbs index 260e40241c..6520ce48a8 100644 --- a/app/assets/javascripts/discourse/app/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/activity.hbs @@ -1,4 +1,4 @@ -{{#d-section pageClass="user-activity" class="user-secondary-navigation" scrollTop="false"}} +{{#d-section pageClass="user-activity" class="user-secondary-navigation" scrollTop=false}}